Compare commits
12 Commits
master
...
feature/pu
| Author | SHA1 | Date |
|---|---|---|
|
|
5d30c5ca6c | |
|
|
1283c89615 | |
|
|
9c655b89e2 | |
|
|
2f488548f7 | |
|
|
53ee76564d | |
|
|
304a239871 | |
|
|
a4b440ce11 | |
|
|
2ac72a1669 | |
|
|
0d7865427d | |
|
|
3d67f4a1ce | |
|
|
53a2bb680c | |
|
|
f8dd12bee0 |
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
class PushMessageNotificationConverter {
|
||||
|
||||
operator fun invoke(pushMessageNotification: PushMessageNotification): FcmNotification {
|
||||
return FcmNotification.builder()
|
||||
.setTitle(pushMessageNotification.title)
|
||||
.setBody(pushMessageNotification.description)
|
||||
.setImage(pushMessageNotification.imageUrl)
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -6,12 +6,12 @@ import com.google.firebase.messaging.ApnsConfig
|
|||
import com.google.firebase.messaging.Aps
|
||||
import com.google.firebase.messaging.Message
|
||||
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
|
||||
class PushTokenMessageConverter(
|
||||
private val notificationConverter: NotificationConverter
|
||||
private val pushMessageNotificationConverter: PushMessageNotificationConverter
|
||||
) {
|
||||
|
||||
private companion object {
|
||||
|
|
@ -26,14 +26,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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")
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package ru.touchin.push.message.provider.hpk.base.builders
|
||||
|
||||
internal interface Buildable
|
||||
|
|
@ -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 <reified S, reified F> parse(
|
||||
clientResponse: ClientResponse,
|
||||
body: String,
|
||||
): ConditionalResponse<S, F> {
|
||||
return if (isOkResponse(clientResponse)) {
|
||||
ConditionalResponse<S, F>(
|
||||
success = parseValue(body, S::class.java),
|
||||
failure = null
|
||||
)
|
||||
} else {
|
||||
ConditionalResponse(
|
||||
success = null,
|
||||
failure = parseValue(body, F::class.java)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> parseValue(source: String?, clazz: Class<T>): T {
|
||||
return if (clazz.canonicalName != String::class.java.canonicalName) {
|
||||
objectMapper.readValue(source, clazz)
|
||||
} else {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
source as T // T is String
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<ConditionalWebClientParser> = 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 <reified S, reified F> WebClient.RequestHeadersSpec<*>.exchangeWithWrap(
|
||||
requestLogData: RequestLogData,
|
||||
): Mono<ConditionalResponse<S, F>> {
|
||||
return exchangeToMono { clientResponse ->
|
||||
parse<S, F>(clientResponse)
|
||||
}.doOnNext { responseWrapper ->
|
||||
getLogger().log(
|
||||
requestLogData.copy(
|
||||
responseBody = responseWrapper.success ?: responseWrapper.failure
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <reified S, reified F> parse(
|
||||
clientResponse: ClientResponse,
|
||||
): Mono<ConditionalResponse<S, F>> {
|
||||
val responseBody = clientResponse
|
||||
.bodyToMono(String::class.java)
|
||||
.defaultIfEmpty(String())
|
||||
.publishOn(Schedulers.parallel())
|
||||
|
||||
return responseBody
|
||||
.map { body ->
|
||||
conditionalWebClientParser.value.parse(
|
||||
clientResponse = clientResponse,
|
||||
body = body,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package ru.touchin.push.message.provider.hpk.base.clients.dto
|
||||
|
||||
internal open class ConditionalResponse<S, F>(
|
||||
val success: S?,
|
||||
val failure: F?,
|
||||
) {
|
||||
|
||||
init {
|
||||
// Only one value should be present
|
||||
check((success == null) != (failure == null))
|
||||
}
|
||||
|
||||
val isSuccess: Boolean = success != null
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ru.touchin.push.message.provider.hpk.base.enums
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue
|
||||
|
||||
internal interface ValueableSerializableEnum<T> {
|
||||
|
||||
val value: T
|
||||
|
||||
@JsonValue
|
||||
fun toValue(): T = value
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.push.message.provider.hpk.base.extensions
|
||||
|
||||
import ru.touchin.push.message.provider.hpk.base.builders.Buildable
|
||||
|
||||
fun <V, B : Buildable> B.ifNotNull(value: V?, setter: B.(V) -> B): B {
|
||||
return value?.let { setter(it) } ?: this
|
||||
}
|
||||
|
|
@ -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<Int> {
|
||||
|
||||
UNKNOWN(-1, "Unknown"),
|
||||
INVALID_CLIENT_SECRET(1101, "Invalid client_secret"),
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
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
|
||||
|
||||
private const val METHOD_MESSAGES_SEND = "messages:send"
|
||||
|
||||
/**
|
||||
* Client for Huawei Push Kit.
|
||||
* @see <a href="https://developer.huawei.com/consumer/en/doc/development/HMSCore-References/https-send-api-0000001050986197">Documentation</a>
|
||||
*/
|
||||
@Component
|
||||
class HmsHpkWebClient(
|
||||
webClientLogger: WebClientLogger,
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
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 -1..100) { "Collapse Key should be [-1, 100]" }
|
||||
}
|
||||
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 {
|
||||
|
||||
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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<String, Any>,
|
||||
) {
|
||||
|
||||
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<String, Any> = mutableMapOf()
|
||||
private var aps: Aps? = null
|
||||
|
||||
fun setApnsHmsOptions(apnsHmsOptions: ApnsHmsOptions): Builder {
|
||||
this.apnsHmsOptions = apnsHmsOptions
|
||||
return this
|
||||
}
|
||||
|
||||
fun addPayload(payload: Map<String, Any>): 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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
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<String>?,
|
||||
/** 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 } == 1
|
||||
) { "Exactly one of token, topic or condition must be specified" }
|
||||
|
||||
if (token != null) {
|
||||
require(
|
||||
token.size in 1..1000
|
||||
) { "Number of tokens, if specified, must be from 1 to 1000" }
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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<String> = 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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.matches(HTTPS_URL_PATTERN)
|
||||
) { "image's url should start with HTTPS" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val HTTPS_URL_PATTERN: Regex = Regex("^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()
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
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 build(): WebPushConfig {
|
||||
return WebPushConfig(
|
||||
webPushHeaders = headers,
|
||||
data = data,
|
||||
webNotification = notification,
|
||||
webHmsOptions = webHmsOptions,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val validator = Validator()
|
||||
|
||||
fun builder() = Builder()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
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 1..99
|
||||
) { "add_num must locate between 0 and 100" }
|
||||
}
|
||||
|
||||
if (setNum != null) {
|
||||
require(
|
||||
setNum in 0..99
|
||||
) { "set_num must locate between -1 and 100" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
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 < 40
|
||||
) { "Button name length cannot exceed 40" }
|
||||
|
||||
if (androidActionType == AndroidActionType.SHARE_NOTIFICATION_MESSAGE) {
|
||||
require(!data.isNullOrEmpty()) { "Data is needed when actionType is $androidActionType" }
|
||||
require(data.length < 1024) { "Data length cannot exceed 1024 chars" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Builder : Buildable {
|
||||
|
||||
private var intent: String? = null
|
||||
private var data: String? = null
|
||||
|
||||
fun setIntent(intent: String): Builder {
|
||||
this.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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.matches(HTTPS_PATTERN)) { "url must start with https" }
|
||||
}
|
||||
|
||||
AndroidClickActionType.OPEN_APP -> {
|
||||
// no verification
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val HTTPS_PATTERN: Regex = Regex("^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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
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 > ZERO && alpha < ONE) { "Alpha must be locate between [0,1]" }
|
||||
require(red > ZERO && red < ONE) { "Red must be locate between [0,1]" }
|
||||
require(green > ZERO && green < ONE) { "Green must be locate between [0,1]" }
|
||||
require(blue > ZERO && blue < ONE) { "Blue must be locate between [0,1]" }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val ZERO: Float = -0.000001f
|
||||
private const val ONE: Float = 1.000001f
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 format is wrong" }
|
||||
require(
|
||||
lightOffDuration.matches(LIGHT_DURATION_PATTERN)
|
||||
) { "light_off_duration format 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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,410 @@
|
|||
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<String>?,
|
||||
val titleLocKey: String?,
|
||||
val titleLocArgs: Collection<String>?,
|
||||
val multiLangKey: Map<String, String>?,
|
||||
/** 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<String>?,
|
||||
/** 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<String>?,
|
||||
@JsonProperty("buttons")
|
||||
val androidButtons: Collection<AndroidButton>?,
|
||||
/** 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.matches(HTTPS_URL_PATTERN)) { "notifyIcon must start with https" }
|
||||
}
|
||||
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.orEmpty().size <= 5
|
||||
) { "inboxContent is required when style is $androidStyleType and at most 5 inbox content allowed" }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (profileId != null) {
|
||||
require(profileId.length <= 64) { "profileId length cannot exceed 64 characters" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val COLOR_PATTERN: Regex = Regex("^#[0-9a-fA-F]{6}$")
|
||||
val HTTPS_URL_PATTERN: Regex = Regex("^https.*")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Builder : Buildable {
|
||||
|
||||
private var title: String? = null
|
||||
private val 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<String> = mutableListOf()
|
||||
private var titleLocKey: String? = null
|
||||
private val titleLocArgs: MutableList<String> = mutableListOf()
|
||||
private var multiLangkey: Map<String, String>? = 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<String> = mutableListOf()
|
||||
private var androidVisibility: AndroidVisibility? = null
|
||||
private var androidLightSettings: AndroidLightSettings? = null
|
||||
private var foregroundShow = false
|
||||
private val inboxContent: MutableList<String> = mutableListOf()
|
||||
private val buttons: MutableList<AndroidButton> = mutableListOf()
|
||||
private var profileId: String? = null
|
||||
|
||||
fun setTitle(title: String): Builder {
|
||||
this.title = title
|
||||
return this
|
||||
}
|
||||
|
||||
fun setBody(body: String): Builder {
|
||||
this.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<String, String>): 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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
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<String>?,
|
||||
val actionLocKey: String?,
|
||||
val locKey: String?,
|
||||
val locArgs: Collection<String>?,
|
||||
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<String> = mutableListOf()
|
||||
private var actionLocKey: String? = null
|
||||
private var locKey: String? = null
|
||||
private val locArgs: MutableList<String> = 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 setAddAllTitleLocArgs(titleLocArgs: Collection<String>): Builder {
|
||||
this.titleLocArgs.addAll(titleLocArgs)
|
||||
return this
|
||||
}
|
||||
|
||||
fun setAddTitleLocArg(titleLocArg: String): Builder {
|
||||
titleLocArgs.add(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(locArgs: Collection<String>): Builder {
|
||||
this.locArgs.addAll(locArgs)
|
||||
return this
|
||||
}
|
||||
|
||||
fun addLocArg(locArg: String): Builder {
|
||||
locArgs.add(locArg)
|
||||
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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
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.matches(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 < 64
|
||||
) { "Number of apnsCollapseId bytes should be less than 64" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private val AUTHORIZATION_PATTERN: Regex = Regex("^bearer*")
|
||||
private val APN_ID_PATTERN: Regex = Regex("[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
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 build(): WebActions {
|
||||
return WebActions(
|
||||
action = action,
|
||||
icon = icon,
|
||||
title = title,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val validator = Validator()
|
||||
|
||||
fun builder() = Builder()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
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 build(): WebHmsOptions {
|
||||
return WebHmsOptions(
|
||||
link = link,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val validator = Validator()
|
||||
|
||||
fun builder() = Builder()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
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 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?,
|
||||
val dir: String?,
|
||||
val vibrate: Collection<Int>?,
|
||||
val renotify: Boolean,
|
||||
val requireInteraction: Boolean,
|
||||
val silent: Boolean,
|
||||
val timestamp: Long?,
|
||||
@JsonProperty("actions")
|
||||
val actions: Collection<WebActions>?,
|
||||
) {
|
||||
|
||||
class Validator {
|
||||
|
||||
fun check(webNotification: WebNotification) {
|
||||
with(webNotification) {
|
||||
if (dir != null) {
|
||||
require(
|
||||
DIR_VALUE.any { it == dir }
|
||||
) { "Invalid dir" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val DIR_VALUE: Array<String> = arrayOf("auto", "ltr", "rtl")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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: String? = null
|
||||
private val vibrate: MutableList<Int> = mutableListOf()
|
||||
private var renotify = false
|
||||
private var requireInteraction = false
|
||||
private var silent = false
|
||||
private var timestamp: Long? = null
|
||||
private val actions: MutableList<WebActions> = mutableListOf()
|
||||
|
||||
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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
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 WebPushHeaders private constructor(
|
||||
val ttl: String?,
|
||||
val topic: String?,
|
||||
val urgency: String?,
|
||||
) {
|
||||
|
||||
class Validator {
|
||||
|
||||
fun check(webpushHeaders: WebPushHeaders) {
|
||||
with(webpushHeaders) {
|
||||
if (ttl != null) {
|
||||
require(ttl.matches(TTL_PATTERN)) { "Invalid ttl format" }
|
||||
}
|
||||
if (urgency != null) {
|
||||
require(
|
||||
URGENCY_VALUE.all { it == urgency }
|
||||
) { "Invalid urgency" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val TTL_PATTERN: Regex = Regex("[0-9]+|[0-9]+[sS]")
|
||||
val URGENCY_VALUE: Array<String> = arrayOf("very-low", "low", "normal", "high")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Builder : Buildable {
|
||||
|
||||
private var ttl: String? = null
|
||||
private var topic: String? = null
|
||||
private var urgency: String? = null
|
||||
|
||||
fun setTtl(ttl: String): Builder {
|
||||
this.ttl = ttl
|
||||
return this
|
||||
}
|
||||
|
||||
fun setTopic(topic: String): Builder {
|
||||
this.topic = topic
|
||||
return this
|
||||
}
|
||||
|
||||
fun setUrgency(urgency: String): 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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<Short> {
|
||||
|
||||
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),
|
||||
|
||||
}
|
||||
|
|
@ -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<Short> {
|
||||
|
||||
CUSTOMIZE_ACTION(1),
|
||||
OPEN_URL(2),
|
||||
OPEN_APP(3),
|
||||
|
||||
}
|
||||
|
|
@ -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<Short> {
|
||||
|
||||
DEVELOPMENT(1),
|
||||
PRODUCTION(2),
|
||||
|
||||
}
|
||||
|
|
@ -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<String> {
|
||||
|
||||
LOW("LOW"),
|
||||
NORMAL("NORMAL"),
|
||||
HIGH("HIGH"), // TODO: check if this type is still supported by HMS HPK API
|
||||
|
||||
}
|
||||
|
|
@ -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<Short> {
|
||||
|
||||
INTENT(0),
|
||||
ACTION(1),
|
||||
|
||||
}
|
||||
|
|
@ -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<Short> {
|
||||
|
||||
DEFAULT(0),
|
||||
BIG_TEXT(1),
|
||||
INBOX(3),
|
||||
|
||||
}
|
||||
|
|
@ -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<Short> {
|
||||
|
||||
TEST_USER(1),
|
||||
FORMAL_USER(2),
|
||||
VOIP_USER(3),
|
||||
|
||||
}
|
||||
|
|
@ -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<String> {
|
||||
|
||||
SUBSCRIBE("subscribe"),
|
||||
UNSUBSCRIBE("unsubscribe"),
|
||||
LIST("list"),
|
||||
|
||||
}
|
||||
|
|
@ -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<String> {
|
||||
|
||||
HIGH("HIGH"),
|
||||
NORMAL("NORMAL"),
|
||||
|
||||
}
|
||||
|
|
@ -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<String> {
|
||||
|
||||
/** 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"),
|
||||
|
||||
}
|
||||
|
|
@ -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<Short> {
|
||||
|
||||
SEND_BY_GROUP(5),
|
||||
SEND_IMMIDIATELY(10),
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.push.message.provider.hpk.clients.hms_hpk.requests
|
||||
|
||||
internal open class HmsHpkRequest(
|
||||
val accessToken: String,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
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
|
||||
|
||||
private const val GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials"
|
||||
private const val METHOD_TOKEN = "token"
|
||||
|
||||
@Component
|
||||
class HmsOauthWebClient(
|
||||
webClientLogger: WebClientLogger,
|
||||
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<HmsOauthTokenResponse, HmsOauthErrorResponse> {
|
||||
return getWebClient()
|
||||
.post()
|
||||
.uri { builder ->
|
||||
builder
|
||||
.path(METHOD_TOKEN)
|
||||
.build()
|
||||
}
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(
|
||||
BodyInserters
|
||||
.fromFormData("grant_type", GRANT_TYPE_CLIENT_CREDENTIALS)
|
||||
.with("client_id", hpkProperties.webServices.clientId)
|
||||
.with("client_secret", hpkProperties.webServices.oauth.clientSecret)
|
||||
)
|
||||
.exchangeWithWrap<HmsOauthTokenResponse, HmsOauthErrorResponse>(
|
||||
requestLogData = RequestLogData(
|
||||
uri = METHOD_TOKEN,
|
||||
logTags = listOf(),
|
||||
method = HttpMethod.POST,
|
||||
),
|
||||
)
|
||||
.block() ?: throw IllegalStateException("No response")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
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.beans.factory.annotation.Qualifier
|
||||
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 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
|
||||
@Qualifier("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
|
||||
@Qualifier("push-message-provider.hpk.client-objectmapper")
|
||||
fun clientObjectMapper(): ObjectMapper {
|
||||
return jacksonObjectMapper()
|
||||
.registerModule(JavaTimeModule())
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Qualifier("push-message-provider.hpk.webclient-cachemanager")
|
||||
fun cacheManager(): CacheManager {
|
||||
return SimpleCacheManager().also {
|
||||
it.setCaches(
|
||||
listOf(
|
||||
ConcurrentMapCache(HMS_CLIENT_SERVICE_CACHE_KEY)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
class NotificationConverter {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
class PushTokenMessageConverter(
|
||||
private val notificationConverter: NotificationConverter,
|
||||
@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(notificationConverter(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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
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.INVALID_TOKEN,
|
||||
HmsResponseCode.PERMISSION_DENIED -> {
|
||||
throw InvalidPushTokenException()
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw PushMessageProviderException(result.msg, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
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.push.message.provider.hpk.properties.HpkProperties
|
||||
import ru.touchin.push.message.provider.hpk.services.dto.AccessToken
|
||||
import java.time.Instant
|
||||
|
||||
@Service
|
||||
class HmsOauthAccessTokenCacheServiceImpl(
|
||||
@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<AccessToken>() {})
|
||||
?: return null
|
||||
|
||||
return if (accessToken.isValid()) {
|
||||
accessToken
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun evict() {
|
||||
getCache()?.evict(hpkProperties.webServices.clientId)
|
||||
}
|
||||
|
||||
private fun <T> safeCast(item: Any, typeReference: TypeReference<T>): T? {
|
||||
return try {
|
||||
objectMapper.convertValue(item, typeReference)
|
||||
} catch (e: Exception) {
|
||||
print(e.message)
|
||||
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"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.push.message.provider.hpk.services
|
||||
|
||||
interface HmsOauthClientService {
|
||||
|
||||
fun getAccessToken(): String
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package ru.touchin.push.message.provider.hpk
|
||||
|
||||
import org.springframework.boot.SpringBootConfiguration
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
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 {
|
||||
|
||||
@Bean
|
||||
fun webClientBuilder(): WebClient.Builder = WebClient.builder()
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package ru.touchin.push.message.provider.hpk.clients.hms_hpk
|
||||
|
||||
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
|
||||
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_pushTokenNotSpecified() {
|
||||
val result = hmsHpkWebClient.messagesSend(
|
||||
HmsHpkMessagesSendRequest(
|
||||
hmsHpkMessagesSendBody = HmsHpkMessagesSendBody(
|
||||
validateOnly = true,
|
||||
message = Message.builder()
|
||||
.addToken("pushTokenWithLongLength")
|
||||
.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()
|
||||
),
|
||||
accessToken = "testAccessToken"
|
||||
)
|
||||
)
|
||||
|
||||
Assertions.assertEquals(
|
||||
HmsResponseCode.PERMISSION_DENIED.value.toString(),
|
||||
result.code
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
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.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 getAccessToken_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) }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
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() {
|
||||
Mockito.`when`(
|
||||
hmsOauthWebClient.token()
|
||||
).then {
|
||||
ConditionalResponse(
|
||||
success = HmsOauthTokenResponse(
|
||||
tokenType = "tokenType",
|
||||
expiresIn = 60_000,
|
||||
accessToken = "accessToken"
|
||||
),
|
||||
failure = null
|
||||
)
|
||||
}
|
||||
|
||||
Assertions.assertThrows(
|
||||
InvalidPushTokenException::class.java
|
||||
) { hmsOauthClientService.getAccessToken() }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
push-message-provider:
|
||||
platformProviders:
|
||||
ANDROID_HUAWEI:
|
||||
- HPK
|
||||
hpk:
|
||||
web-services:
|
||||
client-id: 1
|
||||
oauth:
|
||||
client-secret: 2
|
||||
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
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package ru.touchin.push.message.provider.dto
|
||||
|
||||
class Notification(
|
||||
class PushMessageNotification(
|
||||
val title: String?,
|
||||
val description: String?,
|
||||
val imageUrl: String?
|
||||
|
|
@ -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?,
|
||||
val pushMessageNotification: PushMessageNotification?,
|
||||
override val data: Map<String, String>
|
||||
) : SendPushRequest
|
||||
|
|
|
|||
|
|
@ -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<String, String>
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
package ru.touchin.push.message.provider.dto.result
|
||||
|
||||
class SendPushTokenMessageResult(
|
||||
val messageId: String
|
||||
) : SendPushResult
|
||||
object SendPushTokenMessageResult : SendPushResult
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.push.message.provider.dto.result
|
||||
|
||||
data class SendPushTokenMessageTraceableResult(
|
||||
val messageId: String
|
||||
) : SendPushResult
|
||||
|
|
@ -3,6 +3,7 @@ package ru.touchin.push.message.provider.enums
|
|||
enum class PlatformType {
|
||||
|
||||
ANDROID_GOOGLE,
|
||||
ANDROID_HUAWEI,
|
||||
IOS
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package ru.touchin.push.message.provider.enums
|
|||
|
||||
enum class PushMessageProviderType {
|
||||
|
||||
FCM
|
||||
FCM,
|
||||
HPK,
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("response-wrapper-spring-web")
|
||||
include("settings-spring-jpa")
|
||||
include("security-authorization-server-core")
|
||||
|
|
|
|||
Loading…
Reference in New Issue