diff --git a/build.gradle.kts b/build.gradle.kts index ef1d1b0..78c1891 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "com.cubetiqs" -version = "0.0.1-SNAPSHOT" +version = "0.0.1" java.sourceCompatibility = JavaVersion.VERSION_1_8 repositories { @@ -14,17 +14,17 @@ repositories { } dependencies { - implementation(platform("org.jetbrains.kotlin:kotlin-bom")) - - implementation("com.squareup.okhttp3:okhttp:4.9.0") - implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - // logback driver and slf4j logging - implementation("ch.qos.logback:logback-core:1.3.0-alpha5") - implementation("org.slf4j:slf4j-api:2.0.0-alpha1") + // http client + implementation("com.squareup.okhttp3:okhttp:4.9.0") + // logback driver and slf4j logging + implementation("org.slf4j:slf4j-api:1.7.30") + implementation("org.slf4j:slf4j-simple:1.7.30") + + // test framework testImplementation("org.junit.jupiter:junit-jupiter:5.7.0") } diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..708e7b4 --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +sh gradlew clean +sh gradlew jar diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/email/IEmailProvider.kt b/src/main/kotlin/com/cubetiqs/messaging/client/email/IEmailProvider.kt new file mode 100644 index 0000000..f6fd1de --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/messaging/client/email/IEmailProvider.kt @@ -0,0 +1,5 @@ +package com.cubetiqs.messaging.client.email + +import com.cubetiqs.messaging.client.provider.MessageProvider + +interface IEmailProvider : MessageProvider \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/provider/MessageSender.kt b/src/main/kotlin/com/cubetiqs/messaging/client/provider/MessageSender.kt new file mode 100644 index 0000000..7cc1fcb --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/messaging/client/provider/MessageSender.kt @@ -0,0 +1,27 @@ +package com.cubetiqs.messaging.client.provider + +/** + * Message Sender + * + * @author sombochea + * @since 1.0 + */ +class MessageSender (provider: MessageProvider? = null) { + private var provider: MessageProvider? = null + + init { + this.provider = provider + } + + fun setProvider(provider: MessageProvider) = apply { + this.provider = provider + } + + fun send() = provider?.send() + + companion object { + fun send(provider: MessageProvider): Any? { + return MessageSender(provider).send() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/sms/ISmsProvider.kt b/src/main/kotlin/com/cubetiqs/messaging/client/sms/ISmsProvider.kt new file mode 100644 index 0000000..df76ee8 --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/messaging/client/sms/ISmsProvider.kt @@ -0,0 +1,11 @@ +package com.cubetiqs.messaging.client.sms + +import com.cubetiqs.messaging.client.provider.MessageProvider + +/** + * Sms Provider + * + * @author sombochea + * @since 1.0 + */ +interface ISmsProvider : MessageProvider \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsMessage.kt b/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsMessage.kt new file mode 100644 index 0000000..1bf5613 --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsMessage.kt @@ -0,0 +1,35 @@ +package com.cubetiqs.messaging.client.sms + +data class SmsMessage ( + var to: String, + var sender: String, + var text: String, + // if this true, meant the sender is phone number (from) + // else is the message service id for send the message + var isSenderNumber: Boolean = false, +) { + class SmsMessageBuilder { + private var to: String? = null + private var sender: String? = null + private var text: String? = null + private var isSenderNumber: Boolean? = null + + fun setTo(to: String) = apply { this.to = to } + fun setSender(sender: String) = apply { this.sender = sender } + fun setText(text: String) = apply { this.text = text } + fun isSenderNumber(isSenderNumber: Boolean) = apply { this.isSenderNumber = isSenderNumber } + + fun build(): SmsMessage { + return SmsMessage( + to = to ?: throw IllegalArgumentException("Receiver is required!"), + sender = sender ?: throw IllegalArgumentException("Sender is required!"), + text = text ?: throw IllegalArgumentException("Message is required!"), + isSenderNumber = isSenderNumber ?: false + ) + } + } + + companion object { + fun builder() = SmsMessageBuilder() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsProvider.kt b/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsProvider.kt index 03df496..a00d37c 100644 --- a/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsProvider.kt +++ b/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsProvider.kt @@ -1,12 +1,23 @@ package com.cubetiqs.messaging.client.sms -import com.cubetiqs.messaging.client.provider.MessageProvider - /** - * Sms Provider + * SMS Provider * * @author sombochea * @since 1.0 */ -interface SmsProvider : MessageProvider { +abstract class SmsProvider : ISmsProvider { + private var to: String? = null + private var text: String? = null + + fun getToNumber(): String = to?.trim() ?: throw IllegalArgumentException("Sms receiver is required!") + fun getText(): String = text ?: throw IllegalArgumentException("Sms content is required!") + + fun setText(text: String?) = apply { + this.text = text + } + + fun setToNumber(toNumber: String?) = apply { + this.to = toNumber + } } \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsSendException.kt b/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsSendException.kt new file mode 100644 index 0000000..8179e93 --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsSendException.kt @@ -0,0 +1,7 @@ +package com.cubetiqs.messaging.client.sms + +open class SmsSendException : RuntimeException { + constructor(message: String? = "Sms send occurred!") : super(message) + constructor(message: String?, cause: Throwable?) : super(message, cause) + constructor(cause: Throwable?) : super(cause) +} \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsSimulatorProvider.kt b/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsSimulatorProvider.kt new file mode 100644 index 0000000..0f9f0e2 --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/messaging/client/sms/SmsSimulatorProvider.kt @@ -0,0 +1,12 @@ +package com.cubetiqs.messaging.client.sms + +class SmsSimulatorProvider : SmsProvider() { + private fun getFormatMessage() = """ + To Number: ${getToNumber()} + Message: ${getText()} + """.trimIndent() + + override fun send(): Any { + return getFormatMessage() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/sms/twlio/TwilioConfig.kt b/src/main/kotlin/com/cubetiqs/messaging/client/sms/twlio/TwilioConfig.kt new file mode 100644 index 0000000..3acfdb2 --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/messaging/client/sms/twlio/TwilioConfig.kt @@ -0,0 +1,25 @@ +package com.cubetiqs.messaging.client.sms.twlio + +import com.cubetiqs.messaging.client.util.ConfigUtils + +object TwilioConfig { + const val ENDPOINT = "https://api.twilio.com/2010-04-01/Accounts" + private const val CUBETIQ_TWILIO_TOKEN = "CUBETIQ_TWILIO_TOKEN" + private const val CUBETIQ_TWILIO_ID = "CUBETIQ_TWILIO_ID" + private const val CUBETIQ_TWILIO_SENDER = "CUBETIQ_TWILIO_SENDER" + + @JvmStatic + fun getAccountToken(): String { + return ConfigUtils.getEnv(CUBETIQ_TWILIO_TOKEN, ConfigUtils.getProperty(CUBETIQ_TWILIO_TOKEN)) ?: throw NullPointerException("CUBETIQ_TWILIO_TOKEN is required!") + } + + @JvmStatic + fun getAccountId(): String { + return ConfigUtils.getEnv(CUBETIQ_TWILIO_ID, ConfigUtils.getProperty(CUBETIQ_TWILIO_ID)) ?: throw NullPointerException("CUBETIQ_TWILIO_ID is required!") + } + + @JvmStatic + fun getAccountSender(): String { + return ConfigUtils.getEnv(CUBETIQ_TWILIO_SENDER, ConfigUtils.getProperty(CUBETIQ_TWILIO_SENDER)) ?: throw NullPointerException("CUBETIQ_TWILIO_SENDER is required for SMS Sender!") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/sms/twlio/TwilioProvider.kt b/src/main/kotlin/com/cubetiqs/messaging/client/sms/twlio/TwilioProvider.kt new file mode 100644 index 0000000..75704a9 --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/messaging/client/sms/twlio/TwilioProvider.kt @@ -0,0 +1,10 @@ +package com.cubetiqs.messaging.client.sms.twlio + +import com.cubetiqs.messaging.client.sms.SmsMessage +import com.cubetiqs.messaging.client.sms.SmsProvider + +open class TwilioProvider (private val message: SmsMessage) : SmsProvider() { + override fun send(): Any? { + return TwilioUtils.sendMessage(message) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/sms/twlio/TwilioUtils.kt b/src/main/kotlin/com/cubetiqs/messaging/client/sms/twlio/TwilioUtils.kt new file mode 100644 index 0000000..ad5431d --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/messaging/client/sms/twlio/TwilioUtils.kt @@ -0,0 +1,101 @@ +package com.cubetiqs.messaging.client.sms.twlio + +import com.cubetiqs.messaging.client.sms.SmsMessage +import com.cubetiqs.messaging.client.sms.SmsSendException +import com.cubetiqs.messaging.client.sms.twlio.TwilioConfig.ENDPOINT +import com.cubetiqs.messaging.client.util.Loggable +import com.cubetiqs.messaging.client.webclient.WebClientUtils +import java.util.concurrent.atomic.AtomicInteger + +object TwilioUtils : Loggable { + private val limiters = mutableMapOf() + // able to send the sms + private var capacity = 3 + + private fun increaseSentCount(key: String): Int { + return if (limiters.containsKey(key)) { + limiters[key]!!.incrementAndGet() + } else { + limiters[key] = AtomicInteger(1) + 1 + } + } + + @JvmStatic + // reset statistic for key of sms + fun resetCounter(key: String) { + if (limiters.containsKey(key)) { + limiters[key]!!.setRelease(0) + } + } + + @JvmStatic + // reset all statistic + fun releaseAllCounter() = limiters.clear() + + private var accountId: String? = null + private var accountToken: String? = null + + @JvmStatic + fun init(accountId: String, accountToken: String, capacity: Int = 3) { + log.info("Twilio initializing Account ID: {} and Account Token: {}", accountId, accountToken) + + TwilioUtils.accountId = accountId + TwilioUtils.accountToken = accountToken + TwilioUtils.capacity = capacity + } + + private fun getAccountId(): String { + return this.accountId ?: TwilioConfig.getAccountId() + } + + private fun getAccountToken(): String { + return this.accountToken ?: TwilioConfig.getAccountToken() + } + + @JvmStatic + fun capacity(capacity: Int) = apply { TwilioUtils.capacity = capacity } + + private fun getEndpointMessage(): String { + if (getAccountId().isEmpty() && getAccountToken().isEmpty()) throw IllegalArgumentException("account id and token must be not empty!") + return "$ENDPOINT/$accountId/Messages.json" + } + + @JvmStatic + fun sendMessage(message: SmsMessage): Any { + if (message.to.isEmpty() || message.to.isBlank()) throw IllegalArgumentException("message send to must be not empty or blank!") + if (message.sender.isEmpty() || message.sender.isBlank()) throw IllegalArgumentException("message sender must be not empty or blank!") + if (message.text.isEmpty() || message.text.isBlank()) throw IllegalArgumentException("message must be not empty or blank!") + + if (increaseSentCount(message.to) > capacity) { + throw SmsSendException("send capacity out of bound!") + } + + val body: MutableMap = mutableMapOf() + body["To"] = message.to + body["Body"] = message.text + + if (message.isSenderNumber) { + body["From"] = message.sender + } else { + body["MessagingServiceSid"] = message.sender + } + + val url = getEndpointMessage() + val result = WebClientUtils.postRequest(url, body) + + log.info("Twilio Complete sent message via: {}", message) + return result + } + + @JvmStatic + fun sendMessage(sendTo: String, message: String): Any { + val request = SmsMessage.builder() + .setText(message) + .setSender(TwilioConfig.getAccountSender()) + .setTo(sendTo) + .build() + + return sendMessage(request) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/telegram/TelegramBotUtils.kt b/src/main/kotlin/com/cubetiqs/messaging/client/telegram/TelegramBotUtils.kt index 2bdae0b..a0787c1 100644 --- a/src/main/kotlin/com/cubetiqs/messaging/client/telegram/TelegramBotUtils.kt +++ b/src/main/kotlin/com/cubetiqs/messaging/client/telegram/TelegramBotUtils.kt @@ -15,13 +15,13 @@ object TelegramBotUtils : Loggable { private fun makeRequest( request: Request, ): Response? { - log.debug("Start send message via telegram bot...") + log.info("Start send message via telegram bot...") return try { WebClientUtils.makeRequest(request) } catch (ex: Exception) { ex.printStackTrace() log.error("Telegram make request error {}", ex.message) - null + throw TelegramSendException(ex) } } @@ -49,7 +49,7 @@ object TelegramBotUtils : Loggable { .build() val result = makeRequest(request) - log.debug("Telegram complete sent message to {}", chatId) + log.info("Telegram complete sent message to {}", chatId) return result } @@ -95,7 +95,7 @@ object TelegramBotUtils : Loggable { .build() val result = makeRequest(request) - log.debug("Telegram complete sent message to {}", chatId) + log.info("Telegram complete sent message to {}", chatId) return result } } \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/telegram/TelegramSendException.kt b/src/main/kotlin/com/cubetiqs/messaging/client/telegram/TelegramSendException.kt new file mode 100644 index 0000000..85a785b --- /dev/null +++ b/src/main/kotlin/com/cubetiqs/messaging/client/telegram/TelegramSendException.kt @@ -0,0 +1,7 @@ +package com.cubetiqs.messaging.client.telegram + +open class TelegramSendException : RuntimeException { + constructor(message: String = "Telegram send occurred!") : super(message) + constructor(message: String?, cause: Throwable?) : super(message, cause) + constructor(cause: Throwable?) : super(cause) +} \ No newline at end of file diff --git a/src/main/kotlin/com/cubetiqs/messaging/client/webclient/WebClientUtils.kt b/src/main/kotlin/com/cubetiqs/messaging/client/webclient/WebClientUtils.kt index f6d80aa..675eeb9 100644 --- a/src/main/kotlin/com/cubetiqs/messaging/client/webclient/WebClientUtils.kt +++ b/src/main/kotlin/com/cubetiqs/messaging/client/webclient/WebClientUtils.kt @@ -1,6 +1,7 @@ package com.cubetiqs.messaging.client.webclient import com.cubetiqs.messaging.client.util.Loggable +import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response @@ -18,7 +19,7 @@ object WebClientUtils : Loggable { @JvmStatic fun makeRequest(request: Request): Response { - log.debug("Web is make request to: {} with method: {}", request.url, request.method) + log.info("Web is make request to: {} with method: {}", request.url, request.method) val call = getClient().newCall(request) var response: Response? = null return try { @@ -30,4 +31,20 @@ object WebClientUtils : Loggable { response?.close() } } + + @JvmStatic + fun postRequest(url: String, params: Map): Response { + val requestBody = MultipartBody.Builder() + .setType(MultipartBody.FORM) + params.forEach { + requestBody.addFormDataPart(it.key, it.value) + } + + val request = Request.Builder() + .url(url) + .post(requestBody.build()) + .build() + + return makeRequest(request) + } } \ No newline at end of file