diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3e78743..d983186 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,6 +11,7 @@
+
diff --git a/app/src/main/java/me/capcom/smsgateway/App.kt b/app/src/main/java/me/capcom/smsgateway/App.kt
index a695002..e67c84b 100644
--- a/app/src/main/java/me/capcom/smsgateway/App.kt
+++ b/app/src/main/java/me/capcom/smsgateway/App.kt
@@ -14,6 +14,7 @@ import me.capcom.smsgateway.modules.notifications.notificationsModule
import me.capcom.smsgateway.modules.orchestrator.OrchestratorService
import me.capcom.smsgateway.modules.orchestrator.orchestratorModule
import me.capcom.smsgateway.modules.ping.pingModule
+import me.capcom.smsgateway.modules.receiver.receiverModule
import me.capcom.smsgateway.modules.settings.settingsModule
import me.capcom.smsgateway.modules.webhooks.webhooksModule
import me.capcom.smsgateway.receivers.EventsReceiver
@@ -37,6 +38,7 @@ class App: Application() {
logsModule,
notificationsModule,
messagesModule,
+ receiverModule,
encryptionModule,
me.capcom.smsgateway.modules.gateway.gatewayModule,
healthModule,
diff --git a/app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt b/app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt
index e012944..a5ccf58 100644
--- a/app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt
+++ b/app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt
@@ -30,6 +30,22 @@ object SubscriptionsHelper {
}
}
+ @SuppressLint("MissingPermission")
+ fun getSubscriptionId(context: Context, simSlotIndex: Int): Int? {
+ if (!hasPhoneStatePermission(context)) {
+ return null
+ }
+
+ val subscriptionManager = getSubscriptionsManager(context) ?: return null
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
+ subscriptionManager.activeSubscriptionInfoList.find {
+ it.simSlotIndex == simSlotIndex
+ }?.subscriptionId
+ } else {
+ null
+ }
+ }
+
@SuppressLint("MissingPermission")
fun getSimSlotIndex(context: Context, subscriptionId: Int): Int? {
if (!hasPhoneStatePermission(context)) {
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/localserver/WebService.kt b/app/src/main/java/me/capcom/smsgateway/modules/localserver/WebService.kt
index e8bd8a6..0262c28 100644
--- a/app/src/main/java/me/capcom/smsgateway/modules/localserver/WebService.kt
+++ b/app/src/main/java/me/capcom/smsgateway/modules/localserver/WebService.kt
@@ -9,7 +9,6 @@ import android.os.IBinder
import android.os.PowerManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
-import com.aventrix.jnanoid.jnanoid.NanoIdUtils
import io.ktor.http.HttpStatusCode
import io.ktor.http.toHttpDate
import io.ktor.serialization.gson.gson
@@ -24,28 +23,21 @@ import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.statuspages.StatusPages
-import io.ktor.server.request.receive
import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.routing.get
-import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import io.ktor.util.date.GMTDate
import me.capcom.smsgateway.BuildConfig
import me.capcom.smsgateway.R
-import me.capcom.smsgateway.domain.EntitySource
-import me.capcom.smsgateway.domain.ProcessingState
import me.capcom.smsgateway.extensions.configure
import me.capcom.smsgateway.modules.health.HealthService
import me.capcom.smsgateway.modules.health.domain.Status
import me.capcom.smsgateway.modules.localserver.domain.Device
-import me.capcom.smsgateway.modules.localserver.domain.PostMessageRequest
-import me.capcom.smsgateway.modules.localserver.domain.PostMessageResponse
import me.capcom.smsgateway.modules.localserver.routes.LogsRoutes
+import me.capcom.smsgateway.modules.localserver.routes.MessagesRoutes
import me.capcom.smsgateway.modules.localserver.routes.WebhooksRoutes
-import me.capcom.smsgateway.modules.messages.MessagesService
-import me.capcom.smsgateway.modules.messages.data.SendRequest
import me.capcom.smsgateway.modules.notifications.NotificationsService
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
@@ -55,7 +47,6 @@ import kotlin.concurrent.thread
class WebService : Service() {
private val settings: LocalServerSettings by inject()
- private val messagesService: MessagesService by inject()
private val notificationsService: NotificationsService by inject()
private val healthService: HealthService by inject()
@@ -162,98 +153,12 @@ class WebService : Service() {
call.respond(listOf(device))
}
}
- route("/message") {
- post {
- val request = call.receive()
- if (request.message.isEmpty()) {
- call.respond(
- HttpStatusCode.BadRequest,
- mapOf("message" to "message is empty")
- )
- }
- if (request.phoneNumbers.isEmpty()) {
- call.respond(
- HttpStatusCode.BadRequest,
- mapOf("message" to "phoneNumbers is empty")
- )
- }
- if (request.simNumber != null && request.simNumber < 1) {
- call.respond(
- HttpStatusCode.BadRequest,
- mapOf("message" to "simNumber must be >= 1")
- )
- }
- val skipPhoneValidation =
- call.request.queryParameters["skipPhoneValidation"]
- ?.toBooleanStrict() ?: false
-
- val sendRequest = SendRequest(
- EntitySource.Local,
- me.capcom.smsgateway.modules.messages.data.Message(
- request.id ?: NanoIdUtils.randomNanoId(),
- request.message,
- request.phoneNumbers,
- request.isEncrypted ?: false
- ),
- me.capcom.smsgateway.modules.messages.data.SendParams(
- request.withDeliveryReport ?: true,
- skipPhoneValidation = skipPhoneValidation,
- simNumber = request.simNumber,
- validUntil = request.validUntil,
- )
- )
- messagesService.enqueueMessage(sendRequest)
-
- val messageId = sendRequest.message.id
-
- call.respond(
- HttpStatusCode.Accepted,
- PostMessageResponse(
- id = messageId,
- state = ProcessingState.Pending,
- recipients = request.phoneNumbers.map {
- PostMessageResponse.Recipient(
- it,
- ProcessingState.Pending,
- null
- )
- },
- isEncrypted = request.isEncrypted ?: false,
- mapOf(ProcessingState.Pending to Date())
- )
- )
+ MessagesRoutes(applicationContext, get(), get()).let {
+ route("/message") {
+ it.register(this)
}
- get("{id}") {
- val id = call.parameters["id"]
- ?: return@get call.respond(HttpStatusCode.BadRequest)
-
- val message = try {
- messagesService.getMessage(id)
- ?: return@get call.respond(HttpStatusCode.NotFound)
- } catch (e: Throwable) {
- return@get call.respond(
- HttpStatusCode.InternalServerError,
- mapOf("message" to e.message)
- )
- }
-
- call.respond(
- PostMessageResponse(
- message.message.id,
- message.message.state,
- message.recipients.map {
- PostMessageResponse.Recipient(
- it.phoneNumber,
- it.state,
- it.error
- )
- },
- message.message.isEncrypted,
- message.states.associate {
- it.state to Date(it.updatedAt)
- }
- )
- )
+ route("/messages") {
+ it.register(this)
}
}
WebhooksRoutes(get()).let {
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/localserver/domain/PostMessagesInboxExportRequest.kt b/app/src/main/java/me/capcom/smsgateway/modules/localserver/domain/PostMessagesInboxExportRequest.kt
new file mode 100644
index 0000000..4e93785
--- /dev/null
+++ b/app/src/main/java/me/capcom/smsgateway/modules/localserver/domain/PostMessagesInboxExportRequest.kt
@@ -0,0 +1,26 @@
+package me.capcom.smsgateway.modules.localserver.domain
+
+import java.util.Date
+
+data class PostMessagesInboxExportRequest(
+ val since: Date,
+ val until: Date,
+) {
+ val period: Pair
+ get() = since to until
+
+ fun validate(): PostMessagesInboxExportRequest {
+ if (since == null) {
+ throw IllegalArgumentException("since is required")
+ }
+
+ if (until == null) {
+ throw IllegalArgumentException("until is required")
+ }
+
+ if (since.after(until)) {
+ throw IllegalArgumentException("since must be before until")
+ }
+ return this
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/localserver/routes/MessagesRoutes.kt b/app/src/main/java/me/capcom/smsgateway/modules/localserver/routes/MessagesRoutes.kt
new file mode 100644
index 0000000..894339a
--- /dev/null
+++ b/app/src/main/java/me/capcom/smsgateway/modules/localserver/routes/MessagesRoutes.kt
@@ -0,0 +1,148 @@
+package me.capcom.smsgateway.modules.localserver.routes
+
+import android.content.Context
+import com.aventrix.jnanoid.jnanoid.NanoIdUtils
+import io.ktor.http.HttpStatusCode
+import io.ktor.server.application.call
+import io.ktor.server.request.receive
+import io.ktor.server.response.respond
+import io.ktor.server.routing.Route
+import io.ktor.server.routing.get
+import io.ktor.server.routing.post
+import io.ktor.server.routing.route
+import me.capcom.smsgateway.domain.EntitySource
+import me.capcom.smsgateway.domain.ProcessingState
+import me.capcom.smsgateway.modules.localserver.domain.PostMessageRequest
+import me.capcom.smsgateway.modules.localserver.domain.PostMessageResponse
+import me.capcom.smsgateway.modules.localserver.domain.PostMessagesInboxExportRequest
+import me.capcom.smsgateway.modules.messages.MessagesService
+import me.capcom.smsgateway.modules.messages.data.Message
+import me.capcom.smsgateway.modules.messages.data.SendParams
+import me.capcom.smsgateway.modules.messages.data.SendRequest
+import me.capcom.smsgateway.modules.receiver.ReceiverService
+import java.util.Date
+
+class MessagesRoutes(
+ private val context: Context,
+ private val messagesService: MessagesService,
+ private val receiverService: ReceiverService,
+) {
+ fun register(routing: Route) {
+ routing.apply {
+ messagesRoutes()
+ route("/inbox") {
+ inboxRoutes(context)
+ }
+ }
+ }
+
+ private fun Route.messagesRoutes() {
+ post {
+ val request = call.receive()
+ if (request.message.isEmpty()) {
+ call.respond(
+ HttpStatusCode.BadRequest,
+ mapOf("message" to "message is empty")
+ )
+ }
+ if (request.phoneNumbers.isEmpty()) {
+ call.respond(
+ HttpStatusCode.BadRequest,
+ mapOf("message" to "phoneNumbers is empty")
+ )
+ }
+ if (request.simNumber != null && request.simNumber < 1) {
+ call.respond(
+ HttpStatusCode.BadRequest,
+ mapOf("message" to "simNumber must be >= 1")
+ )
+ }
+ val skipPhoneValidation =
+ call.request.queryParameters["skipPhoneValidation"]
+ ?.toBooleanStrict() ?: false
+
+ val sendRequest = SendRequest(
+ EntitySource.Local,
+ Message(
+ request.id ?: NanoIdUtils.randomNanoId(),
+ request.message,
+ request.phoneNumbers,
+ request.isEncrypted ?: false
+ ),
+ SendParams(
+ request.withDeliveryReport ?: true,
+ skipPhoneValidation = skipPhoneValidation,
+ simNumber = request.simNumber,
+ validUntil = request.validUntil,
+ )
+ )
+ messagesService.enqueueMessage(sendRequest)
+
+ val messageId = sendRequest.message.id
+
+ call.respond(
+ HttpStatusCode.Accepted,
+ PostMessageResponse(
+ id = messageId,
+ state = ProcessingState.Pending,
+ recipients = request.phoneNumbers.map {
+ PostMessageResponse.Recipient(
+ it,
+ ProcessingState.Pending,
+ null
+ )
+ },
+ isEncrypted = request.isEncrypted ?: false,
+ mapOf(ProcessingState.Pending to Date())
+ )
+ )
+ }
+ get("{id}") {
+ val id = call.parameters["id"]
+ ?: return@get call.respond(HttpStatusCode.BadRequest)
+
+ val message = try {
+ messagesService.getMessage(id)
+ ?: return@get call.respond(HttpStatusCode.NotFound)
+ } catch (e: Throwable) {
+ return@get call.respond(
+ HttpStatusCode.InternalServerError,
+ mapOf("message" to e.message)
+ )
+ }
+
+ call.respond(
+ PostMessageResponse(
+ message.message.id,
+ message.message.state,
+ message.recipients.map {
+ PostMessageResponse.Recipient(
+ it.phoneNumber,
+ it.state,
+ it.error
+ )
+ },
+ message.message.isEncrypted,
+ message.states.associate {
+ it.state to Date(it.updatedAt)
+ }
+ )
+ )
+ }
+ }
+
+ private fun Route.inboxRoutes(context: Context) {
+ post("export") {
+ val request = call.receive().validate()
+ try {
+ receiverService.export(context, request.period)
+ call.respond(HttpStatusCode.Accepted)
+ } catch (e: Exception) {
+ call.respond(
+ HttpStatusCode.InternalServerError,
+ mapOf("message" to "Failed to export inbox: ${e.message}")
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/logs/LogsService.kt b/app/src/main/java/me/capcom/smsgateway/modules/logs/LogsService.kt
index 3ada929..172e2e1 100644
--- a/app/src/main/java/me/capcom/smsgateway/modules/logs/LogsService.kt
+++ b/app/src/main/java/me/capcom/smsgateway/modules/logs/LogsService.kt
@@ -31,20 +31,24 @@ class LogsService(
return dao.selectByPeriod(from, to)
}
- suspend fun insert(
+ fun insert(
priority: LogEntry.Priority,
module: String,
message: String,
context: Any? = null
) {
- dao.insert(
- LogEntry(
- priority,
- module,
- message,
- context = context?.let { gson.toJsonTree(it) }
+ try {
+ dao.insert(
+ LogEntry(
+ priority,
+ module,
+ message,
+ context = context?.let { gson.toJsonTree(it) }
+ )
)
- )
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
}
suspend fun truncate() {
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/logs/db/LogEntriesDao.kt b/app/src/main/java/me/capcom/smsgateway/modules/logs/db/LogEntriesDao.kt
index 6619ae9..f87e810 100644
--- a/app/src/main/java/me/capcom/smsgateway/modules/logs/db/LogEntriesDao.kt
+++ b/app/src/main/java/me/capcom/smsgateway/modules/logs/db/LogEntriesDao.kt
@@ -14,7 +14,7 @@ interface LogEntriesDao {
fun selectLast(): LiveData>
@Insert
- suspend fun insert(entry: LogEntry)
+ fun insert(entry: LogEntry)
@Query("DELETE FROM logs_entries WHERE createdAt < :until")
suspend fun truncate(until: Long)
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/push/Event.kt b/app/src/main/java/me/capcom/smsgateway/modules/push/Event.kt
index 3372b5b..4b2f9ce 100644
--- a/app/src/main/java/me/capcom/smsgateway/modules/push/Event.kt
+++ b/app/src/main/java/me/capcom/smsgateway/modules/push/Event.kt
@@ -8,4 +8,7 @@ enum class Event {
@SerializedName("WebhooksUpdated")
WebhooksUpdated,
+
+ @SerializedName("MessagesExportRequested")
+ MessagesExportRequested,
}
\ No newline at end of file
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/push/payloads/MessagesExportRequestedPayload.kt b/app/src/main/java/me/capcom/smsgateway/modules/push/payloads/MessagesExportRequestedPayload.kt
new file mode 100644
index 0000000..135b0bc
--- /dev/null
+++ b/app/src/main/java/me/capcom/smsgateway/modules/push/payloads/MessagesExportRequestedPayload.kt
@@ -0,0 +1,17 @@
+package me.capcom.smsgateway.modules.push.payloads
+
+import com.google.gson.GsonBuilder
+import me.capcom.smsgateway.extensions.configure
+import java.util.Date
+
+data class MessagesExportRequestedPayload(
+ val since: Date,
+ val until: Date,
+) {
+ companion object {
+ fun from(json: String): MessagesExportRequestedPayload {
+ val gson = GsonBuilder().configure().create()
+ return gson.fromJson(json, MessagesExportRequestedPayload::class.java)
+ }
+ }
+}
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt b/app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt
index f7bedb5..619e22c 100644
--- a/app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt
+++ b/app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt
@@ -5,14 +5,13 @@ import android.content.Context
import android.content.Intent
import android.provider.Telephony.Sms.Intents
import me.capcom.smsgateway.helpers.SubscriptionsHelper
-import me.capcom.smsgateway.modules.receiver.events.MessageReceivedEvent
-import me.capcom.smsgateway.modules.webhooks.WebHooksService
-import me.capcom.smsgateway.modules.webhooks.domain.WebHookEvent
+import me.capcom.smsgateway.modules.receiver.data.InboxMessage
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.Date
class MessagesReceiver : BroadcastReceiver(), KoinComponent {
+ private val receiverSvc: ReceiverService by inject()
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intents.SMS_RECEIVED_ACTION) {
@@ -23,32 +22,29 @@ class MessagesReceiver : BroadcastReceiver(), KoinComponent {
val firstMessage = messages.first()
val text = messages.joinToString(separator = "") { it.displayMessageBody }
- val event = MessageReceivedEvent(
- message = text,
- phoneNumber = firstMessage.displayOriginatingAddress,
- simNumber = extractSimNumber(context, intent),
- receivedAt = Date(firstMessage.timestampMillis),
+ receiverSvc.process(
+ context,
+ InboxMessage(
+ address = firstMessage.displayOriginatingAddress,
+ body = text,
+ date = Date(firstMessage.timestampMillis),
+ subscriptionId = extractSubscriptionId(context, intent),
+ )
)
-
- webHooksService.emit(WebHookEvent.SmsReceived, event)
}
- private fun extractSimNumber(context: Context, intent: Intent): Int? {
- if (intent.extras?.containsKey("android.telephony.extra.SLOT_INDEX") == true) {
- return intent.extras?.getInt("android.telephony.extra.SLOT_INDEX")?.let { it + 1 }
- }
-
- val subscriptionId = when {
+ private fun extractSubscriptionId(context: Context, intent: Intent): Int? {
+ return when {
intent.extras?.containsKey("android.telephony.extra.SUBSCRIPTION_INDEX") == true -> intent.extras?.getInt(
"android.telephony.extra.SUBSCRIPTION_INDEX"
)
intent.extras?.containsKey("subscription") == true -> intent.extras?.getInt("subscription")
- else -> null
- } ?: return null
+ intent.extras?.containsKey("android.telephony.extra.SLOT_INDEX") == true -> intent.extras?.getInt(
+ "android.telephony.extra.SLOT_INDEX"
+ )?.let { SubscriptionsHelper.getSubscriptionId(context, it) }
- return SubscriptionsHelper.getSimSlotIndex(context, subscriptionId)?.let { it + 1 }
+ else -> null
+ }
}
-
- private val webHooksService: WebHooksService by inject()
}
\ No newline at end of file
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/receiver/Module.kt b/app/src/main/java/me/capcom/smsgateway/modules/receiver/Module.kt
new file mode 100644
index 0000000..9f9f643
--- /dev/null
+++ b/app/src/main/java/me/capcom/smsgateway/modules/receiver/Module.kt
@@ -0,0 +1,10 @@
+package me.capcom.smsgateway.modules.receiver
+
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.module
+
+val receiverModule = module {
+ singleOf(::ReceiverService)
+}
+
+val MODULE_NAME = "receiver"
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt b/app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt
new file mode 100644
index 0000000..6f63649
--- /dev/null
+++ b/app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt
@@ -0,0 +1,134 @@
+package me.capcom.smsgateway.modules.receiver
+
+import android.content.Context
+import android.os.Build
+import android.provider.Telephony
+import me.capcom.smsgateway.helpers.SubscriptionsHelper
+import me.capcom.smsgateway.modules.logs.LogsService
+import me.capcom.smsgateway.modules.logs.db.LogEntry
+import me.capcom.smsgateway.modules.receiver.data.InboxMessage
+import me.capcom.smsgateway.modules.webhooks.WebHooksService
+import me.capcom.smsgateway.modules.webhooks.domain.WebHookEvent
+import me.capcom.smsgateway.modules.webhooks.payload.SmsEventPayload
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import java.util.Date
+
+class ReceiverService : KoinComponent {
+ private val webHooksService: WebHooksService by inject()
+ private val logsService: LogsService by inject()
+
+ fun export(context: Context, period: Pair) {
+ logsService.insert(
+ LogEntry.Priority.DEBUG,
+ MODULE_NAME,
+ "ReceiverService::export - start",
+ mapOf("period" to period)
+ )
+
+ select(context, period)
+ .forEach {
+ process(context, it)
+ }
+
+ logsService.insert(
+ LogEntry.Priority.DEBUG,
+ MODULE_NAME,
+ "ReceiverService::export - end",
+ mapOf("period" to period)
+ )
+ }
+
+ fun process(context: Context, message: InboxMessage) {
+ logsService.insert(
+ LogEntry.Priority.DEBUG,
+ MODULE_NAME,
+ "ReceiverService::process - message received",
+ mapOf("message" to message)
+ )
+
+ val event = SmsEventPayload.SmsReceived(
+ messageId = message.hashCode().toUInt().toString(16),
+ message = message.body,
+ phoneNumber = message.address,
+ simNumber = message.subscriptionId?.let {
+ SubscriptionsHelper.getSimSlotIndex(
+ context,
+ it
+ )
+ }?.let { it + 1 },
+ receivedAt = message.date,
+ )
+
+ webHooksService.emit(WebHookEvent.SmsReceived, event)
+
+ logsService.insert(
+ LogEntry.Priority.DEBUG,
+ MODULE_NAME,
+ "ReceiverService::process - message processed",
+ mapOf("event" to event)
+ )
+ }
+
+ fun select(context: Context, period: Pair): List {
+ logsService.insert(
+ LogEntry.Priority.DEBUG,
+ MODULE_NAME,
+ "ReceiverService::select - start",
+ mapOf("period" to period)
+ )
+
+ val projection = mutableListOf(
+ Telephony.Sms._ID,
+ Telephony.Sms.ADDRESS,
+ Telephony.Sms.DATE,
+ Telephony.Sms.BODY,
+ )
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
+ projection += Telephony.Sms.SUBSCRIPTION_ID
+ }
+
+ val selection = "${Telephony.Sms.DATE} >= ? AND ${Telephony.Sms.DATE} <= ?"
+ val selectionArgs = arrayOf(
+ period.first.time.toString(),
+ period.second.time.toString()
+ )
+ val sortOrder = Telephony.Sms.DATE
+
+ val contentResolver = context.contentResolver
+ val cursor = contentResolver.query(
+ Telephony.Sms.Inbox.CONTENT_URI,
+ projection.toTypedArray(),
+ selection,
+ selectionArgs,
+ sortOrder
+ )
+
+ val messages = mutableListOf()
+
+ cursor?.use { cursor ->
+ while (cursor.moveToNext()) {
+ messages.add(
+ InboxMessage(
+ address = cursor.getString(1),
+ date = Date(cursor.getLong(2)),
+ body = cursor.getString(3),
+ subscriptionId = when {
+ projection.size > 4 -> cursor.getInt(4).takeIf { it >= 0 }
+ else -> null
+ }
+ )
+ )
+ }
+ }
+
+ logsService.insert(
+ LogEntry.Priority.DEBUG,
+ MODULE_NAME,
+ "ReceiverService::select - end",
+ mapOf("messages" to messages.size)
+ )
+
+ return messages
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt b/app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt
new file mode 100644
index 0000000..7eab41d
--- /dev/null
+++ b/app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt
@@ -0,0 +1,29 @@
+package me.capcom.smsgateway.modules.receiver.data
+
+import java.util.Date
+
+data class InboxMessage(
+ val address: String,
+ val body: String,
+ val date: Date,
+ val subscriptionId: Int?
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is InboxMessage) return false
+
+ if (address != other.address) return false
+ if (body != other.body) return false
+ if (date != other.date) return false
+ if (subscriptionId != other.subscriptionId) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = address.hashCode()
+ result = 31 * result + body.hashCode()
+ result = 31 * result + date.hashCode()
+ result = 31 * result + (subscriptionId ?: 0)
+ return result
+ }
+}
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/receiver/events/MessageReceivedEvent.kt b/app/src/main/java/me/capcom/smsgateway/modules/receiver/events/MessageReceivedEvent.kt
deleted file mode 100644
index a3be6a4..0000000
--- a/app/src/main/java/me/capcom/smsgateway/modules/receiver/events/MessageReceivedEvent.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package me.capcom.smsgateway.modules.receiver.events
-
-import me.capcom.smsgateway.modules.events.AppEvent
-import java.util.Date
-
-class MessageReceivedEvent(
- val message: String,
- val phoneNumber: String,
- val receivedAt: Date,
- val simNumber: Int?,
-) : AppEvent(
- name = NAME
-) {
- companion object {
- const val NAME = "MessageReceivedEvent"
- }
-}
diff --git a/app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/SmsEventPayload.kt b/app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/SmsEventPayload.kt
index 92ec291..85bcbf1 100644
--- a/app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/SmsEventPayload.kt
+++ b/app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/SmsEventPayload.kt
@@ -28,4 +28,12 @@ sealed class SmsEventPayload(
val failedAt: Date,
val reason: String,
) : SmsEventPayload(messageId, phoneNumber, simNumber)
+
+ class SmsReceived(
+ messageId: String,
+ phoneNumber: String,
+ simNumber: Int?,
+ val message: String,
+ val receivedAt: Date,
+ ) : SmsEventPayload(messageId, phoneNumber, simNumber)
}
\ No newline at end of file
diff --git a/app/src/main/java/me/capcom/smsgateway/services/PushService.kt b/app/src/main/java/me/capcom/smsgateway/services/PushService.kt
index 47b1279..0cbf9cd 100644
--- a/app/src/main/java/me/capcom/smsgateway/services/PushService.kt
+++ b/app/src/main/java/me/capcom/smsgateway/services/PushService.kt
@@ -18,11 +18,14 @@ import me.capcom.smsgateway.modules.gateway.workers.RegistrationWorker
import me.capcom.smsgateway.modules.gateway.workers.WebhooksUpdateWorker
import me.capcom.smsgateway.modules.push.Event
import me.capcom.smsgateway.modules.push.events.PushMessageEnqueuedEvent
+import me.capcom.smsgateway.modules.push.payloads.MessagesExportRequestedPayload
+import me.capcom.smsgateway.modules.receiver.ReceiverService
import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
import org.koin.core.component.inject
class PushService : FirebaseMessagingService(), KoinComponent {
- private val settingsHelper by lazy { SettingsHelper(this) }
+ private val settingsHelper by inject()
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val eventBus by inject()
@@ -38,9 +41,22 @@ class PushService : FirebaseMessagingService(), KoinComponent {
Log.d(this.javaClass.name, message.data.toString())
val event = message.data["event"]?.let { Event.valueOf(it) } ?: Event.MessageEnqueued
+ val data = message.data["data"]
when (event) {
Event.MessageEnqueued -> scope.launch { eventBus.emit(PushMessageEnqueuedEvent()) }
Event.WebhooksUpdated -> WebhooksUpdateWorker.start(this)
+ Event.MessagesExportRequested -> data
+ ?.let {
+ MessagesExportRequestedPayload.from(
+ data
+ )
+ }
+ ?.let { payload ->
+ get().export(
+ this,
+ payload.since to payload.until
+ )
+ }
}
} catch (e: Throwable) {
e.printStackTrace()
diff --git a/app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt b/app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt
index 759e07a..7b96817 100644
--- a/app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt
+++ b/app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt
@@ -222,9 +222,10 @@ class HomeFragment : Fragment() {
private fun requestPermissionsAndStart() {
val permissionsRequired =
listOf(
- Manifest.permission.SEND_SMS,
Manifest.permission.READ_PHONE_STATE,
+ Manifest.permission.READ_SMS,
Manifest.permission.RECEIVE_SMS,
+ Manifest.permission.SEND_SMS,
)
.filter {
ContextCompat.checkSelfPermission(
@@ -261,7 +262,11 @@ class HomeFragment : Fragment() {
// app.
Log.d(javaClass.name, "Permissions granted")
} else {
- Toast.makeText(requireContext(), "Not all permissions granted", Toast.LENGTH_SHORT)
+ Toast.makeText(
+ requireContext(),
+ "Not all permissions granted, some features may not work",
+ Toast.LENGTH_SHORT
+ )
.show()
}
diff --git a/docs/api/swagger.json b/docs/api/swagger.json
index d170428..6974857 100644
--- a/docs/api/swagger.json
+++ b/docs/api/swagger.json
@@ -25,7 +25,7 @@
}
],
"paths": {
- "/message": {
+ "/messages": {
"post": {
"tags": [
"Messages"
@@ -75,7 +75,7 @@
]
}
},
- "/message/{id}": {
+ "/messages/{id}": {
"get": {
"tags": [
"Messages"
@@ -417,6 +417,74 @@
}
]
}
+ },
+ "/messages/inbox/export": {
+ "post": {
+ "summary": "Request inbox messages export",
+ "description": "Initiates process of inbox messages export via webhooks. For each message the `sms:received` webhook will be triggered. The webhooks will be triggered without specific order.",
+ "operationId": "post-messages-inbox-export",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "since": {
+ "type": "string",
+ "description": "The start of the time range to export.",
+ "format": "date-time",
+ "writeOnly": true
+ },
+ "until": {
+ "type": "string",
+ "description": "The end of the time range to export",
+ "format": "date-time",
+ "writeOnly": true
+ },
+ "deviceId": {
+ "type": "string",
+ "description": "The ID of the device to export messages for. Not required for Local mode."
+ }
+ },
+ "required": [
+ "since",
+ "until",
+ "deviceId"
+ ]
+ }
+ }
+ }
+ },
+ "responses": {
+ "202": {
+ "description": "Accepted"
+ },
+ "400": {
+ "$ref": "#/components/responses/ErrorResponse"
+ },
+ "401": {
+ "$ref": "#/components/responses/ErrorResponse"
+ },
+ "500": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ },
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "servers": [
+ {
+ "url": "http://device-ip:8080",
+ "description": "Local Server"
+ },
+ {
+ "url": "https://api.sms-gate.app/3rdparty/v1",
+ "description": "Cloud Server"
+ }
+ ]
+ }
}
},
"tags": [
@@ -581,9 +649,11 @@
"enum": [
"sms:received",
"sms:sent",
- "system:ping"
+ "system:ping",
+ "sms:delivered",
+ "sms:failed"
],
- "description": "The type of event being reported. For example, 'sms:received'.",
+ "description": "The type of event being reported",
"example": "sms:received"
},
"id": {
@@ -623,7 +693,9 @@
"enum": [
"sms:received",
"sms:sent",
- "system:ping"
+ "system:ping",
+ "sms:delivered",
+ "sms:failed"
],
"description": "The type of event that triggered the webhook."
},
@@ -636,6 +708,12 @@
{
"$ref": "#/components/schemas/SmsSentPayload"
},
+ {
+ "$ref": "#/components/schemas/SmsDeliveredPayload"
+ },
+ {
+ "$ref": "#/components/schemas/SmsFailedPayload"
+ },
{
"$ref": "#/components/schemas/SystemPingPayload"
}
@@ -644,55 +722,108 @@
}
},
"SmsReceivedPayload": {
- "type": "object",
"title": "SmsReceivedPayload",
"description": "Payload of `sms:received` event",
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/SmsEventPayload"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string",
+ "description": "The content of the SMS message received."
+ },
+ "receivedAt": {
+ "type": "string",
+ "description": "The timestamp when the SMS message was received.",
+ "format": "date-time"
+ }
+ }
+ }
+ ]
+ },
+ "SmsEventPayload": {
+ "type": "object",
+ "title": "SmsEventPayload",
+ "description": "Base payload of SMS-related events",
"properties": {
- "message": {
+ "messageId": {
"type": "string",
- "description": "The content of the SMS message received."
+ "description": "The unique identifier of the SMS message."
},
"phoneNumber": {
"type": "string",
- "description": "The phone number from which the SMS message was sent."
+ "description": "The phone number of the sender (for incoming messages) or recipient (for outgoing messages)."
},
"simNumber": {
"type": "integer",
"nullable": true,
- "description": "The SIM card number that received the SMS. May be null on some Android devices.",
- "minimum": 1
- },
- "receivedAt": {
- "type": "string",
- "description": "The timestamp when the SMS message was received.",
- "format": "date-time"
+ "description": "The SIM card number that sent the SMS. May be `null` if the SIM can't be determined or the default was used."
}
}
},
"SmsSentPayload": {
- "type": "object",
"title": "SmsSentPayload",
"description": "Payload of `sms:sent` event",
- "properties": {
- "id": {
- "type": "string",
- "description": "The unique identifier of the SMS message."
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/SmsEventPayload"
},
- "phoneNumber": {
- "type": "string",
- "description": "The phone number to which the SMS message was sent."
+ {
+ "type": "object",
+ "properties": {
+ "sentAt": {
+ "type": "string",
+ "description": "The timestamp when the SMS message was sent.",
+ "format": "date-time"
+ }
+ }
+ }
+ ]
+ },
+ "SmsDeliveredPayload": {
+ "title": "SmsDeliveredPayload",
+ "description": "Payload of `sms:delivered` event",
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/SmsEventPayload"
},
- "simNumber": {
- "type": "integer",
- "nullable": true,
- "description": "The SIM card number that sent the SMS. May be `null` if default SIM is used."
+ {
+ "type": "object",
+ "properties": {
+ "deliveredAt": {
+ "type": "string",
+ "description": "The timestamp when the SMS message was delivered.",
+ "format": "date-time"
+ }
+ }
+ }
+ ]
+ },
+ "SmsFailedPayload": {
+ "title": "SmsFailedPayload",
+ "description": "Payload of `sms:failed` event",
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/SmsEventPayload"
},
- "sentAt": {
- "type": "string",
- "description": "The timestamp when the SMS message was sent.",
- "format": "date-time"
+ {
+ "type": "object",
+ "properties": {
+ "failedAt": {
+ "type": "string",
+ "description": "The timestamp when the SMS message was failed.",
+ "format": "date-time"
+ },
+ "reason": {
+ "type": "string",
+ "description": "The reason description"
+ }
+ }
}
- }
+ ]
},
"SystemPingPayload": {
"type": "object",