Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] manual webhook trigger #164

Merged
merged 7 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/me/capcom/smsgateway/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,6 +38,7 @@ class App: Application() {
logsModule,
notificationsModule,
messagesModule,
receiverModule,
encryptionModule,
me.capcom.smsgateway.modules.gateway.gatewayModule,
healthModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()

Expand Down Expand Up @@ -162,98 +153,12 @@ class WebService : Service() {
call.respond(listOf(device))
}
}
route("/message") {
post {
val request = call.receive<PostMessageRequest>()
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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Date, Date>
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
}
}
Original file line number Diff line number Diff line change
@@ -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<PostMessageRequest>()
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<PostMessagesInboxExportRequest>().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}")
)
}
}
}
}
Loading
Loading