From 41f4b1e6d1e4d79f7027b16c4b4a3bd6248cb1a0 Mon Sep 17 00:00:00 2001 From: mekya Date: Thu, 18 Jan 2024 22:57:17 +0300 Subject: [PATCH 1/9] Initial implementation for FCM push notification --- pom.xml | 5 ++ src/main/java/io/antmedia/AppSettings.java | 35 ++++++++- .../io/antmedia/datastore/db/DataStore.java | 13 ++++ .../datastore/db/InMemoryDataStore.java | 12 ++++ .../io/antmedia/datastore/db/MapDBStore.java | 12 ++++ .../io/antmedia/datastore/db/MongoStore.java | 12 ++++ .../io/antmedia/datastore/db/RedisStore.java | 12 ++++ .../db/types/PushNotificationToken.java | 60 ++++++++++++++++ .../datastore/db/types/Subscriber.java | 2 +- .../db/types/SubscriberMetadata.java | 56 +++++++++++++++ .../io/antmedia/datastore/db/types/User.java | 2 +- .../IPushNotificationService.java | 66 +++++++++++++++++ .../PushNotificationServiceCommunity.java | 32 +++++++++ .../rest/PushNotificationService.java | 44 ++++++++++++ .../java/io/antmedia/rest/model/Result.java | 4 +- .../websocket/WebSocketConstants.java | 47 ++++++++++++ .../AntMediaApplicationAdaptorUnitTest.java | 72 +++++++++++++++++++ 17 files changed, 481 insertions(+), 5 deletions(-) create mode 100644 src/main/java/io/antmedia/datastore/db/types/PushNotificationToken.java create mode 100644 src/main/java/io/antmedia/datastore/db/types/SubscriberMetadata.java create mode 100644 src/main/java/io/antmedia/pushnotification/IPushNotificationService.java create mode 100644 src/main/java/io/antmedia/pushnotification/PushNotificationServiceCommunity.java create mode 100644 src/main/java/io/antmedia/rest/PushNotificationService.java diff --git a/pom.xml b/pom.xml index df5061f15..7821a2274 100644 --- a/pom.xml +++ b/pom.xml @@ -614,6 +614,11 @@ ${redisson.version} provided + + com.google.firebase + firebase-admin + 9.2.0 + diff --git a/src/main/java/io/antmedia/AppSettings.java b/src/main/java/io/antmedia/AppSettings.java index 829d95c93..a192556f1 100644 --- a/src/main/java/io/antmedia/AppSettings.java +++ b/src/main/java/io/antmedia/AppSettings.java @@ -2052,7 +2052,23 @@ public boolean isWriteStatsToDatastore() { */ @Value("${sendAudioLevelToViewers:true}") private boolean sendAudioLevelToViewers = true; - + + /** + * Firebase Service Account Key JSON to send push notification + * through Firebase Cloud Messaging + */ + @Value("${firebaseAccountKeyJSON:#{null}}") + private String firebaseAccountKeyJSON = null; + + /** + * This is JWT Secret to authenticate the user for push notifications. + * + * JWT token should be generated with the following secret: subscriberId(username, email, etc.) + subscriberAuthenticationKey + * + */ + @Value("${subscriberAuthenticationKey:#{ T(org.apache.commons.lang3.RandomStringUtils).randomAlphanumeric(32)}}") + private String subscriberAuthenticationKey = RandomStringUtils.randomAlphanumeric(32); + public void setWriteStatsToDatastore(boolean writeStatsToDatastore) { this.writeStatsToDatastore = writeStatsToDatastore; } @@ -3532,4 +3548,21 @@ public String getTimeTokenSecretForPlay() { public void setTimeTokenSecretForPlay(String timeTokenSecretForPlay) { this.timeTokenSecretForPlay = timeTokenSecretForPlay; } + + public String getFirebaseAccountKeyJSON() { + return firebaseAccountKeyJSON; + } + + public void setFirebaseAccountKeyJSON(String firebaseAccountKeyJSON) { + this.firebaseAccountKeyJSON = firebaseAccountKeyJSON; + } + + public String getSubscriberAuthenticationKey() { + return subscriberAuthenticationKey; + } + + public void setSubscriberAuthenticationKey(String subscriberAuthenticationKey) { + this.subscriberAuthenticationKey = subscriberAuthenticationKey; + } + } diff --git a/src/main/java/io/antmedia/datastore/db/DataStore.java b/src/main/java/io/antmedia/datastore/db/DataStore.java index 938dc7987..f42cabbae 100644 --- a/src/main/java/io/antmedia/datastore/db/DataStore.java +++ b/src/main/java/io/antmedia/datastore/db/DataStore.java @@ -24,8 +24,10 @@ import io.antmedia.datastore.db.types.ConnectionEvent; import io.antmedia.datastore.db.types.Endpoint; import io.antmedia.datastore.db.types.P2PConnection; +import io.antmedia.datastore.db.types.PushNotificationToken; import io.antmedia.datastore.db.types.StreamInfo; import io.antmedia.datastore.db.types.Subscriber; +import io.antmedia.datastore.db.types.SubscriberMetadata; import io.antmedia.datastore.db.types.SubscriberStats; import io.antmedia.datastore.db.types.TensorFlowObject; import io.antmedia.datastore.db.types.Token; @@ -1404,6 +1406,17 @@ public List getWebRTCViewerList(Map webRTCView * @param metaData new meta data */ public abstract boolean updateStreamMetaData(String streamId, String metaData); + + /** + * Saves push notification token to the database to send push notification later + * + * @param subscriberId + * @param pushNotificationToken + * @return true if it's successful, false if it fails + */ + public abstract boolean save(String subscriberId, PushNotificationToken pushNotificationToken); + + public abstract SubscriberMetadata getSubscriberMetaData(String subscriberId); //************************************** diff --git a/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java b/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java index 3304d4c9a..b0a44394b 100644 --- a/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java +++ b/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java @@ -21,8 +21,10 @@ import io.antmedia.datastore.db.types.ConferenceRoom; import io.antmedia.datastore.db.types.Endpoint; import io.antmedia.datastore.db.types.P2PConnection; +import io.antmedia.datastore.db.types.PushNotificationToken; import io.antmedia.datastore.db.types.StreamInfo; import io.antmedia.datastore.db.types.Subscriber; +import io.antmedia.datastore.db.types.SubscriberMetadata; import io.antmedia.datastore.db.types.TensorFlowObject; import io.antmedia.datastore.db.types.Token; import io.antmedia.datastore.db.types.VoD; @@ -1085,4 +1087,14 @@ public boolean updateStreamMetaData(String streamId, String metaData) { } return result; } + + @Override + public SubscriberMetadata getSubscriberMetaData(String subscriberId) { + return null; + } + + @Override + public boolean save(String subscriberId, PushNotificationToken pushNotificationToken) { + return false; + } } \ No newline at end of file diff --git a/src/main/java/io/antmedia/datastore/db/MapDBStore.java b/src/main/java/io/antmedia/datastore/db/MapDBStore.java index d5c54c2f5..0533d5644 100644 --- a/src/main/java/io/antmedia/datastore/db/MapDBStore.java +++ b/src/main/java/io/antmedia/datastore/db/MapDBStore.java @@ -18,7 +18,9 @@ import org.slf4j.LoggerFactory; import io.antmedia.datastore.db.types.Broadcast; +import io.antmedia.datastore.db.types.PushNotificationToken; import io.antmedia.datastore.db.types.StreamInfo; +import io.antmedia.datastore.db.types.SubscriberMetadata; import io.antmedia.muxer.IAntMediaStreamHandler; import io.vertx.core.Vertx; @@ -146,5 +148,15 @@ public List getStreamInfoList(String streamId) { public void saveStreamInfo(StreamInfo streamInfo) { //no need to implement this method, it is used in cluster mode } + + @Override + public SubscriberMetadata getSubscriberMetaData(String subscriberId) { + return null; + } + + @Override + public boolean save(String subscriberId, PushNotificationToken pushNotificationToken) { + return false; + } } diff --git a/src/main/java/io/antmedia/datastore/db/MongoStore.java b/src/main/java/io/antmedia/datastore/db/MongoStore.java index 5fae12ddc..6b8c37bfe 100644 --- a/src/main/java/io/antmedia/datastore/db/MongoStore.java +++ b/src/main/java/io/antmedia/datastore/db/MongoStore.java @@ -43,8 +43,10 @@ import io.antmedia.datastore.db.types.ConferenceRoom; import io.antmedia.datastore.db.types.Endpoint; import io.antmedia.datastore.db.types.P2PConnection; +import io.antmedia.datastore.db.types.PushNotificationToken; import io.antmedia.datastore.db.types.StreamInfo; import io.antmedia.datastore.db.types.Subscriber; +import io.antmedia.datastore.db.types.SubscriberMetadata; import io.antmedia.datastore.db.types.TensorFlowObject; import io.antmedia.datastore.db.types.Token; import io.antmedia.datastore.db.types.VoD; @@ -1529,4 +1531,14 @@ public boolean updateStreamMetaData(String streamId, String metaData) { public Datastore getSubscriberDatastore() { return subscriberDatastore; } + + @Override + public SubscriberMetadata getSubscriberMetaData(String subscriberId) { + return null; + } + + @Override + public boolean save(String subscriberId, PushNotificationToken pushNotificationToken) { + return false; + } } diff --git a/src/main/java/io/antmedia/datastore/db/RedisStore.java b/src/main/java/io/antmedia/datastore/db/RedisStore.java index 351c5aea7..d3047a5e2 100644 --- a/src/main/java/io/antmedia/datastore/db/RedisStore.java +++ b/src/main/java/io/antmedia/datastore/db/RedisStore.java @@ -19,7 +19,9 @@ import io.antmedia.datastore.db.types.Broadcast; import io.antmedia.datastore.db.types.P2PConnection; +import io.antmedia.datastore.db.types.PushNotificationToken; import io.antmedia.datastore.db.types.StreamInfo; +import io.antmedia.datastore.db.types.SubscriberMetadata; import io.antmedia.muxer.IAntMediaStreamHandler; public class RedisStore extends MapBasedDataStore { @@ -181,6 +183,16 @@ public List getLocalLiveBroadcasts(String hostAddress) return getActiveBroadcastList(hostAddress); } + + @Override + public SubscriberMetadata getSubscriberMetaData(String subscriberId) { + return null; + } + + @Override + public boolean save(String subscriberId, PushNotificationToken pushNotificationToken) { + return false; + } } diff --git a/src/main/java/io/antmedia/datastore/db/types/PushNotificationToken.java b/src/main/java/io/antmedia/datastore/db/types/PushNotificationToken.java new file mode 100644 index 000000000..8306da501 --- /dev/null +++ b/src/main/java/io/antmedia/datastore/db/types/PushNotificationToken.java @@ -0,0 +1,60 @@ +package io.antmedia.datastore.db.types; + +import dev.morphia.annotations.Entity; +import io.swagger.annotations.ApiModel; + +@ApiModel(value="PushNotificationToken", description="The endpoint class, such as Facebook, Twitter or custom RTMP endpoints") +@Entity +public class PushNotificationToken { + + private String token; + + /** + * fcm or apn + * fcm: Firebase Cloud Messagnig + * apn: Apple Push Notification + */ + private String serviceName; + + private String extraData; + + //Keep empty constructor for construction from mongodb and similar ways. + public PushNotificationToken() { + } + + public PushNotificationToken(String token, String serviceName) { + super(); + this.setToken(token); + this.setServiceName(serviceName); + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getExtraData() { + return extraData; + } + + public void setExtraData(String extraData) { + this.extraData = extraData; + } + + + + + + +} diff --git a/src/main/java/io/antmedia/datastore/db/types/Subscriber.java b/src/main/java/io/antmedia/datastore/db/types/Subscriber.java index 64a07b8d1..9d19f7388 100644 --- a/src/main/java/io/antmedia/datastore/db/types/Subscriber.java +++ b/src/main/java/io/antmedia/datastore/db/types/Subscriber.java @@ -16,7 +16,7 @@ @Entity("subscriber") @Indexes({ @Index(fields = @Field("subscriberId")), @Index(fields = @Field("streamId")) }) -@ApiModel(value="Subscriber", description="The time based token subscriber class") +@ApiModel(value="Subscriber", description="The time based token subscriber class. This keeps which subscriber can access to which stream and which TOTP") public class Subscriber { @JsonIgnore public static final String PLAY_TYPE = "play"; diff --git a/src/main/java/io/antmedia/datastore/db/types/SubscriberMetadata.java b/src/main/java/io/antmedia/datastore/db/types/SubscriberMetadata.java new file mode 100644 index 000000000..c2657f1e1 --- /dev/null +++ b/src/main/java/io/antmedia/datastore/db/types/SubscriberMetadata.java @@ -0,0 +1,56 @@ +package io.antmedia.datastore.db.types; + +import java.util.List; + +import org.bson.types.ObjectId; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Id; +import io.swagger.annotations.ApiModelProperty; + +@Entity +public class SubscriberMetadata { + + + @JsonIgnore + @Id + @ApiModelProperty(value = "the db id of the SubscriberMetadata") + private ObjectId dbId; + + /** + * Subscriber id. It can be username, email or any random number + */ + @ApiModelProperty(value = "the subscriber id") + private String subscriberId; + + @ApiModelProperty(value = "Push notification tokens provided by FCM and APN") + private List pushNotificationTokens; + + public ObjectId getDbId() { + return dbId; + } + + public void setDbId(ObjectId dbId) { + this.dbId = dbId; + } + + public String getSubscriberId() { + return subscriberId; + } + + public void setSubscriberId(String subscriberId) { + this.subscriberId = subscriberId; + } + + public List getPushNotificationTokens() { + return pushNotificationTokens; + } + + public void setPushNotificationTokens(List pushNotificationTokens) { + this.pushNotificationTokens = pushNotificationTokens; + } + + +} diff --git a/src/main/java/io/antmedia/datastore/db/types/User.java b/src/main/java/io/antmedia/datastore/db/types/User.java index 103fa21f5..06c1ca90e 100644 --- a/src/main/java/io/antmedia/datastore/db/types/User.java +++ b/src/main/java/io/antmedia/datastore/db/types/User.java @@ -16,7 +16,7 @@ import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; -@ApiModel(value="User", description="The basic user class") +@ApiModel(value="User", description="The basic user class for accessing the web panel") @Entity(value = "user") @Indexes({ @Index(fields = @Field("email")), @Index(fields = @Field("fullName")) }) public class User { diff --git a/src/main/java/io/antmedia/pushnotification/IPushNotificationService.java b/src/main/java/io/antmedia/pushnotification/IPushNotificationService.java new file mode 100644 index 000000000..2fbf86fc5 --- /dev/null +++ b/src/main/java/io/antmedia/pushnotification/IPushNotificationService.java @@ -0,0 +1,66 @@ +package io.antmedia.pushnotification; + +import java.util.List; + +import io.antmedia.datastore.db.types.PushNotificationToken; +import io.antmedia.rest.model.Result; + +public interface IPushNotificationService { + + static final String BEAN_NAME = "push.notification.service"; + + public enum PushNotificationServiceTypes { + FIREBASE_CLOUD_MESSAGING("fcm"), + APPLE_PUSH_NOTIFICATION("apn"); + + + private String name; + + PushNotificationServiceTypes(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + } + + + /** + * Send push notification according to service name + * + * @param registerationTokens + * @param jsonMessage + * @param serviceName: fcm or apn + * @return + */ + Result sendNotification(List registerationTokens, String jsonMessage, String serviceName); + + /** + * Send push notifcaiton according to service name + * + * @param topic + * @param jsonMessage + * @param serviceName: fcm or apn + * @return + */ + Result sendNotification(String topic, String jsonMessage, String serviceName); + + /** + * Send notification to both services if they are enabled + * + * @param topic + * @param jsonMessage + * @return + */ + Result sendNotification(String topic, String jsonMessage) ; + + /** + * Send notification to the registrationTokens + * @param registerationTokens + * @param jsonMessage + * @return + */ + Result sendNotification(List registerationTokens, String jsonMessage); +} diff --git a/src/main/java/io/antmedia/pushnotification/PushNotificationServiceCommunity.java b/src/main/java/io/antmedia/pushnotification/PushNotificationServiceCommunity.java new file mode 100644 index 000000000..6a14cc1c1 --- /dev/null +++ b/src/main/java/io/antmedia/pushnotification/PushNotificationServiceCommunity.java @@ -0,0 +1,32 @@ +package io.antmedia.pushnotification; + +import java.util.List; + +import io.antmedia.datastore.db.types.PushNotificationToken; +import io.antmedia.rest.model.Result; + +public class PushNotificationServiceCommunity implements IPushNotificationService { + + private static final String MESSAGE_TO_USE_ENTERPRISE_EDITION = "Push Notification Service is not available community edition. Please use enterprise edition"; + + @Override + public Result sendNotification(List registerationTokens, String jsonMessage, String serviceName) { + return new Result(false, MESSAGE_TO_USE_ENTERPRISE_EDITION); + } + + @Override + public Result sendNotification(String topic, String jsonMessage, String serviceName) { + return new Result(false, MESSAGE_TO_USE_ENTERPRISE_EDITION); + } + + @Override + public Result sendNotification(List registerationTokens, String jsonMessage) { + return new Result(false, MESSAGE_TO_USE_ENTERPRISE_EDITION); + } + + @Override + public Result sendNotification(String topic, String jsonMessage) { + return new Result(false, MESSAGE_TO_USE_ENTERPRISE_EDITION); + } + +} diff --git a/src/main/java/io/antmedia/rest/PushNotificationService.java b/src/main/java/io/antmedia/rest/PushNotificationService.java new file mode 100644 index 000000000..04e6b0ce6 --- /dev/null +++ b/src/main/java/io/antmedia/rest/PushNotificationService.java @@ -0,0 +1,44 @@ +package io.antmedia.rest; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import io.antmedia.rest.model.Result; +import io.swagger.annotations.Api; +import jakarta.ws.rs.Path; + +@Api(value = "") +@Component +@Path("/v2/push-notification") +public class PushNotificationService { + + + + public Result getSubscriberAuthenticationToken() { + return null; + } + + public Result sendPushNotification(List subscriberIdList, String jsonMessage, String serviceName) { + return null; + } + + + public Result sendPushNotification(String topic, String jsonMessage, String serviceName) { + return null; + } + + public Result sendDataChannelMessage(List subscriberIdList, String jsonMessage, String serviceName) { + return null; + } + + public Result sendDataChannelMessage(String subscriberId, String jsonMessage, String serviceName) { + return null; + } + + + + + + +} diff --git a/src/main/java/io/antmedia/rest/model/Result.java b/src/main/java/io/antmedia/rest/model/Result.java index c1fa91b8f..d8aabc311 100644 --- a/src/main/java/io/antmedia/rest/model/Result.java +++ b/src/main/java/io/antmedia/rest/model/Result.java @@ -39,11 +39,11 @@ public class Result { * @param message */ public Result(boolean success, String message) { - this(success, null, message); + this(success, "", message); } public Result(boolean success) { - this(success, null, null); + this(success, "", ""); } public Result(boolean success, String dataId, String message) { diff --git a/src/main/java/io/antmedia/websocket/WebSocketConstants.java b/src/main/java/io/antmedia/websocket/WebSocketConstants.java index c10c9b8de..aa57d100e 100644 --- a/src/main/java/io/antmedia/websocket/WebSocketConstants.java +++ b/src/main/java/io/antmedia/websocket/WebSocketConstants.java @@ -534,5 +534,52 @@ private WebSocketConstants() { */ public static final String BROADCAST = "broadcast"; + + public static final String SEND_PUSH_NOTIFICATION_COMMAND = "sendPushNotification"; + + public static final String REGISTER_PUSH_NOTIFICATION_TOKEN = "reigsterPushNotificationToken"; + + public static final String AUTH_TOKEN_NOT_VALID_ERROR_DEFINITION = "authenticationTokenNotValid"; + + /** + * Push Notificaiton Service Registration Token + */ + public static final String PNS_REGISTRATION_TOKEN = "pnsRegistrationToken"; + + /** + * Push Notificaiton Service type, it can fcm or apn + * FCM: Firebase Cloud Messaging + * APN: Apple Notification Service + */ + public static final String PNS_TYPE = "pnsType"; + + public static final String MISSING_PARAMETER_DEFINITION = "missingParameter"; + + /** + * Information field in websocket communication + */ + public static final String INFORMATION = "information"; + + /** + * Success field in websocket communication. If it's value true, the operation is successful. + * If it's value is false, the operation is failed + */ + public static final String SUCCESS = "success"; + + /** + * Topic field to send push notification + */ + public static final String PUSH_NOTIFICATION_TOPIC = "pushNotificationTopic"; + + /** + * Subscriber id list to send push notification + */ + public static final String SUBSCRIBER_ID_LIST_TO_NOTIFY = "subscriberIdsToNotify"; + + /** + * Push Notification Content + */ + public static final String PUSH_NOTIFICATION_CONTENT = "pushNotificationContent"; + } diff --git a/src/test/java/io/antmedia/test/AntMediaApplicationAdaptorUnitTest.java b/src/test/java/io/antmedia/test/AntMediaApplicationAdaptorUnitTest.java index 6de33fdec..ead6481d1 100644 --- a/src/test/java/io/antmedia/test/AntMediaApplicationAdaptorUnitTest.java +++ b/src/test/java/io/antmedia/test/AntMediaApplicationAdaptorUnitTest.java @@ -17,6 +17,8 @@ import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; @@ -25,6 +27,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -53,6 +56,16 @@ import org.red5.server.stream.ClientBroadcastStream; import org.springframework.context.ApplicationContext; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.MulticastMessage; +import com.google.firebase.messaging.SendResponse; + import io.antmedia.AntMediaApplicationAdapter; import io.antmedia.AppSettings; import io.antmedia.cluster.ClusterNode; @@ -147,6 +160,65 @@ public void after() { } } + + public void testFirebase() throws IOException, FirebaseMessagingException { + FileInputStream serviceAccount = + new FileInputStream("path/to/serviceAccountKey.json"); + + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .build(); + + FirebaseApp.initializeApp(options); + + { + String registrationToken = "YOUR_REGISTRATION_TOKEN"; + + // See documentation on defining a message payload. + Message message = Message.builder() + .putData("score", "850") + .putData("time", "2:45") + .setToken(registrationToken) + .build(); + + // Send a message to the device corresponding to the provided + // registration token. + String response = FirebaseMessaging.getInstance().send(message); + // Response is a message ID string. + System.out.println("Successfully sent message: " + response); + } + + { + + + List registrationTokens = Arrays.asList( + "YOUR_REGISTRATION_TOKEN_1", + // ... + "YOUR_REGISTRATION_TOKEN_n" + ); + + MulticastMessage message = MulticastMessage.builder() + .putData("score", "850") + .putData("time", "2:45") + .addAllTokens(registrationTokens) + .build(); + BatchResponse response = FirebaseMessaging.getInstance().sendMulticast(message); + + if (response.getFailureCount() > 0) { + List responses = response.getResponses(); + List failedTokens = new ArrayList<>(); + for (int i = 0; i < responses.size(); i++) { + if (!responses.get(i).isSuccessful()) { + // The order of responses corresponds to the order of the registration tokens. + failedTokens.add(registrationTokens.get(i)); + } + } + + System.out.println("List of tokens that caused failures: " + failedTokens); + } + } + + } @Test public void testIsIncomingTimeValid() { AppSettings newSettings = new AppSettings(); From 0d7f55ce33c83521a6e57ca294de1d43b416d505 Mon Sep 17 00:00:00 2001 From: mekya Date: Mon, 29 Jan 2024 10:30:10 +0300 Subject: [PATCH 2/9] Add test codes for subscriber metadata operations --- .../io/antmedia/datastore/db/DataStore.java | 14 +++-- .../datastore/db/InMemoryDataStore.java | 8 ++- .../datastore/db/MapBasedDataStore.java | 17 +++++ .../io/antmedia/datastore/db/MapDBStore.java | 55 +++++++++------- .../io/antmedia/datastore/db/MongoStore.java | 29 ++++++++- .../io/antmedia/datastore/db/RedisStore.java | 13 +--- .../db/types/PushNotificationToken.java | 2 + .../db/types/SubscriberMetadata.java | 5 ++ .../io/antmedia/test/db/DBStoresUnitTest.java | 63 ++++++++++++++++++- 9 files changed, 161 insertions(+), 45 deletions(-) diff --git a/src/main/java/io/antmedia/datastore/db/DataStore.java b/src/main/java/io/antmedia/datastore/db/DataStore.java index f42cabbae..b36cd80f4 100644 --- a/src/main/java/io/antmedia/datastore/db/DataStore.java +++ b/src/main/java/io/antmedia/datastore/db/DataStore.java @@ -1408,14 +1408,20 @@ public List getWebRTCViewerList(Map webRTCView public abstract boolean updateStreamMetaData(String streamId, String metaData); /** - * Saves push notification token to the database to send push notification later + * Put subscriber metadata. It overwrites the metadata, if you need to update something, + * first get the {@link #getSubscriberMetaData(String)} , update it and put it * * @param subscriberId - * @param pushNotificationToken - * @return true if it's successful, false if it fails + * @param SubscriberMetadata + * @return */ - public abstract boolean save(String subscriberId, PushNotificationToken pushNotificationToken); + public abstract void putSubscriberMetaData(String subscriberId, SubscriberMetadata metadata); + /** + * Get subscriber metadata + * @param subscriberId + * @return + */ public abstract SubscriberMetadata getSubscriberMetaData(String subscriberId); diff --git a/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java b/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java index b0a44394b..7b68e7a1e 100644 --- a/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java +++ b/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java @@ -40,6 +40,7 @@ public class InMemoryDataStore extends DataStore { private Map> detectionMap = new LinkedHashMap<>(); private Map tokenMap = new LinkedHashMap<>(); private Map subscriberMap = new LinkedHashMap<>(); + private Map subscriberMetadataMap = new LinkedHashMap<>(); private Map roomMap = new LinkedHashMap<>(); private Map webRTCViewerMap = new LinkedHashMap<>(); @@ -1090,11 +1091,12 @@ public boolean updateStreamMetaData(String streamId, String metaData) { @Override public SubscriberMetadata getSubscriberMetaData(String subscriberId) { - return null; + return subscriberMetadataMap.get(subscriberId); } @Override - public boolean save(String subscriberId, PushNotificationToken pushNotificationToken) { - return false; + public void putSubscriberMetaData(String subscriberId, SubscriberMetadata subscriberMetadata) { + subscriberMetadata.setSubscriberId(subscriberId); + subscriberMetadataMap.put(subscriberId, subscriberMetadata); } } \ No newline at end of file diff --git a/src/main/java/io/antmedia/datastore/db/MapBasedDataStore.java b/src/main/java/io/antmedia/datastore/db/MapBasedDataStore.java index 544618e2d..53d6fbba3 100644 --- a/src/main/java/io/antmedia/datastore/db/MapBasedDataStore.java +++ b/src/main/java/io/antmedia/datastore/db/MapBasedDataStore.java @@ -27,6 +27,7 @@ import io.antmedia.datastore.db.types.P2PConnection; import io.antmedia.datastore.db.types.StreamInfo; import io.antmedia.datastore.db.types.Subscriber; +import io.antmedia.datastore.db.types.SubscriberMetadata; import io.antmedia.datastore.db.types.TensorFlowObject; import io.antmedia.datastore.db.types.Token; import io.antmedia.datastore.db.types.VoD; @@ -44,6 +45,7 @@ public abstract class MapBasedDataStore extends DataStore { protected Map subscriberMap; protected Map conferenceRoomMap; protected Map webRTCViewerMap; + protected Map subscriberMetadataMap; public static final String REPLACE_CHARS_REGEX = "[\n|\r|\t]"; @@ -1106,5 +1108,20 @@ public Broadcast getBroadcastFromMap(String streamId) } return null; } + + @Override + public void putSubscriberMetaData(String subscriberId, SubscriberMetadata metadata) { + metadata.setSubscriberId(subscriberId); + subscriberMetadataMap.put(subscriberId, gson.toJson(metadata)); + } + + @Override + public SubscriberMetadata getSubscriberMetaData(String subscriberId) { + String jsonString = subscriberMetadataMap.get(subscriberId); + if(jsonString != null) { + return gson.fromJson(jsonString, SubscriberMetadata.class); + } + return null; + } } diff --git a/src/main/java/io/antmedia/datastore/db/MapDBStore.java b/src/main/java/io/antmedia/datastore/db/MapDBStore.java index 0533d5644..a05e127ec 100644 --- a/src/main/java/io/antmedia/datastore/db/MapDBStore.java +++ b/src/main/java/io/antmedia/datastore/db/MapDBStore.java @@ -8,6 +8,7 @@ import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.Map.Entry; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -31,6 +32,8 @@ public class MapDBStore extends MapBasedDataStore { private Vertx vertx; private long timerId; + + private AtomicBoolean committing = new AtomicBoolean(false); protected static Logger logger = LoggerFactory.getLogger(MapDBStore.class); private static final String MAP_NAME = "BROADCAST"; private static final String VOD_MAP_NAME = "VOD"; @@ -39,6 +42,8 @@ public class MapDBStore extends MapBasedDataStore { private static final String SUBSCRIBER = "SUBSCRIBER"; private static final String CONFERENCE_ROOM_MAP_NAME = "CONFERENCE_ROOM"; private static final String WEBRTC_VIEWER = "WEBRTC_VIEWER"; + private static final String SUBSCRIBER_METADATA = "SUBSCRIBER_METADATA"; + public MapDBStore(String dbName, Vertx vertx) { @@ -75,20 +80,34 @@ public MapDBStore(String dbName, Vertx vertx) { webRTCViewerMap = db.treeMap(WEBRTC_VIEWER).keySerializer(Serializer.STRING).valueSerializer(Serializer.STRING) .counterEnable().createOrOpen(); + + subscriberMetadataMap = db.treeMap(SUBSCRIBER_METADATA).keySerializer(Serializer.STRING).valueSerializer(Serializer.STRING) + .counterEnable().createOrOpen(); - timerId = vertx.setPeriodic(5000, id -> - - vertx.executeBlocking(b -> { - - synchronized (this) - { - if (available) { - db.commit(); - } - } - - }, false, null) - ); + timerId = vertx.setPeriodic(5000, + id -> + vertx.executeBlocking(() -> { + + //if it's committing, just let the thread proceed here and become free immediately for other jobs + if (committing.compareAndSet(false, true)) + { + try { + synchronized (this) + { + if (available) { + db.commit(); + } + } + } + finally { + committing.compareAndSet(true, false); + } + } + + return null; + + }, + false)); available = true; } @@ -149,14 +168,6 @@ public void saveStreamInfo(StreamInfo streamInfo) { //no need to implement this method, it is used in cluster mode } - @Override - public SubscriberMetadata getSubscriberMetaData(String subscriberId) { - return null; - } - - @Override - public boolean save(String subscriberId, PushNotificationToken pushNotificationToken) { - return false; - } + } diff --git a/src/main/java/io/antmedia/datastore/db/MongoStore.java b/src/main/java/io/antmedia/datastore/db/MongoStore.java index 6b8c37bfe..b93f5ead9 100644 --- a/src/main/java/io/antmedia/datastore/db/MongoStore.java +++ b/src/main/java/io/antmedia/datastore/db/MongoStore.java @@ -59,7 +59,8 @@ public class MongoStore extends DataStore { public static final String VOD_ID = "vodId"; private static final String VIEWER_ID = "viewerId"; private static final String TOKEN_ID = "tokenId"; - public static final String STREAM_ID = "streamId"; + public static final String STREAM_ID = "streamId"; + public static final String SUBSCRIBER_ID = "subscriberId"; private Datastore datastore; private Datastore vodDatastore; private Datastore tokenDatastore; @@ -1534,11 +1535,33 @@ public Datastore getSubscriberDatastore() { @Override public SubscriberMetadata getSubscriberMetaData(String subscriberId) { + synchronized(this) { + try { + return datastore.find(SubscriberMetadata.class).filter(Filters.eq(SUBSCRIBER_ID, subscriberId)).first(); + } catch (Exception e) { + logger.error(ExceptionUtils.getStackTrace(e)); + } + } return null; } @Override - public boolean save(String subscriberId, PushNotificationToken pushNotificationToken) { - return false; + public void putSubscriberMetaData(String subscriberId, SubscriberMetadata metadata) { + + try { + //delete the subscriberId if exists to make it compatible with all datastores + Query query = datastore.find(SubscriberMetadata.class).filter(Filters.eq(SUBSCRIBER_ID, subscriberId)); + long deletedCount = query.delete().getDeletedCount(); + if (deletedCount > 0) { + logger.info("There is a SubsriberMetadata exists in database. It's deleted(deletedCount:{}) and it'll put to make it easy and compatible.", deletedCount); + } + + metadata.setSubscriberId(subscriberId); + synchronized(this) { + datastore.save(metadata); + } + } catch (Exception e) { + logger.error(ExceptionUtils.getStackTrace(e)); + } } } diff --git a/src/main/java/io/antmedia/datastore/db/RedisStore.java b/src/main/java/io/antmedia/datastore/db/RedisStore.java index d3047a5e2..99cfda88c 100644 --- a/src/main/java/io/antmedia/datastore/db/RedisStore.java +++ b/src/main/java/io/antmedia/datastore/db/RedisStore.java @@ -63,6 +63,7 @@ public RedisStore(String redisConnectionUrl, String dbName) { webRTCViewerMap = redisson.getMap(dbName+"WebRTCViewers"); streamInfoMap = redisson.getMap(dbName+"StreamInfo"); p2pMap = redisson.getMap(dbName+"P2P"); + subscriberMetadataMap = redisson.getMap(dbName+"SubscriberMetaData"); available = true; } @@ -183,16 +184,4 @@ public List getLocalLiveBroadcasts(String hostAddress) return getActiveBroadcastList(hostAddress); } - - @Override - public SubscriberMetadata getSubscriberMetaData(String subscriberId) { - return null; - } - - @Override - public boolean save(String subscriberId, PushNotificationToken pushNotificationToken) { - return false; - } - - } diff --git a/src/main/java/io/antmedia/datastore/db/types/PushNotificationToken.java b/src/main/java/io/antmedia/datastore/db/types/PushNotificationToken.java index 8306da501..d1d1e7f83 100644 --- a/src/main/java/io/antmedia/datastore/db/types/PushNotificationToken.java +++ b/src/main/java/io/antmedia/datastore/db/types/PushNotificationToken.java @@ -1,6 +1,7 @@ package io.antmedia.datastore.db.types; import dev.morphia.annotations.Entity; +import io.antmedia.pushnotification.IPushNotificationService.PushNotificationServiceTypes; import io.swagger.annotations.ApiModel; @ApiModel(value="PushNotificationToken", description="The endpoint class, such as Facebook, Twitter or custom RTMP endpoints") @@ -10,6 +11,7 @@ public class PushNotificationToken { private String token; /** + * {@link PushNotificationServiceTypes} * fcm or apn * fcm: Firebase Cloud Messagnig * apn: Apple Push Notification diff --git a/src/main/java/io/antmedia/datastore/db/types/SubscriberMetadata.java b/src/main/java/io/antmedia/datastore/db/types/SubscriberMetadata.java index c2657f1e1..bdc1cd7b3 100644 --- a/src/main/java/io/antmedia/datastore/db/types/SubscriberMetadata.java +++ b/src/main/java/io/antmedia/datastore/db/types/SubscriberMetadata.java @@ -7,10 +7,15 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Field; import dev.morphia.annotations.Id; +import dev.morphia.annotations.Index; +import dev.morphia.annotations.Indexes; +import dev.morphia.utils.IndexType; import io.swagger.annotations.ApiModelProperty; @Entity +@Indexes({ @Index(fields = @Field("subscriberId")) }) public class SubscriberMetadata { diff --git a/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java b/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java index a98c460cb..6e05edabb 100644 --- a/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java +++ b/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java @@ -46,8 +46,10 @@ import io.antmedia.datastore.db.types.ConnectionEvent; import io.antmedia.datastore.db.types.Endpoint; import io.antmedia.datastore.db.types.P2PConnection; +import io.antmedia.datastore.db.types.PushNotificationToken; import io.antmedia.datastore.db.types.StreamInfo; import io.antmedia.datastore.db.types.Subscriber; +import io.antmedia.datastore.db.types.SubscriberMetadata; import io.antmedia.datastore.db.types.SubscriberStats; import io.antmedia.datastore.db.types.TensorFlowObject; import io.antmedia.datastore.db.types.Token; @@ -55,6 +57,7 @@ import io.antmedia.datastore.db.types.WebRTCViewerInfo; import io.antmedia.muxer.IAntMediaStreamHandler; import io.antmedia.muxer.MuxAdaptor; +import io.antmedia.pushnotification.IPushNotificationService.PushNotificationServiceTypes; import io.antmedia.settings.ServerSettings; import io.vertx.core.Vertx; @@ -92,6 +95,7 @@ public void testMapDBStore() throws Exception { DataStore dataStore = new MapDBStore("testdb", vertx); + testSubscriberMetaData(dataStore); testGetActiveBroadcastCount(dataStore); testBlockSubscriber(dataStore); testBugFreeStreamId(dataStore); @@ -174,6 +178,8 @@ public void testMapDBPersistent() { public void testMemoryDataStore() throws Exception { DataStore dataStore = new InMemoryDataStore("testdb"); + + testSubscriberMetaData(dataStore); testBlockSubscriber(dataStore); testBugFreeStreamId(dataStore); testUnexpectedBroadcastOffset(dataStore); @@ -223,7 +229,7 @@ public void testMemoryDataStore() throws Exception { } - + @Test public void testMongoStore() throws Exception { @@ -234,6 +240,7 @@ public void testMongoStore() throws Exception { dataStore = new MongoStore("localhost", "", "", "testdb"); + testSubscriberMetaData(dataStore); testBlockSubscriber(dataStore); testTimeBasedSubscriberOperations(dataStore); testBugFreeStreamId(dataStore); @@ -291,6 +298,7 @@ public void testRedisStore() throws Exception { dataStore.close(true); dataStore = new RedisStore("redis://127.0.0.1:6379", "testdb"); + testSubscriberMetaData(dataStore); testBlockSubscriber(dataStore); testBugFreeStreamId(dataStore); testUnexpectedBroadcastOffset(dataStore); @@ -3104,4 +3112,57 @@ public void testBlockSubscriber(DataStore dataStore){ } + + private void testSubscriberMetaData(DataStore dataStore) { + //save subscriberMetadata to the data store + + String subscriberId = RandomStringUtils.randomAlphanumeric(12); + + SubscriberMetadata subscriberMetaData = dataStore.getSubscriberMetaData(subscriberId); + assertNull(subscriberMetaData); + + SubscriberMetadata metadata = new SubscriberMetadata(); + List pushNotificationTokens = new ArrayList<>(); + String tokenValue = RandomStringUtils.randomAlphabetic(65); + + PushNotificationToken token = new PushNotificationToken(tokenValue, PushNotificationServiceTypes.FIREBASE_CLOUD_MESSAGING.toString()); + pushNotificationTokens.add(token); + + metadata.setPushNotificationTokens(pushNotificationTokens); + dataStore.putSubscriberMetaData(subscriberId, metadata); + + //get the value with the id + subscriberMetaData = dataStore.getSubscriberMetaData(subscriberId); + assertNotNull(subscriberMetaData); + assertEquals(subscriberId, subscriberMetaData.getSubscriberId()); + assertEquals(1, subscriberMetaData.getPushNotificationTokens().size()); + assertEquals(tokenValue, subscriberMetaData.getPushNotificationTokens().get(0).getToken()); + assertEquals("fcm", subscriberMetaData.getPushNotificationTokens().get(0).getServiceName()); + assertNull(subscriberMetaData.getPushNotificationTokens().get(0).getExtraData()); + + + String tokenValue2 = RandomStringUtils.randomAlphabetic(65); + + PushNotificationToken token2 = new PushNotificationToken(tokenValue2, PushNotificationServiceTypes.APPLE_PUSH_NOTIFICATION.toString()); + String extraData = RandomStringUtils.randomAlphanumeric(12); + token2.setExtraData(extraData); + subscriberMetaData.getPushNotificationTokens().add(token2); + + + dataStore.putSubscriberMetaData(subscriberId, subscriberMetaData); + + subscriberMetaData = dataStore.getSubscriberMetaData(subscriberId); + + assertNotNull(subscriberMetaData); + assertEquals(subscriberId, subscriberMetaData.getSubscriberId()); + assertEquals(2, subscriberMetaData.getPushNotificationTokens().size()); + assertEquals(tokenValue, subscriberMetaData.getPushNotificationTokens().get(0).getToken()); + assertEquals("fcm", subscriberMetaData.getPushNotificationTokens().get(0).getServiceName()); + assertNull(subscriberMetaData.getPushNotificationTokens().get(0).getExtraData()); + + assertEquals(tokenValue2, subscriberMetaData.getPushNotificationTokens().get(1).getToken()); + assertEquals("apn", subscriberMetaData.getPushNotificationTokens().get(1).getServiceName()); + assertEquals(extraData, subscriberMetaData.getPushNotificationTokens().get(1).getExtraData()); + + } } From 8ae337c7e890c6bcabf6375223a3a74f997b20f8 Mon Sep 17 00:00:00 2001 From: mekya Date: Sun, 4 Feb 2024 13:45:01 +0300 Subject: [PATCH 3/9] Implement Push Notification REST API --- .../java/io/antmedia/filter/JWTFilter.java | 21 ++++ .../IPushNotificationService.java | 30 +++--- .../PushNotificationServiceCommunity.java | 13 +-- .../rest/PushNotificationService.java | 101 +++++++++++++++--- 4 files changed, 129 insertions(+), 36 deletions(-) diff --git a/src/main/java/io/antmedia/filter/JWTFilter.java b/src/main/java/io/antmedia/filter/JWTFilter.java index 96597099d..0462927a6 100644 --- a/src/main/java/io/antmedia/filter/JWTFilter.java +++ b/src/main/java/io/antmedia/filter/JWTFilter.java @@ -105,6 +105,27 @@ public static boolean isJWTTokenValid(String jwtSecretKey, String jwtToken) { return result; } + + + public static boolean isJWTTokenValid(String jwtSecretKey, String jwtToken, String issuer) { + boolean result = false; + + try { + Algorithm algorithm = Algorithm.HMAC256(jwtSecretKey); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer(issuer) + .build(); + verifier.verify(jwtToken); + result = true; + } + catch (JWTVerificationException ex) { + logger.error(ExceptionUtils.getStackTrace(ex)); + } + + return result; + } + + public static String generateJwtToken(String jwtSecretKey, long expireDateUnixTimeStampMs) { return generateJwtToken(jwtSecretKey, expireDateUnixTimeStampMs, ""); } diff --git a/src/main/java/io/antmedia/pushnotification/IPushNotificationService.java b/src/main/java/io/antmedia/pushnotification/IPushNotificationService.java index 2fbf86fc5..b951fc222 100644 --- a/src/main/java/io/antmedia/pushnotification/IPushNotificationService.java +++ b/src/main/java/io/antmedia/pushnotification/IPushNotificationService.java @@ -2,7 +2,6 @@ import java.util.List; -import io.antmedia.datastore.db.types.PushNotificationToken; import io.antmedia.rest.model.Result; public interface IPushNotificationService { @@ -25,18 +24,7 @@ public String toString() { return this.name; } } - - - /** - * Send push notification according to service name - * - * @param registerationTokens - * @param jsonMessage - * @param serviceName: fcm or apn - * @return - */ - Result sendNotification(List registerationTokens, String jsonMessage, String serviceName); - + /** * Send push notifcaiton according to service name * @@ -56,11 +44,21 @@ public String toString() { */ Result sendNotification(String topic, String jsonMessage) ; + + /** + * Send notification according to the subscriberIds + * @param subscriberIds + * @param jsonMessage + * @return + */ + Result sendNotification(List subscriberIds, String jsonMessage); + + /** - * Send notification to the registrationTokens - * @param registerationTokens + * Send notification according to the subscriberIds and service + * @param subscriberIds * @param jsonMessage * @return */ - Result sendNotification(List registerationTokens, String jsonMessage); + Result sendNotification(List subscriberIds, String jsonMessage, String serviceName); } diff --git a/src/main/java/io/antmedia/pushnotification/PushNotificationServiceCommunity.java b/src/main/java/io/antmedia/pushnotification/PushNotificationServiceCommunity.java index 6a14cc1c1..39bd279ab 100644 --- a/src/main/java/io/antmedia/pushnotification/PushNotificationServiceCommunity.java +++ b/src/main/java/io/antmedia/pushnotification/PushNotificationServiceCommunity.java @@ -9,23 +9,24 @@ public class PushNotificationServiceCommunity implements IPushNotificationServic private static final String MESSAGE_TO_USE_ENTERPRISE_EDITION = "Push Notification Service is not available community edition. Please use enterprise edition"; + @Override - public Result sendNotification(List registerationTokens, String jsonMessage, String serviceName) { + public Result sendNotification(String topic, String jsonMessage, String serviceName) { return new Result(false, MESSAGE_TO_USE_ENTERPRISE_EDITION); } @Override - public Result sendNotification(String topic, String jsonMessage, String serviceName) { + public Result sendNotification(String topic, String jsonMessage) { return new Result(false, MESSAGE_TO_USE_ENTERPRISE_EDITION); } - + @Override - public Result sendNotification(List registerationTokens, String jsonMessage) { + public Result sendNotification(List subscriberIds, String jsonMessage) { return new Result(false, MESSAGE_TO_USE_ENTERPRISE_EDITION); } - + @Override - public Result sendNotification(String topic, String jsonMessage) { + public Result sendNotification(List subscriberIds, String jsonMessage, String serviceName) { return new Result(false, MESSAGE_TO_USE_ENTERPRISE_EDITION); } diff --git a/src/main/java/io/antmedia/rest/PushNotificationService.java b/src/main/java/io/antmedia/rest/PushNotificationService.java index 04e6b0ce6..cb0109f64 100644 --- a/src/main/java/io/antmedia/rest/PushNotificationService.java +++ b/src/main/java/io/antmedia/rest/PushNotificationService.java @@ -2,43 +2,116 @@ import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; +import org.springframework.web.context.WebApplicationContext; +import io.antmedia.AntMediaApplicationAdapter; +import io.antmedia.AppSettings; +import io.antmedia.datastore.db.types.PushNotificationToken; +import io.antmedia.filter.JWTFilter; +import io.antmedia.pushnotification.IPushNotificationService; import io.antmedia.rest.model.Result; import io.swagger.annotations.Api; +import jakarta.servlet.ServletContext; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; @Api(value = "") @Component @Path("/v2/push-notification") public class PushNotificationService { + @Context + protected ServletContext servletContext; + protected ApplicationContext appCtx; + + private IPushNotificationService pushNotificationService; - public Result getSubscriberAuthenticationToken() { - return null; - } - - public Result sendPushNotification(List subscriberIdList, String jsonMessage, String serviceName) { - return null; - } + private AppSettings appSettings; + - public Result sendPushNotification(String topic, String jsonMessage, String serviceName) { - return null; + public ApplicationContext getAppContext() { + if (servletContext != null) { + appCtx = (ApplicationContext) servletContext + .getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); + } + return appCtx; } - public Result sendDataChannelMessage(List subscriberIdList, String jsonMessage, String serviceName) { - return null; + + public IPushNotificationService getPushNotificationService() { + if (pushNotificationService == null) { + ApplicationContext appContext = getAppContext(); + if (appContext != null) { + pushNotificationService = (IPushNotificationService) appContext.getBean(IPushNotificationService.BEAN_NAME); + } + } + return pushNotificationService; } - public Result sendDataChannelMessage(String subscriberId, String jsonMessage, String serviceName) { - return null; + public AppSettings getAppSettings() { + if (appSettings == null) { + ApplicationContext appContext = getAppContext(); + if (appContext != null) { + appSettings = (AppSettings) appContext.getBean(AppSettings.BEAN_NAME); + } + } + return appSettings; } - + @GET + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("/subscriber-auth-token") + public Result getSubscriberAuthenticationToken(@QueryParam("subscriberId") String subscriberId, @QueryParam("timeoutSeconds") int timeoutDurationInSeconds) { + if (timeoutDurationInSeconds <= 0) { + //one hour default - 3600 seconds + timeoutDurationInSeconds = 3600; + } + long expireTimeMs = System.currentTimeMillis() + (timeoutDurationInSeconds * 1000); + String jwtToken = JWTFilter.generateJwtToken(getAppSettings().getSubscriberAuthenticationKey(), expireTimeMs, subscriberId); + + return new Result(true, jwtToken, "Token is available in dataId field"); + } + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("/subscribers") + public Result sendPushNotification(List subscriberIdList, String jsonMessage, @QueryParam("serviceName") String serviceName) { + if (StringUtils.isBlank(serviceName)) { + return getPushNotificationService().sendNotification(subscriberIdList, jsonMessage); + } + else { + return getPushNotificationService().sendNotification(subscriberIdList, jsonMessage, serviceName); + } + } + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("/topics/{topic}") + public Result sendPushNotification(@PathParam("topic") String topic, String jsonMessage, String serviceName) { + if (StringUtils.isBlank(serviceName)) { + return getPushNotificationService().sendNotification(topic, jsonMessage); + } + else { + return getPushNotificationService().sendNotification(topic, jsonMessage, serviceName); + } + } + + } From e22189d1093aa53e70c7da36b2805ee635791f06 Mon Sep 17 00:00:00 2001 From: mekya Date: Sun, 4 Feb 2024 14:30:02 +0300 Subject: [PATCH 4/9] Fix the jersey issue --- .../java/io/antmedia/rest/PushNotificationService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/antmedia/rest/PushNotificationService.java b/src/main/java/io/antmedia/rest/PushNotificationService.java index cb0109f64..41a12d98d 100644 --- a/src/main/java/io/antmedia/rest/PushNotificationService.java +++ b/src/main/java/io/antmedia/rest/PushNotificationService.java @@ -16,6 +16,7 @@ import io.swagger.annotations.Api; import jakarta.servlet.ServletContext; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -72,7 +73,6 @@ public AppSettings getAppSettings() { @GET - @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Path("/subscriber-auth-token") public Result getSubscriberAuthenticationToken(@QueryParam("subscriberId") String subscriberId, @QueryParam("timeoutSeconds") int timeoutDurationInSeconds) { @@ -90,7 +90,7 @@ public Result getSubscriberAuthenticationToken(@QueryParam("subscriberId") Strin @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Path("/subscribers") - public Result sendPushNotification(List subscriberIdList, String jsonMessage, @QueryParam("serviceName") String serviceName) { + public Result sendPushNotification(@FormParam("subscribers") List subscriberIdList, String jsonMessage, @QueryParam("serviceName") String serviceName) { if (StringUtils.isBlank(serviceName)) { return getPushNotificationService().sendNotification(subscriberIdList, jsonMessage); } @@ -102,8 +102,8 @@ public Result sendPushNotification(List subscriberIdList, String jsonMes @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Path("/topics/{topic}") - public Result sendPushNotification(@PathParam("topic") String topic, String jsonMessage, String serviceName) { + @Path("/topics/{topic}/{serviceName}") + public Result sendPushNotification(@PathParam("topic") String topic, String jsonMessage, @PathParam("serviceName") String serviceName) { if (StringUtils.isBlank(serviceName)) { return getPushNotificationService().sendNotification(topic, jsonMessage); } From f4b2b7b7cefff35a49e2da82f5ea4811908c7814 Mon Sep 17 00:00:00 2001 From: mekya Date: Sun, 4 Feb 2024 15:19:05 +0300 Subject: [PATCH 5/9] Fix test case --- src/test/java/io/antmedia/test/AppSettingsUnitTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/io/antmedia/test/AppSettingsUnitTest.java b/src/test/java/io/antmedia/test/AppSettingsUnitTest.java index 08ef83bd9..65d2492cb 100644 --- a/src/test/java/io/antmedia/test/AppSettingsUnitTest.java +++ b/src/test/java/io/antmedia/test/AppSettingsUnitTest.java @@ -495,12 +495,14 @@ public void testUnsetAppSettings(AppSettings appSettings) { assertEquals(true, appSettings.isSendAudioLevelToViewers()); assertNull(appSettings.getTimeTokenSecretForPublish()); assertNull(appSettings.getTimeTokenSecretForPlay()); + assertNotNull(appSettings.getSubscriberAuthenticationKey()); + assertNull(appSettings.getFirebaseAccountKeyJSON()); //if we add a new field, we just need to check its default value in this test //When a new field is added or removed please update the number of fields and make this test pass //by also checking its default value. assertEquals("New field is added to settings. PAY ATTENTION: Please CHECK ITS DEFAULT VALUE and fix the number of fields.", - 168, numberOfFields); + 170, numberOfFields); } From 36db7c8b79aac8d2d71bcef750a25672009e7579 Mon Sep 17 00:00:00 2001 From: mekya Date: Sun, 4 Feb 2024 17:56:24 +0300 Subject: [PATCH 6/9] Fix test case --- .../java/io/antmedia/datastore/db/InMemoryDataStore.java | 3 ++- ...icationService.java => PushNotificationRestService.java} | 6 +----- src/test/java/io/antmedia/test/StreamFetcherUnitTest.java | 4 +++- 3 files changed, 6 insertions(+), 7 deletions(-) rename src/main/java/io/antmedia/rest/{PushNotificationService.java => PushNotificationRestService.java} (94%) diff --git a/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java b/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java index 7b68e7a1e..766685868 100644 --- a/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java +++ b/src/main/java/io/antmedia/datastore/db/InMemoryDataStore.java @@ -11,17 +11,18 @@ import java.util.Map.Entry; import java.util.Set; import java.util.regex.Pattern; + import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import io.antmedia.AntMediaApplicationAdapter; import io.antmedia.datastore.db.types.Broadcast; import io.antmedia.datastore.db.types.ConferenceRoom; import io.antmedia.datastore.db.types.Endpoint; import io.antmedia.datastore.db.types.P2PConnection; -import io.antmedia.datastore.db.types.PushNotificationToken; import io.antmedia.datastore.db.types.StreamInfo; import io.antmedia.datastore.db.types.Subscriber; import io.antmedia.datastore.db.types.SubscriberMetadata; diff --git a/src/main/java/io/antmedia/rest/PushNotificationService.java b/src/main/java/io/antmedia/rest/PushNotificationRestService.java similarity index 94% rename from src/main/java/io/antmedia/rest/PushNotificationService.java rename to src/main/java/io/antmedia/rest/PushNotificationRestService.java index 41a12d98d..4ccdd5955 100644 --- a/src/main/java/io/antmedia/rest/PushNotificationService.java +++ b/src/main/java/io/antmedia/rest/PushNotificationRestService.java @@ -7,13 +7,10 @@ import org.springframework.stereotype.Component; import org.springframework.web.context.WebApplicationContext; -import io.antmedia.AntMediaApplicationAdapter; import io.antmedia.AppSettings; -import io.antmedia.datastore.db.types.PushNotificationToken; import io.antmedia.filter.JWTFilter; import io.antmedia.pushnotification.IPushNotificationService; import io.antmedia.rest.model.Result; -import io.swagger.annotations.Api; import jakarta.servlet.ServletContext; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.FormParam; @@ -26,10 +23,9 @@ import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; -@Api(value = "") @Component @Path("/v2/push-notification") -public class PushNotificationService { +public class PushNotificationRestService { @Context protected ServletContext servletContext; diff --git a/src/test/java/io/antmedia/test/StreamFetcherUnitTest.java b/src/test/java/io/antmedia/test/StreamFetcherUnitTest.java index c5186b93e..1f1705741 100644 --- a/src/test/java/io/antmedia/test/StreamFetcherUnitTest.java +++ b/src/test/java/io/antmedia/test/StreamFetcherUnitTest.java @@ -34,6 +34,7 @@ import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; import org.awaitility.Awaitility; import org.bytedeco.ffmpeg.avcodec.AVCodecParameters; import org.bytedeco.ffmpeg.avcodec.AVPacket; @@ -764,9 +765,10 @@ public void testCameraStartedProperly() { assertEquals(1, getInstance().getMuxAdaptors().size()); String str3=fetcher3.getCameraError().getMessage(); + assertTrue(fetcher3.getCameraError().isSuccess()); logger.info("error: "+str3); - assertNull(fetcher3.getCameraError().getMessage()); + assertTrue(StringUtils.isBlank(fetcher3.getCameraError().getMessage())); Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> { return fetcher3.isStreamAlive(); From de126dc85a087715c3d270af72b8c7068e404333 Mon Sep 17 00:00:00 2001 From: mekya Date: Sun, 4 Feb 2024 20:52:32 +0300 Subject: [PATCH 7/9] Add test codes --- .../db/types/SubscriberMetadata.java | 7 +- .../rest/PushNotificationRestService.java | 5 + .../websocket/WebSocketConstants.java | 2 +- .../io/antmedia/test/db/DBStoresUnitTest.java | 6 +- .../antmedia/test/filter/JWTFilterTest.java | 13 +++ .../PushNotificationCommunityTest.java | 38 +++++++ .../rest/PushNotificationRestServiceTest.java | 106 ++++++++++++++++++ 7 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 src/test/java/io/antmedia/test/pushnotification/PushNotificationCommunityTest.java create mode 100644 src/test/java/io/antmedia/test/rest/PushNotificationRestServiceTest.java diff --git a/src/main/java/io/antmedia/datastore/db/types/SubscriberMetadata.java b/src/main/java/io/antmedia/datastore/db/types/SubscriberMetadata.java index bdc1cd7b3..7101dd62f 100644 --- a/src/main/java/io/antmedia/datastore/db/types/SubscriberMetadata.java +++ b/src/main/java/io/antmedia/datastore/db/types/SubscriberMetadata.java @@ -1,6 +1,7 @@ package io.antmedia.datastore.db.types; import java.util.List; +import java.util.Map; import org.bson.types.ObjectId; @@ -31,7 +32,7 @@ public class SubscriberMetadata { private String subscriberId; @ApiModelProperty(value = "Push notification tokens provided by FCM and APN") - private List pushNotificationTokens; + private Map pushNotificationTokens; public ObjectId getDbId() { return dbId; @@ -49,11 +50,11 @@ public void setSubscriberId(String subscriberId) { this.subscriberId = subscriberId; } - public List getPushNotificationTokens() { + public Map getPushNotificationTokens() { return pushNotificationTokens; } - public void setPushNotificationTokens(List pushNotificationTokens) { + public void setPushNotificationTokens(Map pushNotificationTokens) { this.pushNotificationTokens = pushNotificationTokens; } diff --git a/src/main/java/io/antmedia/rest/PushNotificationRestService.java b/src/main/java/io/antmedia/rest/PushNotificationRestService.java index 4ccdd5955..5fb988709 100644 --- a/src/main/java/io/antmedia/rest/PushNotificationRestService.java +++ b/src/main/java/io/antmedia/rest/PushNotificationRestService.java @@ -107,6 +107,11 @@ public Result sendPushNotification(@PathParam("topic") String topic, String json return getPushNotificationService().sendNotification(topic, jsonMessage, serviceName); } } + + + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; + } diff --git a/src/main/java/io/antmedia/websocket/WebSocketConstants.java b/src/main/java/io/antmedia/websocket/WebSocketConstants.java index aa57d100e..223fd2463 100644 --- a/src/main/java/io/antmedia/websocket/WebSocketConstants.java +++ b/src/main/java/io/antmedia/websocket/WebSocketConstants.java @@ -537,7 +537,7 @@ private WebSocketConstants() { public static final String SEND_PUSH_NOTIFICATION_COMMAND = "sendPushNotification"; - public static final String REGISTER_PUSH_NOTIFICATION_TOKEN = "reigsterPushNotificationToken"; + public static final String REGISTER_PUSH_NOTIFICATION_TOKEN_COMMAND = "registerPushNotificationToken"; public static final String AUTH_TOKEN_NOT_VALID_ERROR_DEFINITION = "authenticationTokenNotValid"; diff --git a/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java b/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java index 6e05edabb..da396a846 100644 --- a/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java +++ b/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java @@ -3122,11 +3122,11 @@ private void testSubscriberMetaData(DataStore dataStore) { assertNull(subscriberMetaData); SubscriberMetadata metadata = new SubscriberMetadata(); - List pushNotificationTokens = new ArrayList<>(); + Map pushNotificationTokens = new HashMap<>(); String tokenValue = RandomStringUtils.randomAlphabetic(65); PushNotificationToken token = new PushNotificationToken(tokenValue, PushNotificationServiceTypes.FIREBASE_CLOUD_MESSAGING.toString()); - pushNotificationTokens.add(token); + pushNotificationTokens.put(tokenValue, token); metadata.setPushNotificationTokens(pushNotificationTokens); dataStore.putSubscriberMetaData(subscriberId, metadata); @@ -3146,7 +3146,7 @@ private void testSubscriberMetaData(DataStore dataStore) { PushNotificationToken token2 = new PushNotificationToken(tokenValue2, PushNotificationServiceTypes.APPLE_PUSH_NOTIFICATION.toString()); String extraData = RandomStringUtils.randomAlphanumeric(12); token2.setExtraData(extraData); - subscriberMetaData.getPushNotificationTokens().add(token2); + subscriberMetaData.getPushNotificationTokens().put(tokenValue2, token2); dataStore.putSubscriberMetaData(subscriberId, subscriberMetaData); diff --git a/src/test/java/io/antmedia/test/filter/JWTFilterTest.java b/src/test/java/io/antmedia/test/filter/JWTFilterTest.java index 1e6741b7c..e10442f68 100644 --- a/src/test/java/io/antmedia/test/filter/JWTFilterTest.java +++ b/src/test/java/io/antmedia/test/filter/JWTFilterTest.java @@ -1,6 +1,8 @@ package io.antmedia.test.filter; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import java.io.IOException; @@ -150,5 +152,16 @@ public void testDoFilterPass() throws IOException, ServletException { assertEquals(HttpStatus.FORBIDDEN.value(),httpServletResponse.getStatus()); } } + + + @Test + public void testGenerateAndVerifyTokenWithIssuer() { + + String token = JWTFilter.generateJwtToken("testtesttesttesttesttesttesttest", System.currentTimeMillis() + 10000, "test"); + assertTrue(JWTFilter.isJWTTokenValid("testtesttesttesttesttesttesttest", token, "test")); + + assertFalse(JWTFilter.isJWTTokenValid("testtesttesttesttesttesttesttest", token, "test2")); + + } } diff --git a/src/test/java/io/antmedia/test/pushnotification/PushNotificationCommunityTest.java b/src/test/java/io/antmedia/test/pushnotification/PushNotificationCommunityTest.java new file mode 100644 index 000000000..70a3a33f0 --- /dev/null +++ b/src/test/java/io/antmedia/test/pushnotification/PushNotificationCommunityTest.java @@ -0,0 +1,38 @@ +package io.antmedia.test.pushnotification; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.util.Arrays; + +import org.junit.Test; + +import io.antmedia.pushnotification.PushNotificationServiceCommunity; +import io.antmedia.rest.model.Result; + +public class PushNotificationCommunityTest { + + + @Test + public void testPushNotificaitonServiceCommunity() { + PushNotificationServiceCommunity pushNotificationServiceCommunity = new PushNotificationServiceCommunity(); + + Result sendNotification = pushNotificationServiceCommunity.sendNotification("title", "message", "token"); + assertFalse(sendNotification.isSuccess()); + assertEquals("Push Notification Service is not available community edition. Please use enterprise edition", sendNotification.getMessage()); + + sendNotification = pushNotificationServiceCommunity.sendNotification("topic", "message"); + assertFalse(sendNotification.isSuccess()); + assertEquals("Push Notification Service is not available community edition. Please use enterprise edition", sendNotification.getMessage()); + + sendNotification = pushNotificationServiceCommunity.sendNotification(Arrays.asList(""), "message"); + assertFalse(sendNotification.isSuccess()); + assertEquals("Push Notification Service is not available community edition. Please use enterprise edition", sendNotification.getMessage()); + + sendNotification = pushNotificationServiceCommunity.sendNotification(Arrays.asList(""), "message", "serviceName"); + assertFalse(sendNotification.isSuccess()); + assertEquals("Push Notification Service is not available community edition. Please use enterprise edition", sendNotification.getMessage()); + + } + +} diff --git a/src/test/java/io/antmedia/test/rest/PushNotificationRestServiceTest.java b/src/test/java/io/antmedia/test/rest/PushNotificationRestServiceTest.java new file mode 100644 index 000000000..127751a0c --- /dev/null +++ b/src/test/java/io/antmedia/test/rest/PushNotificationRestServiceTest.java @@ -0,0 +1,106 @@ +package io.antmedia.test.rest; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.awaitility.Awaitility; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.context.ApplicationContext; + +import io.antmedia.AppSettings; +import io.antmedia.filter.JWTFilter; +import io.antmedia.pushnotification.IPushNotificationService; +import io.antmedia.rest.PushNotificationRestService; +import io.antmedia.rest.model.Result; +import jakarta.servlet.ServletContext; + +public class PushNotificationRestServiceTest { + + + @Test + public void testSendNotification() { + PushNotificationRestService pushNotificationRestService = new PushNotificationRestService(); + IPushNotificationService pushNotificationService = Mockito.mock(IPushNotificationService.class); + ServletContext servletContext = Mockito.mock(ServletContext.class); + + pushNotificationRestService.setServletContext(servletContext); + + ApplicationContext appContext = Mockito.mock(ApplicationContext.class); + Mockito.when(servletContext.getAttribute(Mockito.anyString())).thenReturn(appContext); + + Mockito.when(appContext.getBean(IPushNotificationService.BEAN_NAME)).thenReturn(pushNotificationService); + + + pushNotificationRestService.sendPushNotification("topic", "jsonMessage","fcm"); + verify(pushNotificationService).sendNotification("topic", "jsonMessage","fcm"); + + pushNotificationRestService.sendPushNotification("topic", "jsonMessage","apns"); + verify(pushNotificationService).sendNotification("topic", "jsonMessage","apns"); + + pushNotificationRestService.sendPushNotification("topic", "jsonMessage",""); + verify(pushNotificationService).sendNotification("topic", "jsonMessage"); + + pushNotificationRestService.sendPushNotification("topic", "jsonMessage", null); + verify(pushNotificationService, times(2)).sendNotification("topic", "jsonMessage"); + + + + pushNotificationRestService.sendPushNotification(Arrays.asList("subscriber1", "subscriber2"), "jsonMessage", "fcm"); + verify(pushNotificationService).sendNotification(Arrays.asList("subscriber1", "subscriber2"), "jsonMessage","fcm"); + + + pushNotificationRestService.sendPushNotification(Arrays.asList("subscriber1", "subscriber2"), "jsonMessage", "apns"); + verify(pushNotificationService).sendNotification(Arrays.asList("subscriber1", "subscriber2"), "jsonMessage","apns"); + + pushNotificationRestService.sendPushNotification(Arrays.asList("subscriber1", "subscriber2"), "jsonMessage", ""); + verify(pushNotificationService).sendNotification(Arrays.asList("subscriber1", "subscriber2"), "jsonMessage"); + + pushNotificationRestService.sendPushNotification(Arrays.asList("subscriber1", "subscriber2"), "jsonMessage", null); + verify(pushNotificationService, times(2)).sendNotification(Arrays.asList("subscriber1", "subscriber2"), "jsonMessage"); + + + } + + + + + @Test + public void testGetToken() { + PushNotificationRestService pushNotificationRestService = new PushNotificationRestService(); + IPushNotificationService pushNotificationService = Mockito.mock(IPushNotificationService.class); + ServletContext servletContext = Mockito.mock(ServletContext.class); + + pushNotificationRestService.setServletContext(servletContext); + + ApplicationContext appContext = Mockito.mock(ApplicationContext.class); + Mockito.when(servletContext.getAttribute(Mockito.anyString())).thenReturn(appContext); + + Mockito.when(appContext.getBean(IPushNotificationService.BEAN_NAME)).thenReturn(pushNotificationService); + AppSettings appSettings = new AppSettings(); + when(appContext.getBean(AppSettings.BEAN_NAME)).thenReturn(appSettings); + + Result result = pushNotificationRestService.getSubscriberAuthenticationToken("subscriber1", 0); + assertTrue(JWTFilter.isJWTTokenValid(appSettings.getSubscriberAuthenticationKey(), result.getDataId(), "subscriber1")); + + assertFalse(JWTFilter.isJWTTokenValid(appSettings.getSubscriberAuthenticationKey(), result.getDataId(), "subscriber12")); + + result = pushNotificationRestService.getSubscriberAuthenticationToken("subscriber1", 3); + + assertTrue(JWTFilter.isJWTTokenValid(appSettings.getSubscriberAuthenticationKey(), result.getDataId(), "subscriber1")); + String token = result.getDataId(); + Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() -> { + return !JWTFilter.isJWTTokenValid(appSettings.getSubscriberAuthenticationKey(), token, + "subscriber1"); + }); + + + } + +} From 7c9e0407b369db42e9fec465a869f3c4e9a5e94c Mon Sep 17 00:00:00 2001 From: mekya Date: Sun, 4 Feb 2024 21:34:18 +0300 Subject: [PATCH 8/9] Fix failing test case --- .../io/antmedia/test/db/DBStoresUnitTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java b/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java index da396a846..db37eb4c3 100644 --- a/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java +++ b/src/test/java/io/antmedia/test/db/DBStoresUnitTest.java @@ -3136,9 +3136,9 @@ private void testSubscriberMetaData(DataStore dataStore) { assertNotNull(subscriberMetaData); assertEquals(subscriberId, subscriberMetaData.getSubscriberId()); assertEquals(1, subscriberMetaData.getPushNotificationTokens().size()); - assertEquals(tokenValue, subscriberMetaData.getPushNotificationTokens().get(0).getToken()); - assertEquals("fcm", subscriberMetaData.getPushNotificationTokens().get(0).getServiceName()); - assertNull(subscriberMetaData.getPushNotificationTokens().get(0).getExtraData()); + assertEquals(tokenValue, subscriberMetaData.getPushNotificationTokens().get(tokenValue).getToken()); + assertEquals("fcm", subscriberMetaData.getPushNotificationTokens().get(tokenValue).getServiceName()); + assertNull(subscriberMetaData.getPushNotificationTokens().get(tokenValue).getExtraData()); String tokenValue2 = RandomStringUtils.randomAlphabetic(65); @@ -3156,13 +3156,13 @@ private void testSubscriberMetaData(DataStore dataStore) { assertNotNull(subscriberMetaData); assertEquals(subscriberId, subscriberMetaData.getSubscriberId()); assertEquals(2, subscriberMetaData.getPushNotificationTokens().size()); - assertEquals(tokenValue, subscriberMetaData.getPushNotificationTokens().get(0).getToken()); - assertEquals("fcm", subscriberMetaData.getPushNotificationTokens().get(0).getServiceName()); - assertNull(subscriberMetaData.getPushNotificationTokens().get(0).getExtraData()); + assertEquals(tokenValue, subscriberMetaData.getPushNotificationTokens().get(tokenValue).getToken()); + assertEquals("fcm", subscriberMetaData.getPushNotificationTokens().get(tokenValue).getServiceName()); + assertNull(subscriberMetaData.getPushNotificationTokens().get(tokenValue).getExtraData()); - assertEquals(tokenValue2, subscriberMetaData.getPushNotificationTokens().get(1).getToken()); - assertEquals("apn", subscriberMetaData.getPushNotificationTokens().get(1).getServiceName()); - assertEquals(extraData, subscriberMetaData.getPushNotificationTokens().get(1).getExtraData()); + assertEquals(tokenValue2, subscriberMetaData.getPushNotificationTokens().get(tokenValue2).getToken()); + assertEquals("apn", subscriberMetaData.getPushNotificationTokens().get(tokenValue2).getServiceName()); + assertEquals(extraData, subscriberMetaData.getPushNotificationTokens().get(tokenValue2).getExtraData()); } } From 13311613edc3d5b90a64e548c66ac37dd039ca87 Mon Sep 17 00:00:00 2001 From: mekya Date: Thu, 8 Feb 2024 13:15:47 +0300 Subject: [PATCH 9/9] Use subscriberId field in generating subscriber auth token --- .../java/io/antmedia/filter/JWTFilter.java | 54 ++++++++++++++++++- .../rest/PushNotificationRestService.java | 3 +- .../antmedia/test/filter/JWTFilterTest.java | 17 ++++++ .../rest/PushNotificationRestServiceTest.java | 7 +-- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/antmedia/filter/JWTFilter.java b/src/main/java/io/antmedia/filter/JWTFilter.java index 0462927a6..fce857019 100644 --- a/src/main/java/io/antmedia/filter/JWTFilter.java +++ b/src/main/java/io/antmedia/filter/JWTFilter.java @@ -99,7 +99,8 @@ public static boolean isJWTTokenValid(String jwtSecretKey, String jwtToken) { result = true; } catch (JWTVerificationException ex) { - logger.error(ExceptionUtils.getStackTrace(ex)); + logger.error("JWT token is not valid for a jwtToken"); + } return result; @@ -119,12 +120,61 @@ public static boolean isJWTTokenValid(String jwtSecretKey, String jwtToken, Stri result = true; } catch (JWTVerificationException ex) { - logger.error(ExceptionUtils.getStackTrace(ex)); + logger.error("JWT token is not valid for issuer name: {}", issuer); + + } + + return result; + } + + /** + * This method checks the claim value in the JWT token. For instance, we just need to give claimName as `subscriberId` and claimValue as the subscribers' id such as email + * + *Typical usage is like that + *isJWTTokenValid(jwtSecretKey, jwtToken, "subscriberId", "myemail@example.com"); + * + * @param jwtSecretKey + * @param jwtToken + * @param claimName + * @param claimValue + * @return + */ + public static boolean isJWTTokenValid(String jwtSecretKey, String jwtToken, String claimName, String claimValue) { + boolean result = false; + + try { + Algorithm algorithm = Algorithm.HMAC256(jwtSecretKey); + JWTVerifier verifier = JWT.require(algorithm) + .withClaim(claimName, claimValue) + .build(); + verifier.verify(jwtToken); + result = true; + } + catch (JWTVerificationException ex) { + logger.error("JWT token is not valid for claim name: {}", claimName); } return result; } + public static String generateJwtToken(String jwtSecretKey, long expireDateUnixTimeStampMs, String claimName, String claimValue) { + Date expireDateType = new Date(expireDateUnixTimeStampMs); + String jwtTokenId = null; + try { + Algorithm algorithm = Algorithm.HMAC256(jwtSecretKey); + + jwtTokenId = JWT.create(). + withExpiresAt(expireDateType). + withClaim(claimName, claimValue). + sign(algorithm); + + } catch (Exception e) { + logger.error(ExceptionUtils.getStackTrace(e)); + } + + return jwtTokenId; + } + public static String generateJwtToken(String jwtSecretKey, long expireDateUnixTimeStampMs) { return generateJwtToken(jwtSecretKey, expireDateUnixTimeStampMs, ""); diff --git a/src/main/java/io/antmedia/rest/PushNotificationRestService.java b/src/main/java/io/antmedia/rest/PushNotificationRestService.java index 5fb988709..a70be7afe 100644 --- a/src/main/java/io/antmedia/rest/PushNotificationRestService.java +++ b/src/main/java/io/antmedia/rest/PushNotificationRestService.java @@ -11,6 +11,7 @@ import io.antmedia.filter.JWTFilter; import io.antmedia.pushnotification.IPushNotificationService; import io.antmedia.rest.model.Result; +import io.antmedia.websocket.WebSocketConstants; import jakarta.servlet.ServletContext; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.FormParam; @@ -77,7 +78,7 @@ public Result getSubscriberAuthenticationToken(@QueryParam("subscriberId") Strin timeoutDurationInSeconds = 3600; } long expireTimeMs = System.currentTimeMillis() + (timeoutDurationInSeconds * 1000); - String jwtToken = JWTFilter.generateJwtToken(getAppSettings().getSubscriberAuthenticationKey(), expireTimeMs, subscriberId); + String jwtToken = JWTFilter.generateJwtToken(getAppSettings().getSubscriberAuthenticationKey(), expireTimeMs, WebSocketConstants.SUBSCRIBER_ID, subscriberId); return new Result(true, jwtToken, "Token is available in dataId field"); } diff --git a/src/test/java/io/antmedia/test/filter/JWTFilterTest.java b/src/test/java/io/antmedia/test/filter/JWTFilterTest.java index e10442f68..dd45edfb1 100644 --- a/src/test/java/io/antmedia/test/filter/JWTFilterTest.java +++ b/src/test/java/io/antmedia/test/filter/JWTFilterTest.java @@ -163,5 +163,22 @@ public void testGenerateAndVerifyTokenWithIssuer() { assertFalse(JWTFilter.isJWTTokenValid("testtesttesttesttesttesttesttest", token, "test2")); } + + @Test + public void testJWTTokenValidWithSubscribers() { + + + String token = JWTFilter.generateJwtToken("testtesttesttesttesttesttesttest", System.currentTimeMillis() + 10000, "subscriberId", "test"); + + + assertFalse(JWTFilter.isJWTTokenValid("testtesttesttesttesttesttesttest", token, "subscriberId", "test2")); + + assertFalse(JWTFilter.isJWTTokenValid("testtesttesttesttesttesttesttest", token, "subscriberId_df", "test2")); + + + assertTrue(JWTFilter.isJWTTokenValid("testtesttesttesttesttesttesttest", token, "subscriberId", "test")); + + + } } diff --git a/src/test/java/io/antmedia/test/rest/PushNotificationRestServiceTest.java b/src/test/java/io/antmedia/test/rest/PushNotificationRestServiceTest.java index 127751a0c..77fa66fe9 100644 --- a/src/test/java/io/antmedia/test/rest/PushNotificationRestServiceTest.java +++ b/src/test/java/io/antmedia/test/rest/PushNotificationRestServiceTest.java @@ -19,6 +19,7 @@ import io.antmedia.pushnotification.IPushNotificationService; import io.antmedia.rest.PushNotificationRestService; import io.antmedia.rest.model.Result; +import io.antmedia.websocket.WebSocketConstants; import jakarta.servlet.ServletContext; public class PushNotificationRestServiceTest { @@ -87,13 +88,13 @@ public void testGetToken() { when(appContext.getBean(AppSettings.BEAN_NAME)).thenReturn(appSettings); Result result = pushNotificationRestService.getSubscriberAuthenticationToken("subscriber1", 0); - assertTrue(JWTFilter.isJWTTokenValid(appSettings.getSubscriberAuthenticationKey(), result.getDataId(), "subscriber1")); + assertTrue(JWTFilter.isJWTTokenValid(appSettings.getSubscriberAuthenticationKey(), result.getDataId(), WebSocketConstants.SUBSCRIBER_ID, "subscriber1")); - assertFalse(JWTFilter.isJWTTokenValid(appSettings.getSubscriberAuthenticationKey(), result.getDataId(), "subscriber12")); + assertFalse(JWTFilter.isJWTTokenValid(appSettings.getSubscriberAuthenticationKey(), result.getDataId(), WebSocketConstants.SUBSCRIBER_ID, "subscriber12")); result = pushNotificationRestService.getSubscriberAuthenticationToken("subscriber1", 3); - assertTrue(JWTFilter.isJWTTokenValid(appSettings.getSubscriberAuthenticationKey(), result.getDataId(), "subscriber1")); + assertTrue(JWTFilter.isJWTTokenValid(appSettings.getSubscriberAuthenticationKey(), result.getDataId(), WebSocketConstants.SUBSCRIBER_ID, "subscriber1")); String token = result.getDataId(); Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() -> { return !JWTFilter.isJWTTokenValid(appSettings.getSubscriberAuthenticationKey(), token,