diff --git a/README.md b/README.md index 8d97463..f0b61be 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,47 @@ server.info: ## push-message-provider -Интерфейсы и компоненты для модулей по обеспечению интеграции с сервисами отправки пуш-уведомлений. +Интерфейсы и компоненты для модулей по обеспечению интеграции с сервисами отправки пуш-уведомлений. Является необходимой зависимостью для использования провайдеров. + +Далее рассматривается пример использования подключаемых модулей-провайдеров. +``` kotlin +@Service +class PushSendingService( + private val pushMessageProviderServiceFactory: PushMessageProviderServiceFactory +) { + + fun sendPushMessage() { + val yourPushToken = "pushTokenForChecking" + val platform = PlatformType.ANDROID_GOOGLE + + val pushMessageProvider: PushMessageProviderService = pushMessageProviderServiceFactory.get(platform) + + val result = pushMessageProvider.check( // Проверка валидности токена для обозначения целесообразности отправки + PushTokenCheck( + pushToken = yourPushToken + ) + ) + + if (result.status == PushTokenStatus.VALID) { // Токен валиден, PushMessageProviderService интегрирован в систему + // Отправка пуш-уведомления + pushMessageProvider.send( + PushTokenMessage( + token = yourPushToken, + pushMessageNotification = PushMessageNotification( + title = "Your PushMessage", + description = "Provided by PushMessageProviderService", + imageUrl = null + ), + data = mapOf( + "customKey" to "customData" + ) + ) + ) + } + } + +} +``` ## push-message-provider-fcm @@ -231,7 +271,7 @@ push-message-provider: IOS: - FCM fcm: - appName: # Название приложения + appName: yourAppName auth: # Выбранный тип авторизации client: @@ -258,16 +298,60 @@ C) Данные из файла консоли Firebase, добавляемые auth: credentialsData: type: service_account - projectId: testProjectId - privateKeyId: testPrivateKeyId + projectId: yourProjectId + privateKeyId: yourPrivateKeyId privateKey: | -----BEGIN PRIVATE KEY----- - MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALfBshaLMW2yddmAZJRNXTZzcSbwvY93Dnjj6naWgoBJoB3mOM5bcoyWwBw12A4rwecorz74OUOc6zdqX3j8hwsSyzgAUStKM5PkOvPNRKsI4eXAWU0fmb8h1jyXwftl7EzeBjEMBTpyXkgDk3wLfHN6ciCZrnQndOvS+mMl3b0hAgMBAAECgYEAmIQZByMSrITR0ewCDyFDO52HjhWEkF310hsBkNoNiOMTFZ3vCj/WjJ/W5dM+90wUTYN0KOSnytmkVUNh6K5Yekn+yRg/mBRTwwn88hU6umB8tUqoNz7AyUltAOGyQMWqAAcVgxV+mAp/Y018j69poEHgrW4qKol65/NRZyV7/J0CQQD4rCDjmxGEuA1yMzL2i8NyNl/5vvLVfLcEnVqpHbc1+KfUHZuY7iv38xpzfmErqhCxAXfQ52edq5rXmMIVSbFrAkEAvSvfSSK9XQDJl3NEyfR3BGbsoqKIYOuJAnv4OQPSODZfTNWhc11S8y914qaSWB+Iid9HoLvAIgPH5mrzPzjSowJBAJcw4FZCI+aTmOlEI8ous8gvMy8/X5lZWFUf7s0/2fKgmjmnPsE+ndEFJ6HsxturbLaR8+05pJAClARdRjN3OL0CQGoF+8gmw1ErztCmVyiFbms2MGxagesoN4r/5jg2Tw0YVENg/HMHHCWWNREJ4L2pNsJnNOL+N4oY6mHXEWwesdcCQCUYTfLYxi+Wg/5BSC7fgl/gu0mlx07AzMoMQLDOXdisV5rpxrOoT3BOLBqyccv37AZ3e2gqb8JYyNzO6C0zswQ= -----END PRIVATE KEY----- - clientEmail: testClientEmail - clientId: testClientId - authUri: testAuthUri - tokenUri: testTokenUri - authProviderX509CertUrl: testAuthProviderX509CertUrl - clientX509CertUrl: testClientX509CertUrl + clientEmail: yourClientEmail + clientId: yourClientId + authUri: yourAuthUri + tokenUri: yourTokenUri + authProviderX509CertUrl: yourAuthProviderX509CertUrl + clientX509CertUrl: yourClientX509CertUrl +``` + +## push-message-provider-hpk + +Модуль по обеспечению интеграции с Huawei Push Kit. + +1) Подключение нового провайдера осуществляется при помощи аннотации `@EnablePushMessageProviderHpk`. +2) Для логирования запросов к сервису HPK нужно встроить в контейнер Spring собственный `WebClientLogger` из модуля `logger-spring-web` или же использовать стандартный посредством импорта конфигурации: +``` kotlin +@Import( + SpringLoggerConfiguration::class, + SpringLoggerWebConfiguration::class +) +class YourConfiguration +``` +3) Нужно добавить конфигурацию для считывания модулем. Пример файла в формате yaml: +``` yaml +push-message-provider: + platformProviders: + ANDROID_HUAWEI: + - HPK + hpk: + web-services: + client-id: yourClientId + oauth: + client-secret: yourClientSecret + url: https://oauth-login.cloud.huawei.com/oauth2/v3/ + http: + connection-timeout: 1s + read-timeout: 10s + write-timeout: 10s + ssl: # Опциональная структура + handshake-timeout: 1s + notify-read-timeout: 1s + notify-flush-timeout: 1s + hpk: + url: https://push-api.cloud.huawei.com/v1/ + http: + connection-timeout: 1s + read-timeout: 10s + write-timeout: 10s + ssl: # Опциональная структура + handshake-timeout: 1s + notify-read-timeout: 1s + notify-flush-timeout: 1s ``` diff --git a/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/clients/FcmClient.kt b/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/clients/FcmClient.kt index ec51bbe..126df6b 100644 --- a/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/clients/FcmClient.kt +++ b/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/clients/FcmClient.kt @@ -6,7 +6,7 @@ import org.springframework.stereotype.Component import ru.touchin.push.message.provider.dto.request.PushTokenCheck import ru.touchin.push.message.provider.dto.request.PushTokenMessage import ru.touchin.push.message.provider.dto.result.SendPushResult -import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageResult +import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageTraceableResult import ru.touchin.push.message.provider.enums.PushTokenStatus import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException import ru.touchin.push.message.provider.exceptions.PushMessageProviderException @@ -32,7 +32,7 @@ class FcmClient( fun check(request: PushTokenCheck): PushTokenStatus { val validationRequest = PushTokenMessage( token = request.pushToken, - notification = null, + pushMessageNotification = null, data = emptyMap() ) @@ -54,7 +54,7 @@ class FcmClient( return try { val messageId = firebaseMessaging.send(message, dryRun) - SendPushTokenMessageResult(messageId) + SendPushTokenMessageTraceableResult(messageId) } catch (e: FirebaseMessagingException) { throw firebaseMessagingExceptionConverter(e) } diff --git a/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/configurations/PushMessageProviderFcmConfiguration.kt b/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/configurations/PushMessageProviderFcmConfiguration.kt index b73da71..38613ee 100644 --- a/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/configurations/PushMessageProviderFcmConfiguration.kt +++ b/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/configurations/PushMessageProviderFcmConfiguration.kt @@ -1,6 +1,9 @@ package ru.touchin.push.message.provider.fcm.configurations +import com.fasterxml.jackson.annotation.JsonAutoDetect +import com.fasterxml.jackson.annotation.PropertyAccessor import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature import com.google.auth.oauth2.AccessToken import com.google.auth.oauth2.GoogleCredentials import com.google.firebase.FirebaseApp @@ -25,6 +28,7 @@ class PushMessageProviderFcmConfiguration { @Bean fun firebaseMessaging( properties: PushMessageProviderFcmProperties, + @Qualifier("push-message-provider.fcm.credentials-object-mapper") objectMapper: ObjectMapper ): FirebaseMessaging { val credentials = when { @@ -60,10 +64,22 @@ class PushMessageProviderFcmConfiguration { return FirebaseMessaging.getInstance(firebaseApp) } - @Bean - @Qualifier("push-message-provider.fcm.auth") + @Bean("push-message-provider.fcm.credentials-date-format") fun simpleDateFormat(): SimpleDateFormat { return SimpleDateFormat("yyyy-MM-dd HH:mm:ss X", Locale.getDefault()) } + @Bean("push-message-provider.fcm.credentials-object-mapper") + fun objectMapper( + @Qualifier("push-message-provider.fcm.credentials-date-format") + simpleDateFormat: SimpleDateFormat + ): ObjectMapper { + return ObjectMapper().apply { + configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) + setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + dateFormat = simpleDateFormat + } + } + } diff --git a/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/DateConverter.kt b/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/DateConverter.kt index 36ce38c..5a77ddc 100644 --- a/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/DateConverter.kt +++ b/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/DateConverter.kt @@ -10,7 +10,7 @@ import java.util.* @ConfigurationPropertiesBinding @Component class DateConverter( - @Qualifier("push-message-provider.fcm.auth") + @Qualifier("push-message-provider.fcm.credentials-date-format") private val simpleDateFormat: SimpleDateFormat ) : Converter { diff --git a/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/NotificationConverter.kt b/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/NotificationConverter.kt deleted file mode 100644 index cfd40bc..0000000 --- a/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/NotificationConverter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package ru.touchin.push.message.provider.fcm.converters - -import com.google.firebase.messaging.Notification as FcmNotification -import org.springframework.stereotype.Component -import ru.touchin.push.message.provider.dto.Notification - -@Component -class NotificationConverter { - - operator fun invoke(notification: Notification): FcmNotification { - return FcmNotification.builder() - .setTitle(notification.title) - .setBody(notification.description) - .setImage(notification.imageUrl) - .build() - } - -} diff --git a/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/PushMessageNotificationConverter.kt b/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/PushMessageNotificationConverter.kt new file mode 100644 index 0000000..b2c4219 --- /dev/null +++ b/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/PushMessageNotificationConverter.kt @@ -0,0 +1,18 @@ +package ru.touchin.push.message.provider.fcm.converters + +import com.google.firebase.messaging.Notification as FcmNotification +import org.springframework.stereotype.Component +import ru.touchin.push.message.provider.dto.PushMessageNotification + +@Component("push-message-provider.fcm.push-message-notification-converter") +class PushMessageNotificationConverter { + + operator fun invoke(pushMessageNotification: PushMessageNotification): FcmNotification { + return FcmNotification.builder() + .setTitle(pushMessageNotification.title) + .setBody(pushMessageNotification.description) + .setImage(pushMessageNotification.imageUrl) + .build() + } + +} diff --git a/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/PushTokenMessageConverter.kt b/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/PushTokenMessageConverter.kt index 43fc415..ce0c4b3 100644 --- a/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/PushTokenMessageConverter.kt +++ b/push-message-provider-fcm/src/main/kotlin/ru/touchin/push/message/provider/fcm/converters/PushTokenMessageConverter.kt @@ -5,13 +5,14 @@ import com.google.firebase.messaging.AndroidNotification import com.google.firebase.messaging.ApnsConfig import com.google.firebase.messaging.Aps import com.google.firebase.messaging.Message +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.stereotype.Component -import ru.touchin.push.message.provider.dto.Notification +import ru.touchin.push.message.provider.dto.PushMessageNotification import ru.touchin.push.message.provider.dto.request.PushTokenMessage -@Component +@Component("push-message-provider.fcm.push-token-message-converter") class PushTokenMessageConverter( - private val notificationConverter: NotificationConverter + private val pushMessageNotificationConverter: PushMessageNotificationConverter ) { private companion object { @@ -26,14 +27,14 @@ class PushTokenMessageConverter( .setToken(request.token) .setupApns() .setupAndroid() - .setIfExists(request.notification) + .setIfExists(request.pushMessageNotification) .putAllData(request.data) .build() } - private fun Message.Builder.setIfExists(notification: Notification?): Message.Builder { - return if (notification != null) { - setNotification(notificationConverter(notification)) + private fun Message.Builder.setIfExists(pushMessageNotification: PushMessageNotification?): Message.Builder { + return if (pushMessageNotification != null) { + setNotification(pushMessageNotificationConverter(pushMessageNotification)) } else { this } diff --git a/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/PushMessageProviderFcmTestApplication.kt b/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/PushMessageProviderFcmTestApplication.kt index 055a72d..a3fea68 100644 --- a/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/PushMessageProviderFcmTestApplication.kt +++ b/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/PushMessageProviderFcmTestApplication.kt @@ -1,36 +1,16 @@ package ru.touchin.push.message.provider.fcm -import com.fasterxml.jackson.annotation.JsonAutoDetect -import com.fasterxml.jackson.annotation.PropertyAccessor -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature import com.google.firebase.FirebaseApp -import org.springframework.beans.factory.annotation.Qualifier import org.springframework.boot.SpringBootConfiguration import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.ApplicationListener -import org.springframework.context.annotation.Bean import org.springframework.context.event.ContextRefreshedEvent -import java.text.SimpleDateFormat @TestConfiguration @SpringBootConfiguration @EnablePushMessageProviderFcm class PushMessageProviderFcmTestApplication : ApplicationListener { - @Bean - fun objectMapper( - @Qualifier("push-message-provider.fcm.auth") - simpleDateFormat: SimpleDateFormat - ): ObjectMapper { - return ObjectMapper().apply { - configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) - setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) - dateFormat = simpleDateFormat - } - } - override fun onApplicationEvent(event: ContextRefreshedEvent) { clearSingletonsOutsideContainer() } diff --git a/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/converters/NotificationConverterTest.kt b/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/converters/PushMessageNotificationConverterTest.kt similarity index 69% rename from push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/converters/NotificationConverterTest.kt rename to push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/converters/PushMessageNotificationConverterTest.kt index a15c159..6ecb019 100644 --- a/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/converters/NotificationConverterTest.kt +++ b/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/converters/PushMessageNotificationConverterTest.kt @@ -5,15 +5,15 @@ import org.junit.Assert import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest -import ru.touchin.push.message.provider.dto.Notification +import ru.touchin.push.message.provider.dto.PushMessageNotification import com.google.firebase.messaging.Notification as FcmNotification import org.junit.jupiter.api.DisplayName @SpringBootTest -class NotificationConverterTest { +class PushMessageNotificationConverterTest { @Autowired - lateinit var notificationConverter: NotificationConverter + lateinit var pushMessageNotificationConverter: PushMessageNotificationConverter @Autowired lateinit var objectMapper: ObjectMapper @@ -21,19 +21,19 @@ class NotificationConverterTest { @Test @DisplayName("Конвертация уведомления происходит корректно") fun invoke_basic() { - val notification = Notification( + val pushMessageNotification = PushMessageNotification( title = "title", description = "description", imageUrl = "imageUrl" ) - val realResult = notificationConverter(notification) + val realResult = pushMessageNotificationConverter(pushMessageNotification) val realResultJson = objectMapper.writeValueAsString(realResult) val expectedResult = FcmNotification.builder() - .setTitle(notification.title) - .setBody(notification.description) - .setImage(notification.imageUrl) + .setTitle(pushMessageNotification.title) + .setBody(pushMessageNotification.description) + .setImage(pushMessageNotification.imageUrl) .build() val expectedResultJson = objectMapper.writeValueAsString(expectedResult) diff --git a/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/converters/PushTokenMessageConverterTest.kt b/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/converters/PushTokenMessageConverterTest.kt index bcd0954..0ed8888 100644 --- a/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/converters/PushTokenMessageConverterTest.kt +++ b/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/converters/PushTokenMessageConverterTest.kt @@ -11,7 +11,7 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest -import ru.touchin.push.message.provider.dto.Notification +import ru.touchin.push.message.provider.dto.PushMessageNotification import ru.touchin.push.message.provider.dto.request.PushTokenMessage @SpringBootTest @@ -21,7 +21,7 @@ class PushTokenMessageConverterTest { lateinit var pushTokenMessageConverter: PushTokenMessageConverter @Autowired - lateinit var notificationConverter: NotificationConverter + lateinit var pushMessageNotificationConverter: PushMessageNotificationConverter @Autowired lateinit var objectMapper: ObjectMapper @@ -29,14 +29,14 @@ class PushTokenMessageConverterTest { @Test @DisplayName("Конвертация сообщения с уведомлением происходит корректно") fun invoke_withNotification() { - val notification = Notification( + val pushMessageNotification = PushMessageNotification( title = "title", description = "description", imageUrl = "imageUrl" ) val pushTokenMessage = PushTokenMessage( token = "token", - notification = notification, + pushMessageNotification = pushMessageNotification, data = mapOf("testKey" to "testvalue") ) @@ -45,7 +45,7 @@ class PushTokenMessageConverterTest { val expectedResult = Message.builder() .setToken(pushTokenMessage.token) - .setNotification(notificationConverter(notification)) + .setNotification(pushMessageNotificationConverter(pushMessageNotification)) .putAllData(pushTokenMessage.data) .setupApns() .setupAndroid() @@ -65,7 +65,7 @@ class PushTokenMessageConverterTest { fun invoke_withoutNotification() { val pushTokenMessage = PushTokenMessage( token = "token", - notification = null, + pushMessageNotification = null, data = mapOf("testKey" to "testvalue") ) diff --git a/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/services/PushMessageProviderFcmServiceTest.kt b/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/services/PushMessageProviderFcmServiceTest.kt index c502fa6..32d41a9 100644 --- a/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/services/PushMessageProviderFcmServiceTest.kt +++ b/push-message-provider-fcm/src/test/kotlin/ru/touchin/push/message/provider/fcm/services/PushMessageProviderFcmServiceTest.kt @@ -10,7 +10,7 @@ import org.springframework.boot.test.mock.mockito.MockBean import ru.touchin.push.message.provider.dto.request.PushTokenCheck import ru.touchin.push.message.provider.dto.request.PushTokenMessage import ru.touchin.push.message.provider.dto.result.CheckPushTokenResult -import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageResult +import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageTraceableResult import ru.touchin.push.message.provider.enums.PushTokenStatus import ru.touchin.push.message.provider.fcm.clients.FcmClient import ru.touchin.push.message.provider.services.PushMessageProviderService @@ -29,11 +29,11 @@ class PushMessageProviderFcmServiceTest { fun send_basic() { val request = PushTokenMessage( token = "testToken", - notification = null, + pushMessageNotification = null, data = emptyMap() ) - val expectedResult = SendPushTokenMessageResult("testMessageId") + val expectedResult = SendPushTokenMessageTraceableResult("testMessageId") Mockito.`when`( fcmClient.sendPushTokenMessage(request) diff --git a/push-message-provider-hpk/build.gradle.kts b/push-message-provider-hpk/build.gradle.kts new file mode 100644 index 0000000..b2bcd23 --- /dev/null +++ b/push-message-provider-hpk/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("kotlin") + id("kotlin-spring") + id("maven-publish") +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + implementation("org.springframework.boot:spring-boot-starter-webflux") + + implementation(project(":logger-spring")) + implementation(project(":common-spring-web")) + implementation(project(":push-message-provider")) + + testImplementation(project(":logger-spring-web")) + + testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.testcontainers:junit-jupiter") +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/EnablePushMessageProviderHpk.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/EnablePushMessageProviderHpk.kt new file mode 100644 index 0000000..a1cb474 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/EnablePushMessageProviderHpk.kt @@ -0,0 +1,7 @@ +package ru.touchin.push.message.provider.hpk + +import org.springframework.context.annotation.Import +import ru.touchin.push.message.provider.hpk.configurations.PushMessageProviderHpkConfiguration + +@Import(value = [PushMessageProviderHpkConfiguration::class]) +annotation class EnablePushMessageProviderHpk diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/builders/Buildable.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/builders/Buildable.kt new file mode 100644 index 0000000..a7c1910 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/builders/Buildable.kt @@ -0,0 +1,3 @@ +package ru.touchin.push.message.provider.hpk.base.builders + +internal interface Buildable diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/ConditionalWebClientParser.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/ConditionalWebClientParser.kt new file mode 100644 index 0000000..d1d3063 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/ConditionalWebClientParser.kt @@ -0,0 +1,42 @@ +package ru.touchin.push.message.provider.hpk.base.clients + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.web.reactive.function.client.ClientResponse +import ru.touchin.push.message.provider.hpk.base.clients.dto.ConditionalResponse + +internal open class ConditionalWebClientParser( + private val objectMapper: ObjectMapper, +) { + + open fun isOkResponse(clientResponse: ClientResponse): Boolean { + return clientResponse.statusCode().is2xxSuccessful + } + + @Throws(Exception::class) + inline fun parse( + clientResponse: ClientResponse, + body: String, + ): ConditionalResponse { + return if (isOkResponse(clientResponse)) { + ConditionalResponse( + success = parseValue(body, S::class.java), + failure = null + ) + } else { + ConditionalResponse( + success = null, + failure = parseValue(body, F::class.java) + ) + } + } + + private fun parseValue(source: String?, clazz: Class): T { + return if (clazz.canonicalName != String::class.java.canonicalName) { + objectMapper.readValue(source, clazz) + } else { + @Suppress("UNCHECKED_CAST") + source as T // T is String + } + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/ConfigurableWebClient.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/ConfigurableWebClient.kt new file mode 100644 index 0000000..f2df3d3 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/ConfigurableWebClient.kt @@ -0,0 +1,85 @@ +package ru.touchin.push.message.provider.hpk.base.clients + +import io.netty.channel.ChannelOption +import io.netty.handler.ssl.SslContextBuilder +import io.netty.handler.timeout.ReadTimeoutHandler +import io.netty.handler.timeout.WriteTimeoutHandler +import org.springframework.http.client.reactive.ReactorClientHttpConnector +import org.springframework.web.reactive.function.client.ClientResponse +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers +import reactor.netty.http.client.HttpClient +import ru.touchin.common.spring.web.webclient.BaseLogWebClient +import ru.touchin.common.spring.web.webclient.dto.RequestLogData +import ru.touchin.common.spring.web.webclient.logger.WebClientLogger +import ru.touchin.push.message.provider.hpk.base.clients.dto.ConditionalResponse +import ru.touchin.push.message.provider.hpk.properties.HpkProperties +import java.util.concurrent.TimeUnit + +abstract class ConfigurableWebClient( + webClientLogger: WebClientLogger, + webClientBuilder: WebClient.Builder, + protected val webService: HpkProperties.WebService, +) : BaseLogWebClient(webClientLogger, webClientBuilder) { + + private val conditionalWebClientParser: Lazy = lazy { + ConditionalWebClientParser( + objectMapper = getObjectMapper(), + ) + } + + protected fun WebClient.Builder.setTimeouts(): WebClient.Builder { + val httpClient: HttpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, webService.http.connectionTimeout.toMillis().toInt()) + .doOnConnected { setup -> + setup.addHandlerLast(ReadTimeoutHandler(webService.http.readTimeout.toMillis(), TimeUnit.MILLISECONDS)) + setup.addHandlerLast(WriteTimeoutHandler(webService.http.writeTimeout.toMillis(), TimeUnit.MILLISECONDS)) + } + .let { httpClient -> + webService.ssl?.let { ssl -> + httpClient.secure { builder -> + builder + .sslContext(SslContextBuilder.forClient().build()) + .handshakeTimeout(ssl.handshakeTimeout) + .closeNotifyFlushTimeout(ssl.notifyFlushTimeout) + .closeNotifyReadTimeout(ssl.notifyReadTimeout) + } + } ?: httpClient + } + + return clientConnector(ReactorClientHttpConnector(httpClient)) + } + + internal inline fun WebClient.RequestHeadersSpec<*>.exchangeWithWrap( + requestLogData: RequestLogData, + ): Mono> { + return exchangeToMono { clientResponse -> + parse(clientResponse) + }.doOnNext { responseWrapper -> + getLogger().log( + requestLogData.copy( + responseBody = responseWrapper.success ?: responseWrapper.failure + ) + ) + } + } + + internal inline fun parse( + clientResponse: ClientResponse, + ): Mono> { + val responseBody = clientResponse + .bodyToMono(String::class.java) + .defaultIfEmpty(String()) + .publishOn(Schedulers.parallel()) + + return responseBody + .map { body -> + conditionalWebClientParser.value.parse( + clientResponse = clientResponse, + body = body, + ) + } + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/dto/ConditionalResponse.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/dto/ConditionalResponse.kt new file mode 100644 index 0000000..ee865b8 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/dto/ConditionalResponse.kt @@ -0,0 +1,15 @@ +package ru.touchin.push.message.provider.hpk.base.clients.dto + +internal open class ConditionalResponse( + val success: S?, + val failure: F?, +) { + + init { + // Only one value should be present + check((success == null) != (failure == null)) + } + + val isSuccess: Boolean = success != null + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/enums/ValueableSerializableEnum.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/enums/ValueableSerializableEnum.kt new file mode 100644 index 0000000..388d0c9 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/enums/ValueableSerializableEnum.kt @@ -0,0 +1,12 @@ +package ru.touchin.push.message.provider.hpk.base.enums + +import com.fasterxml.jackson.annotation.JsonValue + +internal interface ValueableSerializableEnum { + + val value: T + + @JsonValue + fun toValue(): T = value + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/extensions/BuilderExtensions.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/extensions/BuilderExtensions.kt new file mode 100644 index 0000000..fb00d9c --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/extensions/BuilderExtensions.kt @@ -0,0 +1,7 @@ +package ru.touchin.push.message.provider.hpk.base.extensions + +import ru.touchin.push.message.provider.hpk.base.builders.Buildable + +fun B.ifNotNull(value: V?, setter: B.(V) -> B): B { + return value?.let { setter(it) } ?: this +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms/enums/HmsResponseCode.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms/enums/HmsResponseCode.kt new file mode 100644 index 0000000..796f7fc --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms/enums/HmsResponseCode.kt @@ -0,0 +1,54 @@ +package ru.touchin.push.message.provider.hpk.clients.hms.enums + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +internal enum class HmsResponseCode( + override val value: Int, + val description: String, +) : ValueableSerializableEnum { + + UNKNOWN(-1, "Unknown"), + INVALID_CLIENT_SECRET(1101, "Invalid client_secret: app or server has mismatching credentials"), + SUCCESS(80000000, "Success"), + SOME_TOKENS_ARE_INVALID(80100000, "Some tokens are right, the others are illegal"), + PARAMETERS_ARE_INVALID(80100001, "Parameters check error"), + PUSH_TOKEN_NOT_SPECIFIED(80100002, "Token number should be one when send sys message"), + INCORRECT_MESSAGE_STRUCTURE(80100003, "Incorrect message structure"), + TTL_IS_INVALID(80100004, "TTL is less than current time, please check"), + COLLAPSE_KEY_IS_INVALID(80100013, "Collapse_key is illegal, please check"), + MESSAGE_DATA_IS_VULNERABLE(80100016, "Message contains sensitive information, please check"), + TOPIC_AMOUNT_EXCEEDED(80100017, "A maximum of 100 topic-based messages can be sent at the same time"), + INVALID_MESSAGE_BODY(80100018, "Invalid message body"), + OAUTH_AUTHENTICATION_ERROR(80200001, "Oauth authentication error"), + OAUTH_TOKEN_EXPIRED(80200003, "Oauth Token expired"), + PERMISSION_DENIED(80300002, "There is no permission to send a message to a specified device"), + INVALID_TOKEN(80300007, "The specified token is invalid"), + MESSAGE_SIZE_EXCEEDED(80300008, "The message body size exceeds the default value set by the system (4K)"), + TOKEN_AMOUNT_EXCEEDED(80300010, "Tokens exceed the default value"), + MESSAGE_PERMISSION_DENIED(80300011, "No permission to send high-level notification messages"), + OAUTH_SERVER_ERROR(80600003, "Request OAuth service failed"), + INTERNAL_SERVER_ERROR(81000001, "System inner error"), + GROUP_ERROR(82000001, "GroupKey or groupName error"), + GROUP_MISMATCH(82000002, "GroupKey and groupName do not match"), + INVALID_TOKEN_ARRAY(82000003, "Token array is null"), + GROUP_NOT_EXIST(82000004, "Group do not exist"), + GROUP_APP_MISMATCH(82000005, "Group do not belong to this app"), + INVALID_TOKEN_ARRAY_OR_GROUP(82000006, "Token array or group number is transfinited"), + INVALID_TOPIC(82000007, "Invalid topic"), + TOKEN_AMOUNT_IS_NULL_OR_EXCEEDED(82000008, "Token array null or transfinited"), + TOO_MANY_TOPICS(82000009, "Topic amount exceeded: at most 2000"), + SOME_TOKENS_ARE_INCORRECT(82000010, "Some tokens are incorrect"), + TOKEN_IS_NULL(82000011, "Token is null"), + DATA_LOCATION_NOT_SPECIFIED(82000012, "Data storage location is not selected"), + DATA_LOCATION_MISMATCH(82000013, "Data storage location does not match the actual data"); + + companion object { + + fun fromCode(code: String): HmsResponseCode { + return values().find { it.value.toString() == code } + ?: UNKNOWN + } + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/HmsHpkWebClient.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/HmsHpkWebClient.kt new file mode 100644 index 0000000..cf6cbb1 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/HmsHpkWebClient.kt @@ -0,0 +1,71 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.client.WebClient +import ru.touchin.common.spring.web.webclient.dto.RequestLogData +import ru.touchin.common.spring.web.webclient.logger.WebClientLogger +import ru.touchin.push.message.provider.hpk.base.clients.ConfigurableWebClient +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.bodies.HmsHpkMessagesSendBody +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.requests.HmsHpkMessagesSendRequest +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.responses.HmsHpkResponse +import ru.touchin.push.message.provider.hpk.properties.HpkProperties + +/** + * Client for Huawei Push Kit. + * @see Documentation + */ +@Component +class HmsHpkWebClient( + webClientLogger: WebClientLogger, + @Qualifier("push-message-provider.hpk.hms-hpk-webclient-builder") + webClientBuilder: WebClient.Builder, + private val hpkProperties: HpkProperties, + @Qualifier("push-message-provider.hpk.webclient-objectmapper") + private val objectMapper: ObjectMapper, +) : ConfigurableWebClient(webClientLogger, webClientBuilder, hpkProperties.webServices.hpk) { + + override fun getObjectMapper(): ObjectMapper = objectMapper + + override fun getWebClient(): WebClient { + return getWebClientBuilder( + url = webService.url.toString(), + ) + .setTimeouts() + .build() + } + + internal fun messagesSend(hmsHpkMessagesSendRequest: HmsHpkMessagesSendRequest): HmsHpkResponse { + val url = "${hpkProperties.webServices.clientId}/$METHOD_MESSAGES_SEND" + + return getWebClient().post() + .uri { builder -> + builder + .path(url) + .build() + } + .contentType(MediaType.APPLICATION_JSON) + .headers { it.setBearerAuth(hmsHpkMessagesSendRequest.accessToken) } + .body(BodyInserters.fromValue(hmsHpkMessagesSendRequest.hmsHpkMessagesSendBody)) + .exchange( + clazz = HmsHpkResponse::class.java, + requestLogData = RequestLogData( + uri = url, + logTags = listOf(), + method = HttpMethod.POST, + ) + ) + .block() ?: throw IllegalStateException("No response") + } + + private companion object { + + const val METHOD_MESSAGES_SEND = "messages:send" + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/bodies/HmsHpkMessagesSendBody.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/bodies/HmsHpkMessagesSendBody.kt new file mode 100644 index 0000000..79910f9 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/bodies/HmsHpkMessagesSendBody.kt @@ -0,0 +1,12 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.bodies + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.Message + +internal class HmsHpkMessagesSendBody( + /** Send "dry" message without notification delivery */ + val validateOnly: Boolean, + /** Message structure, which must contain the valid message payload and valid sending object. */ + @JsonProperty("message") + val message: Message, +) diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/AndroidConfig.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/AndroidConfig.kt new file mode 100644 index 0000000..0da975a --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/AndroidConfig.kt @@ -0,0 +1,163 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.base.builders.Buildable +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android.AndroidNotificationConfig +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidFastAppTargetType +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidUrgency + +internal data class AndroidConfig private constructor( + /** + * Mode for the Push Kit server to cache messages sent to an offline device. + * These cached messages will be delivered once the device goes online again. + * The options are as follows: + * + * 0: Only the latest message sent by each app to the user device is cached. + * + * -1: All messages are cached. + * + * 1-100: message cache group ID. Messages are cached by group. Each group can cache only one message for each app. + * + * For example, if you send 10 messages and set collapse_key to 1 for the first five messages and to 2 for the rest, the latest message whose value of collapse_key is 1 and the latest message whose value of collapse_key is 2 are sent to the user after the user's device goes online. + * + * The default value is -1. + * */ + val collapseKey: Short?, + @JsonProperty("urgency") + val androidUrgency: AndroidUrgency?, + val category: String?, + /** + * Message cache duration, in seconds. + * When a user device is offline, the Push Kit server caches messages. + * If the user device goes online within the message cache time, the cached messages are delivered. + * Otherwise, the messages are discarded. + * */ + val ttl: String?, + /** + * Tag of a message in a batch delivery task. + * The tag is returned to your server when Push Kit sends the message receipt. + * */ + val biTag: String?, + /** State of a mini program when a quick app sends a data message. Default is [AndroidFastAppTargetType.PRODUCTION]*/ + @JsonProperty("fast_app_target") + val androidFastAppTargetType: AndroidFastAppTargetType?, + /** Custom message payload. If the data parameter is set, the value of the [Message.data] field is overwritten. */ + val data: String?, + @JsonProperty("notification") + val androidNotificationConfig: AndroidNotificationConfig?, + /** Unique receipt ID that associates with the receipt URL and configuration of the downlink message. */ + val receiptId: String?, +) { + + class Validator { + + fun check(androidConfig: AndroidConfig, notification: Notification?) { + with(androidConfig) { + if (collapseKey != null) { + require( + collapseKey in COLLAPSE_KEY_RANGE_CONSTRAINT + ) + { "Collapse Key must be in $COLLAPSE_KEY_MIN_VALUE and $COLLAPSE_KEY_MAX_VALUE" } + } + if (ttl != null) { + require(ttl.matches(TTL_PATTERN)) { "The TTL's format is wrong" } + } + if (androidNotificationConfig != null) { + AndroidNotificationConfig.validator.check(androidNotificationConfig, notification) + } + } + } + + private companion object { + + const val COLLAPSE_KEY_MIN_VALUE: Byte = -1 + const val COLLAPSE_KEY_MAX_VALUE: Byte = 100 + val COLLAPSE_KEY_RANGE_CONSTRAINT: IntRange = COLLAPSE_KEY_MIN_VALUE..COLLAPSE_KEY_MAX_VALUE + val TTL_PATTERN: Regex = Regex("\\d+|\\d+[sS]|\\d+.\\d{1,9}|\\d+.\\d{1,9}[sS]") + + } + + } + + class Builder : Buildable { + + private var collapseKey: Short? = null + private var androidUrgency: AndroidUrgency? = null + private var category: String? = null + private var ttl: String? = null + private var biTag: String? = null + private var androidFastAppTargetType: AndroidFastAppTargetType? = null + private var data: String? = null + private var androidNotificationConfig: AndroidNotificationConfig? = null + private var receiptId: String? = null + + fun setCollapseKey(collapseKey: Short): Builder { + this.collapseKey = collapseKey + return this + } + + fun setUrgency(androidUrgency: AndroidUrgency): Builder { + this.androidUrgency = androidUrgency + return this + } + + fun setCategory(category: String?): Builder { + this.category = category + return this + } + + fun setTtl(ttl: String): Builder { + this.ttl = ttl + return this + } + + fun setBiTag(biTag: String): Builder { + this.biTag = biTag + return this + } + + fun setFastAppTargetType(androidFastAppTargetType: AndroidFastAppTargetType): Builder { + this.androidFastAppTargetType = androidFastAppTargetType + return this + } + + fun setData(data: String): Builder { + this.data = data + return this + } + + fun setAndroidNotificationConfig(androidNotificationConfig: AndroidNotificationConfig): Builder { + this.androidNotificationConfig = androidNotificationConfig + return this + } + + fun setReceiptId(receiptId: String): Builder { + this.receiptId = receiptId + return this + } + + fun build(): AndroidConfig { + return AndroidConfig( + collapseKey = collapseKey, + androidUrgency = androidUrgency, + category = category, + ttl = ttl, + biTag = biTag, + androidFastAppTargetType = androidFastAppTargetType, + data = data, + androidNotificationConfig = androidNotificationConfig, + receiptId = receiptId, + ) + } + + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/ApnsConfig.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/ApnsConfig.kt new file mode 100644 index 0000000..ef8a813 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/ApnsConfig.kt @@ -0,0 +1,76 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.base.builders.Buildable +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns.ApnsHeaders +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns.ApnsHmsOptions +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns.Aps + +internal data class ApnsConfig private constructor( + @JsonProperty("hms_options") + val apnsHmsOptions: ApnsHmsOptions?, + @JsonProperty("apns_headers") + val apnsHeaders: ApnsHeaders, + val payload: Map, +) { + + class Validator { + + fun check(apnsConfig: ApnsConfig) { + with(apnsConfig) { + apnsHmsOptions?.also { ApnsHmsOptions.validator.check(it) } + ApnsHeaders.validator.check(apnsHeaders) + + if (payload[APS_PAYLOAD_KEY] != null) { + val aps: Aps? = payload[APS_PAYLOAD_KEY] as Aps? + + aps?.also { Aps.validator.check(it) } + } + } + } + + } + + class Builder : Buildable { + + private var apnsHmsOptions: ApnsHmsOptions? = null + private var payload: MutableMap = mutableMapOf() + private var aps: Aps? = null + + fun setApnsHmsOptions(apnsHmsOptions: ApnsHmsOptions): Builder { + this.apnsHmsOptions = apnsHmsOptions + return this + } + + fun addPayload(payload: Map): Builder { + this.payload.putAll(payload) + return this + } + + fun setAps(aps: Aps): Builder { + this.aps = aps + payload[APS_PAYLOAD_KEY] = aps + return this + } + + fun build(apnsHeaders: ApnsHeaders): ApnsConfig { + return ApnsConfig( + apnsHmsOptions = apnsHmsOptions, + apnsHeaders = apnsHeaders, + payload = payload + ) + } + + } + + companion object { + + const val APS_PAYLOAD_KEY = "aps" + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/Message.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/Message.kt new file mode 100644 index 0000000..387f393 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/Message.kt @@ -0,0 +1,136 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.base.builders.Buildable + +internal data class Message private constructor( + /** Custom message payload. Map of key-values */ + val data: String?, + /** Notification message content. */ + @JsonProperty("notification") + val notification: Notification?, + /** Android message push control. */ + @JsonProperty("android") + val android: AndroidConfig?, + @JsonProperty("apns") + val apns: ApnsConfig?, + @JsonProperty("webpush") + val webpush: WebPushConfig?, + /** Push token of the target user of a message. */ + val token: Collection?, + /** Topic subscribed by the target user of a message. */ + val topic: String?, + val condition: String?, +) { + + class Validator { + + fun check(message: Message) { + with(message) { + require( + arrayOf( + !token.isNullOrEmpty(), + !topic.isNullOrBlank(), + !condition.isNullOrBlank(), + ).count { it } == MAX_TYPES_OF_DELIVERY + ) { "Exactly one of token, topic or condition must be specified" } + + if (token != null) { + require( + token.size in TOKENS_SIZE_RANGE_CONSTRAINT + ) { "Number of tokens, if specified, must be from $TOKENS_SIZE_MIN to $TOKENS_SIZE_MAX" } + } + + notification?.also { Notification.validator.check(it) } + android?.also { AndroidConfig.validator.check(it, notification) } + apns?.also { ApnsConfig.validator.check(it) } + webpush?.also { WebPushConfig.validator.check(it) } + } + } + + private companion object { + + const val MAX_TYPES_OF_DELIVERY: Int = 1 + const val TOKENS_SIZE_MIN: Byte = 1 + const val TOKENS_SIZE_MAX: Short = 1000 + val TOKENS_SIZE_RANGE_CONSTRAINT: IntRange = TOKENS_SIZE_MIN..TOKENS_SIZE_MAX + + } + + } + + class Builder : Buildable { + + private var data: String? = null + private var notification: Notification? = null + private var androidConfig: AndroidConfig? = null + private var apns: ApnsConfig? = null + private var webpush: WebPushConfig? = null + private val token: MutableList = mutableListOf() + private var topic: String? = null + private var condition: String? = null + + fun setData(data: String): Builder { + this.data = data + return this + } + + fun setNotification(notification: Notification): Builder { + this.notification = notification + return this + } + + fun setAndroidConfig(androidConfig: AndroidConfig): Builder { + this.androidConfig = androidConfig + return this + } + + fun setApns(apns: ApnsConfig): Builder { + this.apns = apns + return this + } + + fun setWebpush(webpush: WebPushConfig): Builder { + this.webpush = webpush + return this + } + + fun addToken(vararg token: String): Builder { + this.token.addAll(token) + return this + } + + fun setTopic(topic: String): Builder { + this.topic = topic + return this + } + + fun setCondition(condition: String): Builder { + this.condition = condition + return this + } + + fun build(): Message { + return Message( + data = data, + notification = notification, + android = androidConfig, + apns = apns, + webpush = webpush, + topic = topic, + condition = condition, + token = token.takeIf(Collection<*>::isNotEmpty), + ) + } + + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/Notification.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/Notification.kt new file mode 100644 index 0000000..a6036d6 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/Notification.kt @@ -0,0 +1,72 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto + +import ru.touchin.push.message.provider.hpk.base.builders.Buildable + +internal data class Notification private constructor( + /** Title for notification. Must be specified here or in [AndroidNotificationConfig.title] */ + val title: String?, + /** Text body for notification. Must be specified here or in [AndroidNotificationConfig.body] */ + val body: String?, + /** Url of image */ + val image: String?, +) { + + class Validator { + + fun check(notification: Notification) { + with(notification) { + if (image != null) { + require( + image.startsWith(HTTPS_URL_PATTERN) + ) { "image's url should start with $HTTPS_URL_PATTERN" } + } + } + } + + private companion object { + + const val HTTPS_URL_PATTERN: String = "https" + + } + + } + + class Builder : Buildable { + + private var title: String? = null + private var body: String? = null + private var image: String? = null + + fun setTitle(title: String): Builder { + this.title = title + return this + } + + fun setBody(body: String): Builder { + this.body = body + return this + } + + fun setImage(image: String): Builder { + this.image = image + return this + } + + fun build(): Notification { + return Notification( + title = title, + body = body, + image = image + ) + } + + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/WebPushConfig.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/WebPushConfig.kt new file mode 100644 index 0000000..4184357 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/WebPushConfig.kt @@ -0,0 +1,77 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.base.builders.Buildable +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web.WebHmsOptions +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web.WebNotification +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web.WebPushHeaders + +internal data class WebPushConfig private constructor( + @JsonProperty("headers") + val webPushHeaders: WebPushHeaders?, + val data: String?, + @JsonProperty("notification") + val webNotification: WebNotification?, + @JsonProperty("hms_options") + val webHmsOptions: WebHmsOptions?, +) { + + class Validator { + + fun check(webPushConfig: WebPushConfig) { + with(webPushConfig) { + webPushHeaders?.let { WebPushHeaders.validator.check(it) } + webNotification?.let { WebNotification.validator.check(it) } + webHmsOptions?.let { WebHmsOptions.validator.check(it) } + } + } + + } + + class Builder : Buildable { + + private var headers: WebPushHeaders? = null + private var data: String? = null + private var notification: WebNotification? = null + private var webHmsOptions: WebHmsOptions? = null + + fun setHeaders(webPushHeaders: WebPushHeaders): Builder { + this.headers = webPushHeaders + return this + } + + fun setData(data: String): Builder { + this.data = data + return this + } + + fun setNotification(webNotification: WebNotification): Builder { + this.notification = notification + return this + } + + fun setWebHmsOptions(webHmsOptions: WebHmsOptions): Builder { + this.webHmsOptions + return this + } + + fun build(): WebPushConfig { + return WebPushConfig( + webPushHeaders = headers, + data = data, + webNotification = notification, + webHmsOptions = webHmsOptions, + ) + } + + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidBadgeNotification.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidBadgeNotification.kt new file mode 100644 index 0000000..bdd39e6 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidBadgeNotification.kt @@ -0,0 +1,78 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.base.builders.Buildable + +internal data class AndroidBadgeNotification private constructor( + /** Accumulative badge number. */ + val addNum: Short?, + /** Full path of the app entry activity class. */ + @JsonProperty("class") + val clazz: String, + /** Badge number. Overrides [addNum]. */ + val setNum: Short?, +) { + + class Validator { + + fun check(androidBadgeNotification: AndroidBadgeNotification) { + with(androidBadgeNotification) { + if (addNum != null) { + require( + addNum in ADD_NUM_MIN_VALUE..ADD_NUM_MAX_VALUE + ) { "add_num must locate in $ADD_NUM_MIN_VALUE and $ADD_NUM_MAX_VALUE" } + } + + if (setNum != null) { + require( + setNum in SET_NUM_RANGE_CONSTRAINT + ) { "set_num must locate between $SET_NUM_MIN_VALUE and $SET_NUM_MAX_VALUE" } + } + } + } + + private companion object { + + const val ADD_NUM_MIN_VALUE: Byte = 1 + const val ADD_NUM_MAX_VALUE: Byte = 99 + val ADD_NUM_RANGE_CONSTRAINT: IntRange = ADD_NUM_MIN_VALUE..ADD_NUM_MAX_VALUE + const val SET_NUM_MIN_VALUE: Byte = 0 + const val SET_NUM_MAX_VALUE: Byte = 99 + val SET_NUM_RANGE_CONSTRAINT: IntRange = SET_NUM_MIN_VALUE..SET_NUM_MAX_VALUE + + } + + } + + class Builder : Buildable { + + private var addNum: Short? = null + private var setNum: Short? = null + + fun setAddNum(addNum: Short): Builder { + this.addNum = addNum + return this + } + + fun setSetNum(setNum: Short): Builder { + this.setNum = setNum + return this + } + + fun build(badgeClass: String): AndroidBadgeNotification { + return AndroidBadgeNotification( + addNum = addNum, + clazz = badgeClass, + setNum = setNum, + ) + } + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidButton.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidButton.kt new file mode 100644 index 0000000..6f1b17e --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidButton.kt @@ -0,0 +1,80 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.base.builders.Buildable +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidActionType +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidIntentType + +internal data class AndroidButton private constructor( + /** Button name. */ + val name: String, + /** Button action. */ + @JsonProperty("action_type") + val androidActionType: AndroidActionType, + /** Method of opening a custom app page. */ + @JsonProperty("intent_type") + val androidIntentType: AndroidIntentType?, + val intent: String?, + /** Map of key-values. */ + val data: String?, +) { + + class Validator { + + fun check(androidButton: AndroidButton) { + with(androidButton) { + require( + name.length <= NAME_MAX_LENGTH + ) { "Button name length cannot exceed $NAME_MAX_LENGTH" } + + if (androidActionType == AndroidActionType.SHARE_NOTIFICATION_MESSAGE) { + require(!data.isNullOrEmpty()) { "Data is needed when actionType is $androidActionType" } + require(data.length <= DATA_MAX_LENGTH) { "Data length cannot exceed $DATA_MAX_LENGTH chars" } + } + } + } + + private companion object { + + const val NAME_MAX_LENGTH: Byte = 40 + const val DATA_MAX_LENGTH: Short = 1024 + + } + + } + + class Builder : Buildable { + + private var intent: String? = null + private var data: String? = null + + fun setIntent(intent: String): Builder { + this.intent = intent + return this + } + + fun setData(data: String): Builder { + this.data = data + return this + } + + fun build(name: String, androidActionType: AndroidActionType, androidIntentType: AndroidIntentType): AndroidButton { + return AndroidButton( + name = name, + androidActionType = androidActionType, + androidIntentType = androidIntentType, + intent = intent, + data = data + ) + } + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidClickAction.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidClickAction.kt new file mode 100644 index 0000000..a15a7e4 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidClickAction.kt @@ -0,0 +1,93 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.base.builders.Buildable +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidClickActionType + +internal data class AndroidClickAction private constructor( + /** Message tapping action type. */ + @JsonProperty("type") + val androidClickActionType: AndroidClickActionType, + val intent: String?, + /** URL to be opened. */ + val url: String?, + /** Action corresponding to the activity of the page to be opened when the custom app page is opened through the action. */ + val action: String?, +) { + + class Validator { + + fun check(androidClickAction: AndroidClickAction) { + with(androidClickAction) { + when (androidClickActionType) { + AndroidClickActionType.CUSTOMIZE_ACTION -> require( + !intent.isNullOrBlank() || !action.isNullOrBlank() + ) { "intent or action is required when click type is $androidClickActionType" } + + AndroidClickActionType.OPEN_URL -> { + require(!url.isNullOrBlank()) { "url is required when click type is $androidClickActionType" } + require(url.startsWith(HTTPS_PATTERN_START)) { "url must start with $HTTPS_PATTERN_START" } + } + + AndroidClickActionType.OPEN_APP -> { + // no verification + } + } + } + } + + private companion object { + + const val HTTPS_PATTERN_START = "https" + + } + + } + + class Builder : Buildable { + + private var intent: String? = null + private var url: String? = null + private var richResource: String? = null + private var action: String? = null + + fun setIntent(intent: String): Builder { + this.intent = intent + return this + } + + fun setUrl(url: String): Builder { + this.url = url + return this + } + + fun setRichResource(richResource: String): Builder { + this.richResource = richResource + return this + } + + fun setAction(action: String): Builder { + this.action = action + return this + } + + fun build(androidClickActionType: AndroidClickActionType): AndroidClickAction { + return AndroidClickAction( + androidClickActionType = androidClickActionType, + intent = intent.takeIf { androidClickActionType == AndroidClickActionType.CUSTOMIZE_ACTION }, + action = action.takeIf { androidClickActionType == AndroidClickActionType.CUSTOMIZE_ACTION }, + url = url.takeIf { androidClickActionType == AndroidClickActionType.OPEN_URL }, + ) + } + + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidColor.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidColor.kt new file mode 100644 index 0000000..000d163 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidColor.kt @@ -0,0 +1,82 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android + +import ru.touchin.push.message.provider.hpk.base.builders.Buildable + +internal data class AndroidColor private constructor( + /** Alpha setting of the RGB color.*/ + val alpha: Float, + /** Red setting of the RGB color. */ + val red: Float, + /** Green setting of the RGB color. */ + val green: Float, + /** Green setting of the RGB color. */ + val blue: Float, +) { + + class Validator { + + fun check(androidColor: AndroidColor) { + with(androidColor) { + require(alpha in COLOR_RANGE_CONSTRAINT) { "Alpha must be locate between [0,1]" } + require(red in COLOR_RANGE_CONSTRAINT) { "Red must be locate between [0,1]" } + require(green in COLOR_RANGE_CONSTRAINT) { "Green must be locate between [0,1]" } + require(blue in COLOR_RANGE_CONSTRAINT) { "Blue must be locate between [0,1]" } + } + } + + private companion object { + + private const val ZERO: Float = 0.0f + private const val ONE: Float = 1.0f + val COLOR_RANGE_CONSTRAINT = ZERO..ONE + + } + + } + + class Builder : Buildable { + + private var alpha: Float = 1.0f + private var red: Float = 0.0f + private var green: Float = 0.0f + private var blue: Float = 0.0f + + fun setAlpha(alpha: Float): Builder { + this.alpha = alpha + return this + } + + fun setRed(red: Float): Builder { + this.red = red + return this + } + + fun setGreen(green: Float): Builder { + this.green = green + return this + } + + fun setBlue(blue: Float): Builder { + this.blue = blue + return this + } + + fun build(): AndroidColor { + return AndroidColor( + alpha = alpha, + red = red, + green = green, + blue = blue + ) + } + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidLightSettings.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidLightSettings.kt new file mode 100644 index 0000000..7d4a837 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidLightSettings.kt @@ -0,0 +1,63 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.base.builders.Buildable + +internal data class AndroidLightSettings private constructor( + /** Breathing light color. */ + @JsonProperty("color") + val androidColor: AndroidColor, + /** Interval when a breathing light is on */ + val lightOnDuration: String, + /** Interval when a breathing light is off */ + val lightOffDuration: String, +) { + + class Validator { + + fun check(androidLightSettings: AndroidLightSettings) { + with(androidLightSettings) { + AndroidColor.validator.check(androidColor) + + require( + lightOnDuration.matches(LIGHT_DURATION_PATTERN) + ) { "light_on_duration pattern is wrong" } + require( + lightOffDuration.matches(LIGHT_DURATION_PATTERN) + ) { "light_off_duration pattern is wrong" } + } + } + + private companion object { + + val LIGHT_DURATION_PATTERN: Regex = Regex("\\d+|\\d+[sS]|\\d+.\\d{1,9}|\\d+.\\d{1,9}[sS]") + + } + + } + + class Builder : Buildable { + + fun build( + color: AndroidColor, + lightOnDuration: String, + lightOffDuration: String + ): AndroidLightSettings { + return AndroidLightSettings( + androidColor = color, + lightOnDuration = lightOnDuration, + lightOffDuration = lightOffDuration, + ) + } + + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidNotificationConfig.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidNotificationConfig.kt new file mode 100644 index 0000000..37a36cb --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/android/AndroidNotificationConfig.kt @@ -0,0 +1,417 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.base.builders.Buildable +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.Notification +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidImportance +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidStyleType +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidVisibility + +internal data class AndroidNotificationConfig private constructor( + /** + * Title of an Android notification message. + * If the title parameter is set, the value of the [Notification.title] field is overwritten. + * */ + val title: String?, + /** + * Body of an Android notification message. + * If the body parameter is set, the value of the [Notification.body] field is overwritten. + * */ + val body: String?, + /** + * Custom app icon on the left of a notification message. + */ + val icon: String?, + /** Custom notification bar button color. */ + val color: String?, + val sound: String?, + /** Indicates whether to use the default ringtone. */ + val defaultSound: Boolean, + /** + * Message tag. + * Messages that use the same message tag in the same app will be overwritten by the latest message. + * */ + val tag: String?, + @JsonProperty("click_action") + val androidClickAction: AndroidClickAction?, + val bodyLocKey: String?, + val bodyLocArgs: Collection?, + val titleLocKey: String?, + val titleLocArgs: Collection?, + val multiLangKey: Map?, + /** Custom channel for displaying notification messages. */ + val channelId: String?, + /** Brief description of a notification message to an Android app. */ + val notifySummary: String?, + /** URL of the custom small image on the right of a notification message. */ + val image: String?, + /** Notification bar style. */ + @JsonProperty("style") + val androidStyleType: AndroidStyleType?, + /** Android notification message title in large text style. */ + val bigTitle: String?, + val bigBody: String?, + /** + * Unique notification ID of a message. + * If a message does not contain the ID or the ID is -1, the NC will generate a unique ID for the message. + * Different notification messages can use the same notification ID, so that new messages can overwrite old messages. + * */ + val notifyId: Int?, + /** + * Message group. + * For example, if 10 messages that contain the same value of group are sent to a device, + * the device displays only the latest message and the total number of messages received in the group, + * but does not display these 10 messages. + */ + val group: String?, + @JsonProperty("badge") + val androidBadgeNotification: AndroidBadgeNotification? = null, + val autoCancel: Boolean, + /** + * Time when Android notification messages are delivered, in the UTC timestamp format. + * If you send multiple messages at the same time, + * they will be sorted based on this value and displayed in the Android notification panel. + * Example: 2014-10-02T15:01:23.045123456Z + */ + @JsonProperty("when") + val sendAt: String?, + val localOnly: Boolean? = null, + /** + * Android notification message priority, which determines the message notification behavior of a user device. + */ + @JsonProperty("importance") + val androidImportance: AndroidImportance?, + /** Indicates whether to use the default vibration mode. */ + val useDefaultVibrate: Boolean, + /** Indicates whether to use the default breathing light. */ + val useDefaultLight: Boolean, + val vibrateConfig: Collection?, + /** Android notification message visibility. */ + @JsonProperty("visibility") + val androidVisibility: AndroidVisibility?, + @JsonProperty("light_settings") + val androidLightSettings: AndroidLightSettings?, + /** + * Indicates whether to display notification messages in the NC when your app is running in the foreground. + * If this parameter is not set, the default value true will be used, + * indicating that notification messages will be displayed in the NC when your app runs in the foreground. + * */ + val foregroundShow: Boolean, + val inboxContent: Collection?, + @JsonProperty("buttons") + val androidButtons: Collection?, + /** ID of the user-app relationship. */ + val profileId: String?, +) { + + class Validator { + + fun check(androidNotificationConfig: AndroidNotificationConfig, notification: Notification?) { + with(androidNotificationConfig) { + androidBadgeNotification?.let { AndroidBadgeNotification.validator.check(it) } + androidLightSettings?.also { AndroidLightSettings.validator.check(it) } + androidClickAction?.also { AndroidClickAction.validator.check(it) } + + require(!notification?.title.isNullOrBlank() || !title.isNullOrBlank()) { "title should be set" } + require(!notification?.body.isNullOrBlank() || !body.isNullOrBlank()) { "body should be set" } + + if (!color.isNullOrBlank()) { + require(color.matches(COLOR_PATTERN)) { "Wrong color format, color must be in the form #RRGGBB" } + } + if (!image.isNullOrBlank()) { + require(image.startsWith(HTTPS_URL_PATTERN)) { "notifyIcon must start with $HTTPS_URL_PATTERN" } + } + if (androidStyleType != null) { + when (androidStyleType) { + AndroidStyleType.DEFAULT -> { + // no verification + } + + AndroidStyleType.BIG_TEXT -> { + require( + !bigTitle.isNullOrBlank() && !bigBody.isNullOrBlank() + ) { "title and body are required when style is $androidStyleType" } + } + + AndroidStyleType.INBOX -> { + require( + !inboxContent.isNullOrEmpty() + ) { "inboxContent is required when style is $androidStyleType" } + require( + inboxContent.size <= INBOX_CONTENT_MAX_ITEMS + ) { "inboxContent must have at most $INBOX_CONTENT_MAX_ITEMS items" } + } + } + } + if (profileId != null) { + require( + profileId.length <= PROFILE_ID_MAX_LENGTH + ) { "profileId length cannot exceed $PROFILE_ID_MAX_LENGTH characters" } + } + } + } + + private companion object { + + val COLOR_PATTERN: Regex = Regex("^#[0-9a-fA-F]{6}$") + const val HTTPS_URL_PATTERN: String = "https" + const val INBOX_CONTENT_MAX_ITEMS: Byte = 5 + const val PROFILE_ID_MAX_LENGTH: Byte = 64 + + } + + } + + class Builder : Buildable { + + private var title: String? = null + private var body: String? = null + private var icon: String? = null + private var color: String? = null + private var sound: String? = null + private var defaultSound = false + private var tag: String? = null + private var bodyLocKey: String? = null + private val bodyLocArgs: MutableList = mutableListOf() + private var titleLocKey: String? = null + private val titleLocArgs: MutableList = mutableListOf() + private var multiLangkey: Map? = null + private var channelId: String? = null + private var notifySummary: String? = null + private var image: String? = null + private var androidStyleType: AndroidStyleType? = null + private var bigTitle: String? = null + private var bigBody: String? = null + private var notifyId: Int? = null + private var group: String? = null + private var androidBadgeNotification: AndroidBadgeNotification? = null + private var autoCancel = true + private var sendAt: String? = null + private var androidImportance: AndroidImportance? = null + private var useDefaultVibrate = false + private var useDefaultLight = false + private val vibrateConfig: MutableList = mutableListOf() + private var androidVisibility: AndroidVisibility? = null + private var androidLightSettings: AndroidLightSettings? = null + private var foregroundShow = false + private val inboxContent: MutableList = mutableListOf() + private val buttons: MutableList = mutableListOf() + private var profileId: String? = null + + fun setTitle(title: String): Builder { + this.title = title + return this + } + + fun setBody(body: String): Builder { + this.body = body + return this + } + + fun setIcon(icon: String): Builder { + this.icon = icon + return this + } + + fun setColor(color: String): Builder { + this.color = color + return this + } + + fun setSound(sound: String): Builder { + this.sound = sound + return this + } + + fun setDefaultSound(defaultSound: Boolean): Builder { + this.defaultSound = defaultSound + return this + } + + fun setTag(tag: String): Builder { + this.tag = tag + return this + } + + fun setBodyLocKey(bodyLocKey: String): Builder { + this.bodyLocKey = bodyLocKey + return this + } + + fun addBodyLocArgs(vararg arg: String): Builder { + bodyLocArgs.addAll(arg) + return this + } + + fun setTitleLocKey(titleLocKey: String): Builder { + this.titleLocKey = titleLocKey + return this + } + + fun addTitleLocArgs(vararg args: String): Builder { + titleLocArgs.addAll(args) + return this + } + + fun setMultiLangkey(multiLangkey: Map): Builder { + this.multiLangkey = multiLangkey + return this + } + + fun setChannelId(channelId: String): Builder { + this.channelId = channelId + return this + } + + fun setNotifySummary(notifySummary: String): Builder { + this.notifySummary = notifySummary + return this + } + + fun setImage(image: String): Builder { + this.image = image + return this + } + + fun setStyle(androidStyleType: AndroidStyleType): Builder { + this.androidStyleType = androidStyleType + return this + } + + fun setBigTitle(bigTitle: String): Builder { + this.bigTitle = bigTitle + return this + } + + fun setBigBody(bigBody: String): Builder { + this.bigBody = bigBody + return this + } + + fun setNotifyId(notifyId: Int): Builder { + this.notifyId = notifyId + return this + } + + fun setGroup(group: String): Builder { + this.group = group + return this + } + + fun setBadge(androidBadgeNotification: AndroidBadgeNotification): Builder { + this.androidBadgeNotification = androidBadgeNotification + return this + } + + fun setAutoCancel(autoCancel: Boolean): Builder { + this.autoCancel = autoCancel + return this + } + + fun sendAt(sendAt: String): Builder { + this.sendAt = sendAt + return this + } + + fun setImportance(androidImportance: AndroidImportance): Builder { + this.androidImportance = androidImportance + return this + } + + fun setUseDefaultVibrate(useDefaultVibrate: Boolean): Builder { + this.useDefaultVibrate = useDefaultVibrate + return this + } + + fun setUseDefaultLight(useDefaultLight: Boolean): Builder { + this.useDefaultLight = useDefaultLight + return this + } + + fun addVibrateConfig(vararg vibrateTimings: String): Builder { + vibrateConfig.addAll(vibrateTimings) + return this + } + + fun setAndroidVisibility(androidVisibility: AndroidVisibility): Builder { + this.androidVisibility = androidVisibility + return this + } + + fun setLightSettings(androidLightSettings: AndroidLightSettings): Builder { + this.androidLightSettings = androidLightSettings + return this + } + + fun setForegroundShow(foregroundShow: Boolean): Builder { + this.foregroundShow = foregroundShow + return this + } + + fun addInboxContent(vararg inboxContent: String): Builder { + this.inboxContent.addAll(inboxContent) + return this + } + + fun addButton(vararg button: AndroidButton): Builder { + buttons.addAll(button) + return this + } + + fun setProfileId(profileId: String): Builder { + this.profileId = profileId + return this + } + + fun build( + androidClickAction: AndroidClickAction, + ): AndroidNotificationConfig { + return AndroidNotificationConfig( + title = title, + body = body, + icon = icon, + color = color, + sound = sound, + defaultSound = defaultSound, + tag = tag, + androidClickAction = androidClickAction, + bodyLocKey = bodyLocKey, + bodyLocArgs = bodyLocArgs.takeIf(Collection<*>::isNotEmpty), + titleLocKey = titleLocKey, + titleLocArgs = titleLocArgs.takeIf(Collection<*>::isNotEmpty), + multiLangKey = multiLangkey, + channelId = channelId, + notifySummary = notifySummary, + image = image, + androidStyleType = androidStyleType, + bigTitle = bigTitle, + bigBody = bigBody, + notifyId = notifyId, + group = group, + androidBadgeNotification = androidBadgeNotification, + autoCancel = autoCancel, + sendAt = sendAt, + androidImportance = androidImportance, + useDefaultVibrate = useDefaultVibrate, + useDefaultLight = useDefaultLight, + vibrateConfig = vibrateConfig.takeIf(Collection<*>::isNotEmpty), + androidVisibility = androidVisibility, + androidLightSettings = androidLightSettings, + foregroundShow = foregroundShow, + inboxContent = inboxContent.takeIf(Collection<*>::isNotEmpty), + androidButtons = buttons.takeIf(Collection<*>::isNotEmpty), + profileId = profileId, + ) + } + + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/apns/ApnsAlert.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/apns/ApnsAlert.kt new file mode 100644 index 0000000..076ff57 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/apns/ApnsAlert.kt @@ -0,0 +1,107 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import ru.touchin.push.message.provider.hpk.base.builders.Buildable + +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy::class) +internal data class ApnsAlert private constructor( + val title: String?, + val body: String?, + val titleLocKey: String?, + val titleLocArgs: Collection?, + val actionLocKey: String?, + val locKey: String?, + val locArgs: Collection?, + val launchImage: String?, +) { + + class Validator { + + fun check(apnsAlert: ApnsAlert) { + with(apnsAlert) { + if (!locArgs.isNullOrEmpty()) { + require(!locKey.isNullOrBlank()) { "locKey is required when specifying locArgs" } + } + if (!titleLocArgs.isNullOrEmpty()) { + require(!titleLocKey.isNullOrBlank()) { "titleLocKey is required when specifying titleLocArgs" } + } + } + } + + } + + class Builder : Buildable { + + private var title: String? = null + private var body: String? = null + private var titleLocKey: String? = null + private val titleLocArgs: MutableList = mutableListOf() + private var actionLocKey: String? = null + private var locKey: String? = null + private val locArgs: MutableList = mutableListOf() + private var launchImage: String? = null + + fun setTitle(title: String): Builder { + this.title = title + return this + } + + fun setBody(body: String): Builder { + this.body = body + return this + } + + fun setTitleLocKey(titleLocKey: String): Builder { + this.titleLocKey = titleLocKey + return this + } + + fun setAddTitleLocArg(vararg titleLocArg: String): Builder { + titleLocArgs.addAll(titleLocArg) + return this + } + + fun setActionLocKey(actionLocKey: String): Builder { + this.actionLocKey = actionLocKey + return this + } + + fun setLocKey(locKey: String): Builder { + this.locKey = locKey + return this + } + + fun addAllLocArgs(vararg locArgs: String): Builder { + this.locArgs.addAll(locArgs) + return this + } + + fun setLaunchImage(launchImage: String): Builder { + this.launchImage = launchImage + return this + } + + fun build(): ApnsAlert { + return ApnsAlert( + title = title, + body = body, + titleLocKey = titleLocKey, + titleLocArgs = titleLocArgs.takeIf(Collection<*>::isNotEmpty), + actionLocKey = actionLocKey, + locKey = locKey, + locArgs = locArgs.takeIf(Collection<*>::isNotEmpty), + launchImage = launchImage, + ) + } + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/apns/ApnsHeaders.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/apns/ApnsHeaders.kt new file mode 100644 index 0000000..6ddb208 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/apns/ApnsHeaders.kt @@ -0,0 +1,109 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import ru.touchin.push.message.provider.hpk.base.builders.Buildable +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.apns.ApnsPriority + +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy::class) +internal data class ApnsHeaders private constructor( + val authorization: String?, + val apnsId: String?, + val apnsExpiration: Long?, + @JsonProperty("apns-priority") + val apnsPriority: ApnsPriority?, + val apnsTopic: String?, + val apnsCollapseId: String?, +) { + + class Validator { + + fun check(apnsHeaders: ApnsHeaders) { + with(apnsHeaders) { + if (authorization != null) { + require( + authorization.startsWith(AUTHORIZATION_PATTERN) + ) { "authorization must start with bearer" } + } + if (apnsId != null) { + require(apnsId.matches(APN_ID_PATTERN)) { "apns-id format error" } + } + if (apnsCollapseId != null) { + require( + apnsCollapseId.toByteArray().size < APNS_COLLAPSE_ID_MAX_SIZE + ) { "Number of apnsCollapseId bytes must be less than $APNS_COLLAPSE_ID_MAX_SIZE" } + } + } + } + + private companion object { + + const val AUTHORIZATION_PATTERN: String = "bearer" + val APN_ID_PATTERN: Regex = Regex("[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}") + const val APNS_COLLAPSE_ID_MAX_SIZE: Byte = 64 + + } + + } + + class Builder : Buildable { + + private var authorization: String? = null + private var apnsId: String? = null + private var apnsExpiration: Long? = null + private var apnsPriority: ApnsPriority? = null + private var apnsTopic: String? = null + private var apnsCollapseId: String? = null + + fun setAuthorization(authorization: String): Builder { + this.authorization = authorization + return this + } + + fun setApnsId(apnsId: String): Builder { + this.apnsId = apnsId + return this + } + + fun setApnsExpiration(apnsExpiration: Long): Builder { + this.apnsExpiration = apnsExpiration + return this + } + + fun setApnsPriority(apnsPriority: ApnsPriority): Builder { + this.apnsPriority = apnsPriority + return this + } + + fun setApnsTopic(apnsTopic: String): Builder { + this.apnsTopic = apnsTopic + return this + } + + fun setApnsCollapseId(apnsCollapseId: String): Builder { + this.apnsCollapseId = apnsCollapseId + return this + } + + fun build(): ApnsHeaders { + return ApnsHeaders( + authorization = authorization, + apnsId = apnsId, + apnsExpiration = apnsExpiration, + apnsPriority = apnsPriority, + apnsTopic = apnsTopic, + apnsCollapseId = apnsCollapseId, + ) + } + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/apns/ApnsHmsOptions.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/apns/ApnsHmsOptions.kt new file mode 100644 index 0000000..36dd182 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/apns/ApnsHmsOptions.kt @@ -0,0 +1,37 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.base.builders.Buildable +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidTargetUserType + +internal data class ApnsHmsOptions private constructor( + @JsonProperty("target_user_type") + val androidTargetUserType: AndroidTargetUserType, +) { + + class Validator { + + fun check(apnsHmsOptions: ApnsHmsOptions) { + // no validation + } + + } + + class Builder : Buildable { + + fun build(androidTargetUserType: AndroidTargetUserType): ApnsHmsOptions { + return ApnsHmsOptions( + androidTargetUserType = androidTargetUserType, + ) + } + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/apns/Aps.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/apns/Aps.kt new file mode 100644 index 0000000..5b0fe94 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/apns/Aps.kt @@ -0,0 +1,89 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.apns + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import ru.touchin.push.message.provider.hpk.base.builders.Buildable + +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy::class) +internal class Aps private constructor( + @JsonProperty("alert") + val alert: ApnsAlert?, + val badge: Int?, + val sound: String?, + val contentAvailable: Int?, + val category: String?, + val threadId: String?, +) { + + class Validator { + + fun check(aps: Aps) { + with(aps) { + if (alert != null) { + ApnsAlert.validator.check(alert) + } + } + } + + } + + class Builder : Buildable { + + private var alert: ApnsAlert? = null + private var badge: Int? = null + private var sound: String? = null + private var contentAvailable: Int? = null + private var category: String? = null + private var threadId: String? = null + + fun setAlert(alert: ApnsAlert): Builder { + this.alert = alert + return this + } + + fun setBadge(badge: Int): Builder { + this.badge = badge + return this + } + + fun setSound(sound: String): Builder { + this.sound = sound + return this + } + + fun setContentAvailable(contentAvailable: Int): Builder { + this.contentAvailable = contentAvailable + return this + } + + fun setCategory(category: String): Builder { + this.category = category + return this + } + + fun setThreadId(threadId: String): Builder { + this.threadId = threadId + return this + } + + fun build(): Aps { + return Aps( + alert = alert, + badge = badge, + sound = sound, + contentAvailable = contentAvailable, + category = category, + threadId = threadId, + ) + } + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/web/WebActions.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/web/WebActions.kt new file mode 100644 index 0000000..5d2d41e --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/web/WebActions.kt @@ -0,0 +1,58 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web + +import ru.touchin.push.message.provider.hpk.base.builders.Buildable + +internal data class WebActions private constructor( + val action: String?, + val icon: String?, + val title: String?, +) { + + class Validator { + + fun check() { + // no validation + } + + } + + class Builder : Buildable { + + private var action: String? = null + private var icon: String? = null + private var title: String? = null + + fun setAction(action: String): Builder { + this.action = action + return this + } + + fun setIcon(icon: String): Builder { + this.icon = icon + return this + } + + fun setTitle(title: String): Builder { + this.title = title + return this + } + + fun build(): WebActions { + return WebActions( + action = action, + icon = icon, + title = title, + ) + } + + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/web/WebHmsOptions.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/web/WebHmsOptions.kt new file mode 100644 index 0000000..e9e652b --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/web/WebHmsOptions.kt @@ -0,0 +1,52 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web + +import ru.touchin.push.message.provider.hpk.base.builders.Buildable +import java.net.MalformedURLException +import java.net.URL + +internal data class WebHmsOptions private constructor( + val link: String?, +) { + + class Validator { + + fun check(webHmsOptions: WebHmsOptions) { + with(webHmsOptions) { + if (!link.isNullOrBlank()) { + try { + URL(link) + } catch (e: MalformedURLException) { + require(false) { "Invalid link" } + } + } + } + } + + } + + class Builder : Buildable { + + private var link: String? = null + + fun setLink(link: String): Builder { + this.link = link + return this + } + + fun build(): WebHmsOptions { + return WebHmsOptions( + link = link, + ) + } + + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/web/WebNotification.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/web/WebNotification.kt new file mode 100644 index 0000000..3689b12 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/web/WebNotification.kt @@ -0,0 +1,150 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.base.builders.Buildable +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.web.WebDir +import java.util.* + +internal data class WebNotification private constructor( + val title: String?, + val body: String?, + val icon: String?, + val image: String?, + val lang: String?, + val tag: String?, + val badge: String?, + @JsonProperty("dir") + val dir: WebDir?, + val vibrate: Collection?, + val renotify: Boolean, + val requireInteraction: Boolean, + val silent: Boolean, + val timestamp: Long?, + @JsonProperty("actions") + val actions: Collection?, +) { + + class Validator { + + fun check(webNotification: WebNotification) { + // no verification required + } + + } + + class Builder : Buildable { + + private var title: String? = null + private var body: String? = null + private var icon: String? = null + private var image: String? = null + private var lang: String? = null + private var tag: String? = null + private var badge: String? = null + private var dir: WebDir? = null + private val vibrate: MutableList = mutableListOf() + private var renotify = false + private var requireInteraction = false + private var silent = false + private var timestamp: Long? = null + private val actions: MutableList = mutableListOf() + + fun setTitle(title: String): Builder { + this.title = title + return this + } + + fun setBody(body: String): Builder { + this.body = body + return this + } + + fun setIcon(icon: String): Builder { + this.icon = icon + return this + } + + fun setImage(image: String): Builder { + this.image = image + return this + } + + fun setLang(lang: String): Builder { + this.lang + return this + } + + fun setTag(tag: String): Builder { + this.tag = tag + return this + } + + fun setBadge(badge: String): Builder { + this.badge = badge + return this + } + + fun setDir(dir: WebDir): Builder { + this.dir = dir + return this + } + + fun addVibrate(vibrate: Int): Builder { + this.vibrate.add(vibrate) + return this + } + + fun setRenotify(renotify: Boolean): Builder { + this.renotify = renotify + return this + } + + fun setRequireInteraction(requireInteraction: Boolean): Builder { + this.requireInteraction = requireInteraction + return this + } + + fun setSilent(silent: Boolean): Builder { + this.silent = silent + return this + } + + fun setTimestamp(timestamp: Long): Builder { + this.timestamp = timestamp + return this + } + + fun addActions(vararg webActions: WebActions): Builder { + this.actions.addAll(webActions) + return this + } + + fun build(): WebNotification { + return WebNotification( + title = title, + body = body, + icon = icon, + image = image, + lang = lang, + tag = tag, + badge = badge, + dir = dir, + vibrate = vibrate.takeIf(Collection<*>::isNotEmpty), + renotify = renotify, + requireInteraction = requireInteraction, + silent = silent, + timestamp = timestamp, + actions = actions.takeIf(Collection<*>::isNotEmpty), + ) + } + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/web/WebPushHeaders.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/web/WebPushHeaders.kt new file mode 100644 index 0000000..7e48d10 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/dto/web/WebPushHeaders.kt @@ -0,0 +1,70 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.web + +import com.fasterxml.jackson.annotation.JsonProperty +import ru.touchin.push.message.provider.hpk.base.builders.Buildable +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.web.WebUrgency + +internal data class WebPushHeaders private constructor( + val ttl: String?, + val topic: String?, + @JsonProperty("urgency") + val urgency: WebUrgency?, +) { + + class Validator { + + fun check(webpushHeaders: WebPushHeaders) { + with(webpushHeaders) { + if (ttl != null) { + require(ttl.matches(TTL_PATTERN)) { "Invalid ttl format" } + } + } + } + + private companion object { + + val TTL_PATTERN: Regex = Regex("[0-9]+|[0-9]+[sS]") + + } + + } + + class Builder : Buildable { + + private var ttl: String? = null + private var topic: String? = null + private var urgency: WebUrgency? = null + + fun setTtl(ttl: String): Builder { + this.ttl = ttl + return this + } + + fun setTopic(topic: String): Builder { + this.topic = topic + return this + } + + fun setUrgency(urgency: WebUrgency): Builder { + this.urgency = urgency + return this + } + + fun build(): WebPushHeaders { + return WebPushHeaders( + ttl = ttl, + topic = topic, + urgency = urgency, + ) + } + } + + companion object { + + val validator = Validator() + + fun builder() = Builder() + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidActionType.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidActionType.kt new file mode 100644 index 0000000..b6acf2d --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidActionType.kt @@ -0,0 +1,17 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +internal enum class AndroidActionType( + override val value: Short +) : ValueableSerializableEnum { + + OPEN_APP_HOME_PAGE(0), + OPEN_CUSTOM_APP_PAGE(1), + OPEN_WEB_PAGE(2), + DELETE_NOTIFICATION_MESSAGE(3), + + /** Only for Huawei devices */ + SHARE_NOTIFICATION_MESSAGE(4), + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidClickActionType.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidClickActionType.kt new file mode 100644 index 0000000..edf1122 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidClickActionType.kt @@ -0,0 +1,13 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +internal enum class AndroidClickActionType( + override val value: Short +) : ValueableSerializableEnum { + + CUSTOMIZE_ACTION(1), + OPEN_URL(2), + OPEN_APP(3), + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidFastAppTargetType.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidFastAppTargetType.kt new file mode 100644 index 0000000..f0e083f --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidFastAppTargetType.kt @@ -0,0 +1,12 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +internal enum class AndroidFastAppTargetType( + override val value: Short +) : ValueableSerializableEnum { + + DEVELOPMENT(1), + PRODUCTION(2), + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidImportance.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidImportance.kt new file mode 100644 index 0000000..0e957df --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidImportance.kt @@ -0,0 +1,13 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +internal enum class AndroidImportance( + override val value: String +) : ValueableSerializableEnum { + + LOW("LOW"), + NORMAL("NORMAL"), + HIGH("HIGH"), // TODO: check if this type is still supported by HMS HPK API + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidIntentType.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidIntentType.kt new file mode 100644 index 0000000..a857f74 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidIntentType.kt @@ -0,0 +1,12 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +enum class AndroidIntentType( + override val value: Short +) : ValueableSerializableEnum { + + INTENT(0), + ACTION(1), + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidStyleType.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidStyleType.kt new file mode 100644 index 0000000..c9c8674 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidStyleType.kt @@ -0,0 +1,13 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +enum class AndroidStyleType( + override val value: Short +) : ValueableSerializableEnum { + + DEFAULT(0), + BIG_TEXT(1), + INBOX(3), + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidTargetUserType.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidTargetUserType.kt new file mode 100644 index 0000000..bdf5253 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidTargetUserType.kt @@ -0,0 +1,13 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +internal enum class AndroidTargetUserType( + override val value: Short +) : ValueableSerializableEnum { + + TEST_USER(1), + FORMAL_USER(2), + VOIP_USER(3), + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidTopicOperation.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidTopicOperation.kt new file mode 100644 index 0000000..2579cf2 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidTopicOperation.kt @@ -0,0 +1,13 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +internal enum class AndroidTopicOperation( + override val value: String +) : ValueableSerializableEnum { + + SUBSCRIBE("subscribe"), + UNSUBSCRIBE("unsubscribe"), + LIST("list"), + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidUrgency.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidUrgency.kt new file mode 100644 index 0000000..c6c0d31 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidUrgency.kt @@ -0,0 +1,12 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +internal enum class AndroidUrgency( + override val value: String +) : ValueableSerializableEnum { + + HIGH("HIGH"), + NORMAL("NORMAL"), + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidVisibility.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidVisibility.kt new file mode 100644 index 0000000..f7c6b32 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/android/AndroidVisibility.kt @@ -0,0 +1,24 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +internal enum class AndroidVisibility( + override val value: String +) : ValueableSerializableEnum { + + /** The visibility is not specified. This value is equivalent to PRIVATE. */ + VISIBILITY_UNSPECIFIED("VISIBILITY_UNSPECIFIED"), + + /** + * If you have set a lock screen password and enabled Hide notification content under Settings > Notifications, + * the content of a received notification message is hidden on the lock screen. + * */ + PRIVATE("PRIVATE"), + + /** The content of a received notification message is displayed on the lock screen. */ + PUBLIC("PUBLIC"), + + /** A received notification message is not displayed on the lock screen. */ + SECRET("SECRET"), + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/apns/ApnsPriority.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/apns/ApnsPriority.kt new file mode 100644 index 0000000..5706fda --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/apns/ApnsPriority.kt @@ -0,0 +1,12 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.apns + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +enum class ApnsPriority( + override val value: Short +) : ValueableSerializableEnum { + + SEND_BY_GROUP(5), + SEND_IMMIDIATELY(10), + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/web/WebDir.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/web/WebDir.kt new file mode 100644 index 0000000..5e35655 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/web/WebDir.kt @@ -0,0 +1,13 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.web + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +enum class WebDir( + override val value: String +) : ValueableSerializableEnum { + + AUTO("auto"), + RTL("rtl"), + LTR("ltr"), + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/web/WebUrgency.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/web/WebUrgency.kt new file mode 100644 index 0000000..7f52989 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/enums/web/WebUrgency.kt @@ -0,0 +1,14 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.web + +import ru.touchin.push.message.provider.hpk.base.enums.ValueableSerializableEnum + +enum class WebUrgency( + override val value: String +) : ValueableSerializableEnum { + + VERY_LOW("very-low"), + LOW("low"), + NORMAL("normal"), + HIGH("high"), + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/requests/HmsHpkMessagesSendRequest.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/requests/HmsHpkMessagesSendRequest.kt new file mode 100644 index 0000000..4499ff8 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/requests/HmsHpkMessagesSendRequest.kt @@ -0,0 +1,10 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.requests + +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.bodies.HmsHpkMessagesSendBody + +internal class HmsHpkMessagesSendRequest( + val hmsHpkMessagesSendBody: HmsHpkMessagesSendBody, + accessToken: String, +) : HmsHpkRequest( + accessToken = accessToken, +) diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/requests/HmsHpkRequest.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/requests/HmsHpkRequest.kt new file mode 100644 index 0000000..90d5ed7 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/requests/HmsHpkRequest.kt @@ -0,0 +1,5 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.requests + +internal open class HmsHpkRequest( + val accessToken: String, +) diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/responses/HmsHpkResponse.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/responses/HmsHpkResponse.kt new file mode 100644 index 0000000..b2903a4 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/responses/HmsHpkResponse.kt @@ -0,0 +1,14 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk.responses + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming + +@JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy::class) +internal class HmsHpkResponse( + /** Result code. */ + val code: String, + /** Result code description. */ + val msg: String, + /** Request ID. */ + val requestId: String, +) diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_oauth/HmsOauthWebClient.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_oauth/HmsOauthWebClient.kt new file mode 100644 index 0000000..ad5f446 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_oauth/HmsOauthWebClient.kt @@ -0,0 +1,78 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_oauth + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.client.WebClient +import ru.touchin.common.spring.web.webclient.dto.RequestLogData +import ru.touchin.common.spring.web.webclient.logger.WebClientLogger +import ru.touchin.push.message.provider.hpk.base.clients.ConfigurableWebClient +import ru.touchin.push.message.provider.hpk.base.clients.dto.ConditionalResponse +import ru.touchin.push.message.provider.hpk.clients.hms_oauth.response.HmsOauthErrorResponse +import ru.touchin.push.message.provider.hpk.clients.hms_oauth.response.HmsOauthTokenResponse +import ru.touchin.push.message.provider.hpk.properties.HpkProperties + +/** + * Client for Huawei Oauth service. + * @see Documentation + */ +@Component +class HmsOauthWebClient( + webClientLogger: WebClientLogger, + @Qualifier("push-message-provider.hpk.hms-oauth-webclient-builder") + webClientBuilder: WebClient.Builder, + private val hpkProperties: HpkProperties, + @Qualifier("push-message-provider.hpk.webclient-objectmapper") + private val objectMapper: ObjectMapper, +) : ConfigurableWebClient(webClientLogger, webClientBuilder, hpkProperties.webServices.oauth) { + + override fun getObjectMapper(): ObjectMapper = objectMapper + + override fun getWebClient(): WebClient { + return getWebClientBuilder( + url = webService.url.toString(), + ) + .setTimeouts() + .build() + } + + internal fun token(): ConditionalResponse { + return getWebClient() + .post() + .uri { builder -> + builder + .path(METHOD_TOKEN) + .build() + } + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body( + BodyInserters + .fromFormData(TOKEN_KEY_GRANT_TYPE, GRANT_TYPE_CLIENT_CREDENTIALS) + .with(TOKEN_KEY_CLIENT_ID, hpkProperties.webServices.clientId) + .with(TOKEN_KEY_CLIENT_SECRET, hpkProperties.webServices.oauth.clientSecret) + ) + .exchangeWithWrap( + requestLogData = RequestLogData( + uri = METHOD_TOKEN, + logTags = listOf(), + method = HttpMethod.POST, + ), + ) + .block() ?: throw IllegalStateException("No response") + } + + + private companion object { + + const val GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials" + const val METHOD_TOKEN = "token" + const val TOKEN_KEY_GRANT_TYPE = "grant_type" + const val TOKEN_KEY_CLIENT_ID = "client_id" + const val TOKEN_KEY_CLIENT_SECRET = "client_secret" + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_oauth/response/HmsOauthErrorResponse.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_oauth/response/HmsOauthErrorResponse.kt new file mode 100644 index 0000000..4bfca26 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_oauth/response/HmsOauthErrorResponse.kt @@ -0,0 +1,7 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_oauth.response + +internal class HmsOauthErrorResponse( + val error: Int, + val subError: Int, + val errorDescription: String, +) diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_oauth/response/HmsOauthTokenResponse.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_oauth/response/HmsOauthTokenResponse.kt new file mode 100644 index 0000000..1ed1b6f --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_oauth/response/HmsOauthTokenResponse.kt @@ -0,0 +1,8 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_oauth.response + +internal class HmsOauthTokenResponse( + val tokenType: String, + val accessToken: String, + /** Expiration in seconds. */ + val expiresIn: Long, +) diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/configurations/PushMessageProviderHpkConfiguration.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/configurations/PushMessageProviderHpkConfiguration.kt new file mode 100644 index 0000000..923f901 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/configurations/PushMessageProviderHpkConfiguration.kt @@ -0,0 +1,62 @@ +package ru.touchin.push.message.provider.hpk.configurations + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.boot.context.properties.ConfigurationPropertiesScan +import org.springframework.cache.CacheManager +import org.springframework.cache.concurrent.ConcurrentMapCache +import org.springframework.cache.support.SimpleCacheManager +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Import +import org.springframework.web.reactive.function.client.WebClient +import ru.touchin.push.message.provider.configurations.PushMessageProviderConfiguration +import ru.touchin.push.message.provider.hpk.services.HmsOauthAccessTokenCacheServiceImpl.Companion.HMS_CLIENT_SERVICE_CACHE_KEY + +@ComponentScan("ru.touchin.push.message.provider.hpk") +@ConfigurationPropertiesScan(basePackages = ["ru.touchin.push.message.provider.hpk"]) +@Import(value = [PushMessageProviderConfiguration::class]) +class PushMessageProviderHpkConfiguration { + + @Bean("push-message-provider.hpk.webclient-objectmapper") + fun webclientObjectMapper(): ObjectMapper { + return jacksonObjectMapper() + .registerModule(JavaTimeModule()) + .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + } + + @Bean("push-message-provider.hpk.client-objectmapper") + fun clientObjectMapper(): ObjectMapper { + return jacksonObjectMapper() + .registerModule(JavaTimeModule()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) + } + + @Bean("push-message-provider.hpk.webclient-cachemanager") + @ConditionalOnMissingBean + fun cacheManager(): CacheManager { + return SimpleCacheManager().also { + it.setCaches( + listOf( + ConcurrentMapCache(HMS_CLIENT_SERVICE_CACHE_KEY) + ) + ) + } + } + + @Bean("push-message-provider.hpk.hms-oauth-webclient-builder") + fun hmsOauthWebClientBuilder(): WebClient.Builder = WebClient.builder() + + @Bean("push-message-provider.hpk.hms-hpk-webclient-builder") + fun hmsHpkWebClientBuilder(): WebClient.Builder = WebClient.builder() + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/converters/PushMessageNotificationConverter.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/converters/PushMessageNotificationConverter.kt new file mode 100644 index 0000000..4d7e8db --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/converters/PushMessageNotificationConverter.kt @@ -0,0 +1,19 @@ +package ru.touchin.push.message.provider.hpk.converters + +import org.springframework.stereotype.Component +import ru.touchin.push.message.provider.dto.PushMessageNotification +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.Notification as HmsNotification +import ru.touchin.push.message.provider.hpk.base.extensions.ifNotNull + +@Component("push-message-provider.hpk.push-message-notification-converter") +class PushMessageNotificationConverter { + + internal operator fun invoke(pushMessageNotification: PushMessageNotification): HmsNotification { + return HmsNotification.builder() + .ifNotNull(pushMessageNotification.imageUrl) { setImage(it) } + .ifNotNull(pushMessageNotification.title) { setTitle(it) } + .ifNotNull(pushMessageNotification.description) { setBody(it) } + .build() + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/converters/PushTokenMessageConverter.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/converters/PushTokenMessageConverter.kt new file mode 100644 index 0000000..04f95aa --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/converters/PushTokenMessageConverter.kt @@ -0,0 +1,56 @@ +package ru.touchin.push.message.provider.hpk.converters + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.stereotype.Component +import ru.touchin.push.message.provider.dto.request.PushTokenMessage +import ru.touchin.push.message.provider.hpk.base.extensions.ifNotNull +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.AndroidConfig +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.Message +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android.AndroidClickAction +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android.AndroidNotificationConfig +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidClickActionType +import kotlin.jvm.Throws + +@Component("push-message-provider.hpk.push-token-message-converter") +class PushTokenMessageConverter( + private val pushMessageNotificationConverter: PushMessageNotificationConverter, + @Qualifier("push-message-provider.hpk.client-objectmapper") + private val objectMapper: ObjectMapper, +) { + + @Throws(IllegalArgumentException::class) + internal operator fun invoke(request: PushTokenMessage): Message { + return Message.builder() + .addToken(request.token) + .ifNotNull(request.data.takeIf(Map<*, *>::isNotEmpty)) { data -> + setData(objectMapper.writeValueAsString(data)) + } + .ifNotNull(request.pushMessageNotification) { notification -> + setNotification(pushMessageNotificationConverter(notification)) + } + .setupAndroidConfig() + .build() + .also { Message.validator.check(it) } + } + + private fun Message.Builder.setupAndroidConfig(): Message.Builder { + return setAndroidConfig( + AndroidConfig.builder() + .setAndroidNotificationConfig( + AndroidNotificationConfig.builder() + .setDefaultSound(USE_DEFAULT_SOUND) + .build(AndroidClickAction.builder().build(DEFAULT_ANDROID_CLICK_ACTION_TYPE)) + ) + .build() + ) + } + + private companion object { + + const val USE_DEFAULT_SOUND = true + val DEFAULT_ANDROID_CLICK_ACTION_TYPE = AndroidClickActionType.OPEN_APP + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/properties/HpkProperties.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/properties/HpkProperties.kt new file mode 100644 index 0000000..3dd6d33 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/properties/HpkProperties.kt @@ -0,0 +1,59 @@ +package ru.touchin.push.message.provider.hpk.properties + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.ConstructorBinding +import java.net.URL +import java.time.Duration + +@ConstructorBinding +@ConfigurationProperties(prefix = "push-message-provider.hpk") +data class HpkProperties( + val webServices: WebServices, +) { + + class WebServices( + val clientId: String, + val oauth: Oauth, + val hpk: Hpk, + ) + + class Oauth( + val clientSecret: String, + url: URL, + http: Http, + ssl: Ssl?, + ) : WebService( + url = url, + http = http, + ssl = ssl, + ) + + class Hpk( + url: URL, + http: Http, + ssl: Ssl?, + ) : WebService( + url = url, + http = http, + ssl = ssl, + ) + + open class WebService( + val url: URL, + val http: Http, + val ssl: Ssl?, + ) + + class Http( + val readTimeout: Duration, + val writeTimeout: Duration, + val connectionTimeout: Duration, + ) + + class Ssl( + val handshakeTimeout: Duration, + val notifyFlushTimeout: Duration, + val notifyReadTimeout: Duration, + ) + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsHpkClientService.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsHpkClientService.kt new file mode 100644 index 0000000..027bcc2 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsHpkClientService.kt @@ -0,0 +1,13 @@ +package ru.touchin.push.message.provider.hpk.services + +import ru.touchin.push.message.provider.dto.request.PushTokenCheck +import ru.touchin.push.message.provider.dto.request.PushTokenMessage +import ru.touchin.push.message.provider.enums.PushTokenStatus + +interface HmsHpkClientService { + + fun send(request: PushTokenMessage) + + fun check(request: PushTokenCheck): PushTokenStatus + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsHpkClientServiceImpl.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsHpkClientServiceImpl.kt new file mode 100644 index 0000000..002f0f8 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsHpkClientServiceImpl.kt @@ -0,0 +1,74 @@ +package ru.touchin.push.message.provider.hpk.services + +import org.springframework.stereotype.Service +import ru.touchin.push.message.provider.dto.request.PushTokenCheck +import ru.touchin.push.message.provider.dto.request.PushTokenMessage +import ru.touchin.push.message.provider.enums.PushTokenStatus +import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException +import ru.touchin.push.message.provider.exceptions.PushMessageProviderException +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.HmsHpkWebClient +import ru.touchin.push.message.provider.hpk.clients.hms.enums.HmsResponseCode +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.bodies.HmsHpkMessagesSendBody +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.requests.HmsHpkMessagesSendRequest +import ru.touchin.push.message.provider.hpk.converters.PushTokenMessageConverter + +@Service +class HmsHpkClientServiceImpl( + private val hmsHpkWebClient: HmsHpkWebClient, + private val hmsOauthClientService: HmsOauthClientService, + private val pushTokenMessageConverter: PushTokenMessageConverter, +) : HmsHpkClientService { + + override fun send(request: PushTokenMessage) { + sendToPushToken(request, dryRun = false) + } + + override fun check(request: PushTokenCheck): PushTokenStatus { + val validationRequest = PushTokenMessage( + token = request.pushToken, + pushMessageNotification = null, + data = emptyMap() + ) + + return try { + sendToPushToken(validationRequest, dryRun = false) + + PushTokenStatus.VALID + } catch (ipte: InvalidPushTokenException) { + PushTokenStatus.INVALID + } catch (pmpe: PushMessageProviderException) { + PushTokenStatus.UNKNOWN + } + } + + @Throws(PushMessageProviderException::class, InvalidPushTokenException::class) + private fun sendToPushToken(request: PushTokenMessage, dryRun: Boolean) { + val accessToken = hmsOauthClientService.getAccessToken() + + val result = hmsHpkWebClient.messagesSend( + HmsHpkMessagesSendRequest( + hmsHpkMessagesSendBody = HmsHpkMessagesSendBody( + validateOnly = dryRun, + message = pushTokenMessageConverter(request), + ), + accessToken = accessToken, + ) + ) + + when (HmsResponseCode.fromCode(result.code)) { + HmsResponseCode.SUCCESS -> { + // pass result + } + + HmsResponseCode.INVALID_TOKEN, + HmsResponseCode.INVALID_CLIENT_SECRET -> { + throw InvalidPushTokenException() + } + + else -> { + throw PushMessageProviderException(result.msg, null) + } + } + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthAccessTokenCacheService.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthAccessTokenCacheService.kt new file mode 100644 index 0000000..4aab4c0 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthAccessTokenCacheService.kt @@ -0,0 +1,11 @@ +package ru.touchin.push.message.provider.hpk.services + +import ru.touchin.push.message.provider.hpk.services.dto.AccessToken + +interface HmsOauthAccessTokenCacheService { + + fun put(accessToken: AccessToken) + fun get(): AccessToken? + fun evict() + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthAccessTokenCacheServiceImpl.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthAccessTokenCacheServiceImpl.kt new file mode 100644 index 0000000..38d7814 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthAccessTokenCacheServiceImpl.kt @@ -0,0 +1,81 @@ +package ru.touchin.push.message.provider.hpk.services + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.cache.Cache +import org.springframework.cache.CacheManager +import org.springframework.stereotype.Service +import ru.touchin.logger.dto.LogData +import ru.touchin.logger.factory.LogBuilderFactory +import ru.touchin.push.message.provider.hpk.properties.HpkProperties +import ru.touchin.push.message.provider.hpk.services.dto.AccessToken +import java.time.Instant + +@Service +class HmsOauthAccessTokenCacheServiceImpl( + private val logBuilderFactory: LogBuilderFactory, + @Qualifier("push-message-provider.hpk.webclient-cachemanager") + private val cacheManager: CacheManager, + @Qualifier("push-message-provider.hpk.client-objectmapper") + private val objectMapper: ObjectMapper, + private val hpkProperties: HpkProperties, +) : HmsOauthAccessTokenCacheService { + + override fun put(accessToken: AccessToken) { + getCache()?.put(hpkProperties.webServices.clientId, accessToken) + } + + override fun get(): AccessToken? { // TODO: implement synchronization for all threads + val cachedValue = getCache() + ?.get(hpkProperties.webServices.clientId) + ?.get() + ?: return null + + val accessToken = safeCast(cachedValue, object : TypeReference() {}) + ?: return null + + return if (accessToken.isValid()) { + accessToken + } else { + null + } + } + + override fun evict() { + getCache()?.evict(hpkProperties.webServices.clientId) + } + + private fun safeCast(item: Any, typeReference: TypeReference): T? { + return try { + objectMapper.convertValue(item, typeReference) + } catch (e: Exception) { + logBuilderFactory.create(this::class.java) + .setMethod("safeCast") + .setError(e) + .build() + .error() + + null + } + } + + private fun AccessToken.isValid(): Boolean { + val expirationTime = with(hpkProperties.webServices.oauth) { + Instant.now().plus(http.connectionTimeout + http.readTimeout + http.writeTimeout) + } + + return expiresAt.isAfter(expirationTime) + } + + private fun getCache(): Cache? { + return cacheManager.getCache(HMS_CLIENT_SERVICE_CACHE_KEY) + } + + companion object { + + const val HMS_CLIENT_SERVICE_CACHE_KEY = "HMS_CLIENT_SERVICE" + + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthClientService.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthClientService.kt new file mode 100644 index 0000000..bc8ca65 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthClientService.kt @@ -0,0 +1,7 @@ +package ru.touchin.push.message.provider.hpk.services + +interface HmsOauthClientService { + + fun getAccessToken(): String + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthClientServiceImpl.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthClientServiceImpl.kt new file mode 100644 index 0000000..8b38b6c --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthClientServiceImpl.kt @@ -0,0 +1,37 @@ +package ru.touchin.push.message.provider.hpk.services + +import org.springframework.stereotype.Service +import ru.touchin.push.message.provider.exceptions.PushMessageProviderException +import ru.touchin.push.message.provider.hpk.clients.hms_oauth.HmsOauthWebClient +import ru.touchin.push.message.provider.hpk.services.dto.AccessToken +import java.time.Instant + +@Service +class HmsOauthClientServiceImpl( + private val hmsOauthAccessTokenCacheService: HmsOauthAccessTokenCacheService, + private val hmsOauthWebClient: HmsOauthWebClient, +) : HmsOauthClientService { + + override fun getAccessToken(): String { + val accessToken = hmsOauthAccessTokenCacheService.get() + ?: retrieveAccessToken().also(hmsOauthAccessTokenCacheService::put) + + return accessToken.value + } + + private fun retrieveAccessToken(): AccessToken { + val result = hmsOauthWebClient.token() + + if (result.success == null) { + throw PushMessageProviderException(result.failure?.errorDescription.orEmpty(), null) + } + + return with(result.success) { + AccessToken( + value = accessToken, + expiresAt = Instant.now().plusSeconds(expiresIn) + ) + } + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/PushMessageProviderHpkService.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/PushMessageProviderHpkService.kt new file mode 100644 index 0000000..dc1926c --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/PushMessageProviderHpkService.kt @@ -0,0 +1,37 @@ +package ru.touchin.push.message.provider.hpk.services + +import org.springframework.stereotype.Service +import ru.touchin.push.message.provider.dto.request.PushTokenCheck +import ru.touchin.push.message.provider.dto.request.PushTokenMessage +import ru.touchin.push.message.provider.dto.request.SendPushRequest +import ru.touchin.push.message.provider.dto.result.CheckPushTokenResult +import ru.touchin.push.message.provider.dto.result.SendPushResult +import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageResult +import ru.touchin.push.message.provider.enums.PushMessageProviderType +import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException +import ru.touchin.push.message.provider.exceptions.PushMessageProviderException +import ru.touchin.push.message.provider.services.PushMessageProviderService + +@Service +class PushMessageProviderHpkService( + private val hmsHpkClientService: HmsHpkClientService, +) : PushMessageProviderService { + + override val type: PushMessageProviderType = PushMessageProviderType.HPK + + @Throws(PushMessageProviderException::class, InvalidPushTokenException::class) + override fun send(request: SendPushRequest): SendPushResult { + when (request) { + is PushTokenMessage -> hmsHpkClientService.send(request) + } + + return SendPushTokenMessageResult + } + + override fun check(request: PushTokenCheck): CheckPushTokenResult { + val result = hmsHpkClientService.check(request) + + return CheckPushTokenResult(result) + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/dto/AccessToken.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/dto/AccessToken.kt new file mode 100644 index 0000000..2966ad4 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/services/dto/AccessToken.kt @@ -0,0 +1,8 @@ +package ru.touchin.push.message.provider.hpk.services.dto + +import java.time.Instant + +data class AccessToken( + val value: String, + val expiresAt: Instant, +) diff --git a/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/PushMessageProviderHpkTestApplication.kt b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/PushMessageProviderHpkTestApplication.kt new file mode 100644 index 0000000..4af6a89 --- /dev/null +++ b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/PushMessageProviderHpkTestApplication.kt @@ -0,0 +1,16 @@ +package ru.touchin.push.message.provider.hpk + +import org.springframework.boot.SpringBootConfiguration +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Import +import ru.touchin.logger.spring.configurations.SpringLoggerConfiguration +import ru.touchin.logger.spring.web.configurations.SpringLoggerWebConfiguration + +@TestConfiguration +@SpringBootConfiguration +@EnablePushMessageProviderHpk +@Import( + SpringLoggerConfiguration::class, + SpringLoggerWebConfiguration::class, +) +class PushMessageProviderHpkTestApplication diff --git a/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/base/builders/BuildableTest.kt b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/base/builders/BuildableTest.kt new file mode 100644 index 0000000..bba59e8 --- /dev/null +++ b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/base/builders/BuildableTest.kt @@ -0,0 +1,52 @@ +package ru.touchin.push.message.provider.hpk.base.builders + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import ru.touchin.push.message.provider.hpk.base.extensions.ifNotNull + +class BuildableTest { + + @Test + fun ifNotNull_setOnNotNull() { + val data = Data(property = true) + val builder = DataBuilder() + + builder.ifNotNull(data.property) { setProperty(it) } + + Assertions.assertNotNull( + builder.property + ) + } + + @Test + fun ifNotNull_notSetOnNull() { + val data = Data(property = null) + val builder = DataBuilder() + + builder.ifNotNull(data.property) { setProperty(it) } + + Assertions.assertNull( + builder.property + ) + } + + private class Data(val property: Boolean?) + + private class DataBuilder : Buildable { + + var property: Boolean? = null + + fun setProperty(property: Boolean): DataBuilder { + this.property = property + return this + } + + fun build(): Data { + return Data( + property = property, + ) + } + + } + +} diff --git a/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/base/enums/ValueableSerializableEnumTest.kt b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/base/enums/ValueableSerializableEnumTest.kt new file mode 100644 index 0000000..cb2dc60 --- /dev/null +++ b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/base/enums/ValueableSerializableEnumTest.kt @@ -0,0 +1,33 @@ +package ru.touchin.push.message.provider.hpk.base.enums + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class ValueableSerializableEnumTest { + + private val objectMapper = jacksonObjectMapper() + + @Test + fun toValue_correctJacksonSerialization() { + val enum = TestEnum.VALUE1 + + val expected = enum.toValue() + + val actual = objectMapper.writeValueAsString(enum) + .let(objectMapper::readTree) + .textValue() + + Assertions.assertEquals( + expected, + actual + ) + } + + private enum class TestEnum(override val value: String) : ValueableSerializableEnum { + + VALUE1("testValue1"), + + } + +} diff --git a/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/HmsHpkWebClientTest.kt b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/HmsHpkWebClientTest.kt new file mode 100644 index 0000000..c91bb25 --- /dev/null +++ b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_hpk/HmsHpkWebClientTest.kt @@ -0,0 +1,69 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_hpk + +import com.fasterxml.jackson.core.JsonParseException +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.bodies.HmsHpkMessagesSendBody +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.AndroidConfig +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.Message +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.Notification +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android.AndroidClickAction +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android.AndroidNotificationConfig +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidClickActionType +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidUrgency +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.requests.HmsHpkMessagesSendRequest + +@SpringBootTest +class HmsHpkWebClientTest { + + @Autowired + lateinit var hmsHpkWebClient: HmsHpkWebClient + + @Test + fun messagesSend_returnsHtmlDocumentStringWith403CodeAtIncorrectAccessToken() { + val result = runCatching { + hmsHpkWebClient.messagesSend( + HmsHpkMessagesSendRequest( + hmsHpkMessagesSendBody = buildHmsHpkMessagesSendBody( + token = "pushTokenWithLongLength" + ), + accessToken = "testAccessToken" + ) + ) + } + + Assertions.assertEquals( + JsonParseException::class.java, + result.exceptionOrNull()?.cause?.let { it::class.java } + ) + } + + private fun buildHmsHpkMessagesSendBody(token: String): HmsHpkMessagesSendBody { + return HmsHpkMessagesSendBody( + validateOnly = true, + message = Message.builder() + .addToken(token) + .setNotification( + Notification.builder() + .setTitle("title") + .setBody("body") + .setImage("https://avatars.githubusercontent.com/u/1435794?s=200&v=4") + .build() + ) + .setAndroidConfig( + AndroidConfig.builder() + .setUrgency(AndroidUrgency.HIGH) + .setAndroidNotificationConfig( + AndroidNotificationConfig.builder() + .setDefaultSound(true) + .build(AndroidClickAction.builder().build(AndroidClickActionType.OPEN_APP)) + ) + .build() + ) + .build() + ) + } + +} diff --git a/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_oauth/HmsOauthWebClientTest.kt b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_oauth/HmsOauthWebClientTest.kt new file mode 100644 index 0000000..8bc44a2 --- /dev/null +++ b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/clients/hms_oauth/HmsOauthWebClientTest.kt @@ -0,0 +1,27 @@ +package ru.touchin.push.message.provider.hpk.clients.hms_oauth + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import ru.touchin.push.message.provider.hpk.clients.hms.enums.HmsResponseCode + +@SpringBootTest +class HmsOauthWebClientTest { + + @Autowired + lateinit var hmsOauthWebClient: HmsOauthWebClient + + @Test + fun token_invalidClientSecretOnInvalidClientSecret() { + val result = hmsOauthWebClient.token() + + Assertions.assertNotNull(result.failure) + + Assertions.assertEquals( + result.failure?.error, + HmsResponseCode.INVALID_CLIENT_SECRET.value + ) + } + +} diff --git a/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/converters/PushTokenMessageConverterTest.kt b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/converters/PushTokenMessageConverterTest.kt new file mode 100644 index 0000000..ff05703 --- /dev/null +++ b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/converters/PushTokenMessageConverterTest.kt @@ -0,0 +1,84 @@ +package ru.touchin.push.message.provider.hpk.converters + +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import ru.touchin.push.message.provider.dto.PushMessageNotification +import ru.touchin.push.message.provider.dto.request.PushTokenMessage +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.Message +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.Notification as HmsNotification +import org.junit.jupiter.api.Assertions +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.AndroidConfig +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android.AndroidClickAction +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.dto.android.AndroidNotificationConfig +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.enums.android.AndroidClickActionType + +@SpringBootTest +class PushTokenMessageConverterTest { + + @Autowired + lateinit var pushTokenMessageConverter: PushTokenMessageConverter + + @Test + fun invoke_buildsComplexMessage() { + val request = PushTokenMessage( + token = "testToken", + pushMessageNotification = PushMessageNotification( + title = "title", + description = "description", + imageUrl = "https://avatars.githubusercontent.com/u/1435794?s=200&v=4" + ), + data = mapOf( + "key1" to "value1", + ) + ) + + val actualResult = pushTokenMessageConverter(request) + + val expectedResult = Message.builder() + .addToken("testToken") + .setNotification( + HmsNotification.builder() + .setTitle("title") + .setBody("description") + .setImage("https://avatars.githubusercontent.com/u/1435794?s=200&v=4") + .build() + ) + .setAndroidConfig( + AndroidConfig.builder() + .setAndroidNotificationConfig( + AndroidNotificationConfig.builder() + .setDefaultSound(true) + .build(AndroidClickAction.builder().build(AndroidClickActionType.OPEN_APP)) + ) + .build() + ) + .setData("{\"key1\":\"value1\"}") + .build() + + Assertions.assertEquals( + expectedResult, + actualResult + ) + } + + @Test + fun invoke_throwsValidationErrorAtHttpImageUrl() { + val request = PushTokenMessage( + token = "testToken", + pushMessageNotification = PushMessageNotification( + title = "title", + description = "description", + imageUrl = "http://avatars.githubusercontent.com/u/1435794?s=200&v=4" + ), + data = mapOf( + "key1" to "value1", + ) + ) + + Assertions.assertThrows( + IllegalArgumentException::class.java + ) { pushTokenMessageConverter(request) } + } + +} diff --git a/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/services/HmsHpkClientServiceTest.kt b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/services/HmsHpkClientServiceTest.kt new file mode 100644 index 0000000..9067f84 --- /dev/null +++ b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/services/HmsHpkClientServiceTest.kt @@ -0,0 +1,123 @@ +package ru.touchin.push.message.provider.hpk.services + +import com.nhaarman.mockitokotlin2.any +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.mock.mockito.MockBean +import ru.touchin.push.message.provider.dto.PushMessageNotification +import ru.touchin.push.message.provider.dto.request.PushTokenMessage +import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException +import ru.touchin.push.message.provider.exceptions.PushMessageProviderException +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.HmsHpkWebClient +import ru.touchin.push.message.provider.hpk.clients.hms.enums.HmsResponseCode +import ru.touchin.push.message.provider.hpk.clients.hms_hpk.responses.HmsHpkResponse + +@SpringBootTest +class HmsHpkClientServiceTest { + + @MockBean + lateinit var hmsHpkWebClient: HmsHpkWebClient + + @MockBean + lateinit var hmsOauthClientService: HmsOauthClientService + + @Autowired + lateinit var hmsHpkClientService: HmsHpkClientService + + @Test + fun send_throwsInvalidPushTokenExceptionForKnownErrors() { + Mockito.`when`( + hmsOauthClientService.getAccessToken() + ).then { "accessToken" } + + Mockito.`when`( + hmsHpkWebClient.messagesSend(any()) + ).then { + HmsHpkResponse( + code = HmsResponseCode.INVALID_TOKEN.value.toString(), + msg = "0", + requestId = "requestId" + ) + } + + val pushTokenMessage = PushTokenMessage( + token = "token", + pushMessageNotification = PushMessageNotification( + title = "title", + description = "description", + imageUrl = null + ), + data = emptyMap() + ) + + Assertions.assertThrows( + InvalidPushTokenException::class.java + ) { hmsHpkClientService.send(pushTokenMessage) } + } + + @Test + fun send_throwsPushMessageProviderExceptionOnOtherExceptions() { + Mockito.`when`( + hmsOauthClientService.getAccessToken() + ).then { "accessToken" } + + Mockito.`when`( + hmsHpkWebClient.messagesSend(any()) + ).then { + HmsHpkResponse( + code = HmsResponseCode.OAUTH_TOKEN_EXPIRED.value.toString(), + msg = "0", + requestId = "requestId" + ) + } + + val pushTokenMessage = PushTokenMessage( + token = "token", + pushMessageNotification = PushMessageNotification( + title = "title", + description = "description", + imageUrl = null + ), + data = emptyMap() + ) + + Assertions.assertThrows( + PushMessageProviderException::class.java + ) { hmsHpkClientService.send(pushTokenMessage) } + } + + @Test + fun send_passesSuccess() { + Mockito.`when`( + hmsOauthClientService.getAccessToken() + ).then { "accessToken" } + + Mockito.`when`( + hmsHpkWebClient.messagesSend(any()) + ).then { + HmsHpkResponse( + code = HmsResponseCode.SUCCESS.value.toString(), + msg = "0", + requestId = "requestId" + ) + } + + val pushTokenMessage = PushTokenMessage( + token = "token", + pushMessageNotification = PushMessageNotification( + title = "title", + description = "description", + imageUrl = null + ), + data = emptyMap() + ) + + Assertions.assertDoesNotThrow { + hmsHpkClientService.send(pushTokenMessage) + } + } + +} diff --git a/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthAccessTokenCacheServiceTest.kt b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthAccessTokenCacheServiceTest.kt new file mode 100644 index 0000000..cd62b9d --- /dev/null +++ b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthAccessTokenCacheServiceTest.kt @@ -0,0 +1,88 @@ +package ru.touchin.push.message.provider.hpk.services + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import ru.touchin.push.message.provider.hpk.services.dto.AccessToken +import java.time.Instant + +@SpringBootTest +class HmsOauthAccessTokenCacheServiceTest { + + @Autowired + lateinit var hmsOauthAccessTokenCacheService: HmsOauthAccessTokenCacheService + + private val validAccessToken: AccessToken + get() = AccessToken( + value = "token", + expiresAt = Instant.now().plusSeconds(600) + ) + + private val expiredAccessToken: AccessToken + get() = AccessToken( + value = "token", + expiresAt = Instant.now().minusSeconds(600) + ) + + @Test + fun get_noCacheReturnsNull() { + hmsOauthAccessTokenCacheService.evict() + + val accessToken = hmsOauthAccessTokenCacheService.get() + + Assertions.assertNull(accessToken) + } + + @Test + fun get_validIsReturned() { + val expected = validAccessToken + + hmsOauthAccessTokenCacheService.put(expected) + + val actual = hmsOauthAccessTokenCacheService.get() + + Assertions.assertEquals( + expected, + actual + ) + } + + @Test + fun get_expiredIsNotReturned() { + hmsOauthAccessTokenCacheService.put(expiredAccessToken) + + val accessToken = hmsOauthAccessTokenCacheService.get() + + Assertions.assertNull(accessToken) + } + + @Test + fun put_valid() { + hmsOauthAccessTokenCacheService.put(validAccessToken) + + val result = hmsOauthAccessTokenCacheService.get() + + Assertions.assertNotNull(result) + } + + @Test + fun put_expired() { + hmsOauthAccessTokenCacheService.put(expiredAccessToken) + + val result = hmsOauthAccessTokenCacheService.get() + + Assertions.assertNull(result) + } + + @Test + fun evict_deletesCache() { + hmsOauthAccessTokenCacheService.put(validAccessToken) + hmsOauthAccessTokenCacheService.evict() + + val result = hmsOauthAccessTokenCacheService.get() + + Assertions.assertNull(result) + } + +} diff --git a/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthClientServiceTest.kt b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthClientServiceTest.kt new file mode 100644 index 0000000..fd1f7ba --- /dev/null +++ b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/services/HmsOauthClientServiceTest.kt @@ -0,0 +1,101 @@ +package ru.touchin.push.message.provider.hpk.services + +import com.fasterxml.jackson.databind.JsonMappingException +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.mock.mockito.MockBean +import ru.touchin.push.message.provider.exceptions.InvalidPushTokenException +import ru.touchin.push.message.provider.exceptions.PushMessageProviderException +import ru.touchin.push.message.provider.hpk.base.clients.dto.ConditionalResponse +import ru.touchin.push.message.provider.hpk.clients.hms.enums.HmsResponseCode +import ru.touchin.push.message.provider.hpk.clients.hms_oauth.HmsOauthWebClient +import ru.touchin.push.message.provider.hpk.clients.hms_oauth.response.HmsOauthErrorResponse +import ru.touchin.push.message.provider.hpk.clients.hms_oauth.response.HmsOauthTokenResponse +import java.net.SocketTimeoutException + +@SpringBootTest +class HmsOauthClientServiceTest { + + @MockBean + lateinit var hmsOauthWebClient: HmsOauthWebClient + + @Autowired + lateinit var hmsOauthClientService: HmsOauthClientService + + @Test + fun getAccessToken_throwsPushMessageProviderExceptionForUnknownError() { + Mockito.`when`( + hmsOauthWebClient.token() + ).then { + ConditionalResponse( + success = null, + failure = HmsOauthErrorResponse( + error = HmsResponseCode.UNKNOWN.value, + subError = 0, + errorDescription = "errorDescription" + ) + ) + } + + Assertions.assertThrows( + PushMessageProviderException::class.java + ) { hmsOauthClientService.getAccessToken() } + } + + @Test + fun getAccessToken_throwsNetworkExceptions() { + Mockito.`when`( + hmsOauthWebClient.token() + ).then { + throw SocketTimeoutException() + } + + Assertions.assertThrows( + SocketTimeoutException::class.java + ) { hmsOauthClientService.getAccessToken() } + } + + + @Test + fun getAccessToken_throwsParsingExceptions() { + Mockito.`when`( + hmsOauthWebClient.token() + ).then { + throw JsonMappingException({ }, "jsonMappingExceptionMsg") + } + + Assertions.assertThrows( + JsonMappingException::class.java + ) { hmsOauthClientService.getAccessToken() } + } + + @Test + fun getAccessToken_cachesExpectedTime() { + val expected = "accessToken" + + Mockito.`when`( + hmsOauthWebClient.token() + ).then { + ConditionalResponse( + success = HmsOauthTokenResponse( + tokenType = "tokenType", + expiresIn = 60_000, + accessToken = expected + ), + failure = null + ) + } + + val actual = hmsOauthClientService.getAccessToken() + + Assertions.assertEquals( + expected, + actual + ) + } + + +} diff --git a/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/services/PushMessageProviderHpkServiceTest.kt b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/services/PushMessageProviderHpkServiceTest.kt new file mode 100644 index 0000000..9a79d75 --- /dev/null +++ b/push-message-provider-hpk/src/test/kotlin/ru/touchin/push/message/provider/hpk/services/PushMessageProviderHpkServiceTest.kt @@ -0,0 +1,60 @@ +package ru.touchin.push.message.provider.hpk.services + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.only +import com.nhaarman.mockitokotlin2.verify +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.mock.mockito.MockBean +import ru.touchin.push.message.provider.dto.request.PushTokenCheck +import ru.touchin.push.message.provider.dto.request.PushTokenMessage +import ru.touchin.push.message.provider.enums.PushTokenStatus +import ru.touchin.push.message.provider.services.PushMessageProviderService + +@SpringBootTest +class PushMessageProviderHpkServiceTest { + + @MockBean + lateinit var hmsHpkClientService: HmsHpkClientService + + @Autowired + lateinit var pushMessageProviderService: PushMessageProviderService + + @Test + @DisplayName("Обработка запроса на отправку единичного сообщения происходит корректно") + fun send_basic() { + Mockito.`when`( + hmsHpkClientService.send(any()) + ).then { + // returns Unit + } + + val request = PushTokenMessage( + token = "testTokenWithLongLength", + pushMessageNotification = null, + data = emptyMap() + ) + + pushMessageProviderService.send(request) + + verify(hmsHpkClientService, only()).send(any()) + } + + @Test + @DisplayName("Обработка запроса на валидацию пуш-токена происходит корректно") + fun check_basic() { + Mockito.`when`( + hmsHpkClientService.check(any()) + ).then { + PushTokenStatus.UNKNOWN + } + + pushMessageProviderService.check(PushTokenCheck("testTokenWithLongLength")) + + verify(hmsHpkClientService, only()).check(any()) + } + +} diff --git a/push-message-provider-hpk/src/test/resources/application.yml b/push-message-provider-hpk/src/test/resources/application.yml new file mode 100644 index 0000000..a0492fb --- /dev/null +++ b/push-message-provider-hpk/src/test/resources/application.yml @@ -0,0 +1,20 @@ +push-message-provider: + platformProviders: + ANDROID_HUAWEI: + - HPK + hpk: + web-services: + client-id: testClientId + oauth: + client-secret: testClientSecret + url: https://oauth-login.cloud.huawei.com/oauth2/v3/ + http: + connection-timeout: 5s + read-timeout: 10s + write-timeout: 10s + hpk: + url: https://push-api.cloud.huawei.com/v1/ + http: + connection-timeout: 5s + read-timeout: 10s + write-timeout: 10s diff --git a/push-message-provider-mock/src/main/kotlin/ru/touchin/push/message/provider/mock/factories/PushMessageProviderMockServiceFactoryImpl.kt b/push-message-provider-mock/src/main/kotlin/ru/touchin/push/message/provider/mock/factories/PushMessageProviderMockServiceFactoryImpl.kt index 291f613..2c2abfd 100644 --- a/push-message-provider-mock/src/main/kotlin/ru/touchin/push/message/provider/mock/factories/PushMessageProviderMockServiceFactoryImpl.kt +++ b/push-message-provider-mock/src/main/kotlin/ru/touchin/push/message/provider/mock/factories/PushMessageProviderMockServiceFactoryImpl.kt @@ -9,7 +9,7 @@ import ru.touchin.push.message.provider.dto.request.PushTokenCheck import ru.touchin.push.message.provider.dto.request.SendPushRequest import ru.touchin.push.message.provider.dto.result.CheckPushTokenResult import ru.touchin.push.message.provider.dto.result.SendPushResult -import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageResult +import ru.touchin.push.message.provider.dto.result.SendPushTokenMessageTraceableResult import ru.touchin.push.message.provider.enums.PlatformType import ru.touchin.push.message.provider.enums.PushMessageProviderType import ru.touchin.push.message.provider.factories.PushMessageProviderServiceFactory @@ -42,7 +42,7 @@ class PushMessageProviderMockServiceFactoryImpl( Thread.sleep(millis) - return SendPushTokenMessageResult( + return SendPushTokenMessageTraceableResult( messageId = UUID.randomUUID().toString(), ) } diff --git a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/Notification.kt b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/PushMessageNotification.kt similarity index 80% rename from push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/Notification.kt rename to push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/PushMessageNotification.kt index 0b10a0a..8605533 100644 --- a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/Notification.kt +++ b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/PushMessageNotification.kt @@ -1,6 +1,6 @@ package ru.touchin.push.message.provider.dto -class Notification( +class PushMessageNotification( val title: String?, val description: String?, val imageUrl: String? diff --git a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/request/PushTokenMessage.kt b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/request/PushTokenMessage.kt index 56b73a8..84f355e 100644 --- a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/request/PushTokenMessage.kt +++ b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/request/PushTokenMessage.kt @@ -1,9 +1,9 @@ package ru.touchin.push.message.provider.dto.request -import ru.touchin.push.message.provider.dto.Notification +import ru.touchin.push.message.provider.dto.PushMessageNotification class PushTokenMessage( val token: String, - override val notification: Notification?, + override val pushMessageNotification: PushMessageNotification?, override val data: Map ) : SendPushRequest diff --git a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/request/SendPushRequest.kt b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/request/SendPushRequest.kt index 0bdaf3f..2e50d82 100644 --- a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/request/SendPushRequest.kt +++ b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/request/SendPushRequest.kt @@ -1,10 +1,10 @@ package ru.touchin.push.message.provider.dto.request -import ru.touchin.push.message.provider.dto.Notification +import ru.touchin.push.message.provider.dto.PushMessageNotification sealed interface SendPushRequest { - val notification: Notification? + val pushMessageNotification: PushMessageNotification? val data: Map } diff --git a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/result/CheckPushTokenResult.kt b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/result/CheckPushTokenResult.kt index e8ddd4b..a746ce0 100644 --- a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/result/CheckPushTokenResult.kt +++ b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/result/CheckPushTokenResult.kt @@ -3,5 +3,5 @@ package ru.touchin.push.message.provider.dto.result import ru.touchin.push.message.provider.enums.PushTokenStatus data class CheckPushTokenResult( - val status: PushTokenStatus + val status: PushTokenStatus, ) diff --git a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/result/SendPushTokenMessageResult.kt b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/result/SendPushTokenMessageResult.kt index c5047fa..95f1d86 100644 --- a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/result/SendPushTokenMessageResult.kt +++ b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/result/SendPushTokenMessageResult.kt @@ -1,5 +1,3 @@ package ru.touchin.push.message.provider.dto.result -class SendPushTokenMessageResult( - val messageId: String -) : SendPushResult +object SendPushTokenMessageResult : SendPushResult diff --git a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/result/SendPushTokenMessageTraceableResult.kt b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/result/SendPushTokenMessageTraceableResult.kt new file mode 100644 index 0000000..273962d --- /dev/null +++ b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/dto/result/SendPushTokenMessageTraceableResult.kt @@ -0,0 +1,5 @@ +package ru.touchin.push.message.provider.dto.result + +data class SendPushTokenMessageTraceableResult( + val messageId: String +) : SendPushResult diff --git a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/enums/PlatformType.kt b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/enums/PlatformType.kt index d88f8ea..f03a082 100644 --- a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/enums/PlatformType.kt +++ b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/enums/PlatformType.kt @@ -3,6 +3,7 @@ package ru.touchin.push.message.provider.enums enum class PlatformType { ANDROID_GOOGLE, + ANDROID_HUAWEI, IOS } diff --git a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/enums/PushMessageProviderType.kt b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/enums/PushMessageProviderType.kt index 3999187..cd8aa96 100644 --- a/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/enums/PushMessageProviderType.kt +++ b/push-message-provider/src/main/kotlin/ru/touchin/push/message/provider/enums/PushMessageProviderType.kt @@ -2,6 +2,7 @@ package ru.touchin.push.message.provider.enums enum class PushMessageProviderType { - FCM + FCM, + HPK, } diff --git a/settings.gradle.kts b/settings.gradle.kts index 6461615..b9c349c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,6 +45,7 @@ include("validation-spring") include("version-spring-web") include("push-message-provider") include("push-message-provider-fcm") +include("push-message-provider-hpk") include("push-message-provider-mock") include("response-wrapper-spring-web") include("settings-spring-jpa")