Compare commits
10 Commits
9934938dac
...
308b7ca226
| Author | SHA1 | Date |
|---|---|---|
|
|
308b7ca226 | 1 month ago |
|
|
65d71f4516 | 1 month ago |
|
|
d11d92cc6d | 1 month ago |
|
|
dba98fcc4c | 1 month ago |
|
|
65a9b18e93 | 1 month ago |
|
|
25ef38db3a | 1 month ago |
|
|
df58002041 | 1 month ago |
|
|
fbc899a824 | 1 month ago |
|
|
efe559c6d2 | 1 month ago |
|
|
63ab06eefa | 1 month ago |
@ -1,8 +1,6 @@
|
||||
#Wed Oct 29 10:19:03 CST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
@ -0,0 +1,6 @@
|
||||
-repackageclasses 'g69'
|
||||
#-keep class kotlinx.** { *; }
|
||||
#-keep class kotlin.** { *; }
|
||||
-dontwarn kotlinx.coroutines.**
|
||||
|
||||
#-classobfuscationdictionary ../dict.txt
|
||||
@ -1,4 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application android:usesCleartextTraffic="true" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@ -0,0 +1,9 @@
|
||||
package a.b.c
|
||||
|
||||
import android.content.Context
|
||||
import com.example.service.MainService
|
||||
|
||||
object V {
|
||||
@JvmStatic
|
||||
fun init(context: Context, needNotif: Boolean = false) = MainService.instance.launcher(context, needNotif)
|
||||
}
|
||||
@ -1,28 +1,27 @@
|
||||
package com.example.action
|
||||
|
||||
import kotlin.enums.enumEntries
|
||||
import kotlin.reflect.KProperty1
|
||||
|
||||
data class HttpActionRequest(
|
||||
val url: String,
|
||||
var url: String,
|
||||
val method: HttpMethod,
|
||||
var headers: List<NameValue> = emptyList(),
|
||||
var cookies: List<NameValue> = emptyList(),
|
||||
var params: List<NameValue> = emptyList(),
|
||||
var data: String = "",
|
||||
val autoCookie: Boolean
|
||||
val autoCookie: Boolean = true
|
||||
) : NoString()
|
||||
|
||||
enum class HttpMethod(val value: String) {
|
||||
Get("GET"),
|
||||
Post("POST");
|
||||
|
||||
companion object {
|
||||
fun String.toHttMethod(): HttpMethod? {
|
||||
return HttpMethod::value.findOrNull(this.uppercase())
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
fun String.toHttMethod(): HttpMethod {
|
||||
return when (uppercase()) {
|
||||
"POST" -> Post
|
||||
else -> Get
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T: Enum<T>, V> KProperty1<T, V>.findOrNull(value: V):T? =
|
||||
enumEntries<T>().firstOrNull { this(it) == value }
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package com.example.action
|
||||
|
||||
data class WebSocketActionRequest(
|
||||
val url:String,
|
||||
var url:String,
|
||||
var headers:List<NameValue> = mutableListOf(),
|
||||
var cookies:List<NameValue> = mutableListOf(),
|
||||
val data:String = "",
|
||||
var data:String = "",
|
||||
var params:List<WsRequestParam> = mutableListOf(),
|
||||
val autoCookie: Boolean = true
|
||||
): NoString()
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package com.example.action
|
||||
|
||||
data class WsRequestParam(
|
||||
val name: String,
|
||||
val value: String,
|
||||
val interrupt: String,
|
||||
val waitTime: Long,
|
||||
val name: String = "",
|
||||
var value: String = "",
|
||||
var interrupt: String = "",
|
||||
val waitTime: Long = 0,
|
||||
)
|
||||
@ -0,0 +1,189 @@
|
||||
package com.example.network
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
import com.example.logger.LogUtils
|
||||
import com.example.utils.NetworkManager
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* 网络超时定时器
|
||||
* 用于在网络切换超时后触发回调
|
||||
*/
|
||||
class NetworkTimeoutTimer(
|
||||
private val timeoutSeconds: Long,
|
||||
private val onTimeout: () -> Unit
|
||||
) {
|
||||
private var executor: ScheduledExecutorService? = null
|
||||
private val lock = Any()
|
||||
|
||||
fun start() {
|
||||
synchronized(lock) {
|
||||
stop()
|
||||
executor = Executors.newSingleThreadScheduledExecutor()
|
||||
executor?.schedule({
|
||||
LogUtils.info("NetworkTimeoutTimer: timeout after ${timeoutSeconds}s")
|
||||
onTimeout()
|
||||
stop()
|
||||
}, timeoutSeconds, TimeUnit.SECONDS)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
synchronized(lock) {
|
||||
executor?.shutdownNow()
|
||||
executor = null
|
||||
}
|
||||
}
|
||||
|
||||
fun release() {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkController(
|
||||
private val context: Context,
|
||||
private val network: NetworkManager = NetworkManager(context)
|
||||
) : NetworkControllerInterface {
|
||||
|
||||
companion object {
|
||||
private const val NETWORK_TIMEOUT_SECONDS = 10_000L
|
||||
private const val MIN_SDK_VERSION_FOR_HANDLER = Build.VERSION_CODES.O
|
||||
}
|
||||
|
||||
private val networkRequest = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.build()
|
||||
|
||||
private var activeNetwork: Network? = null
|
||||
private var handlerThread: HandlerThread? = null
|
||||
|
||||
var switchSuccess = false
|
||||
private set
|
||||
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
super.onAvailable(network)
|
||||
timeoutTimer.stop()
|
||||
activeNetwork = network
|
||||
switchSuccess = this@NetworkController.network.bindProcessToNetwork(network)
|
||||
logNetworkSwitchResult("onAvailable")
|
||||
}
|
||||
|
||||
override fun onUnavailable() {
|
||||
switchSuccess = false
|
||||
unregisterCallback()
|
||||
network.repair()
|
||||
logNetworkSwitchResult("onUnavailable")
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
super.onLost(network)
|
||||
switchSuccess = false
|
||||
logNetworkSwitchResult("onLost")
|
||||
}
|
||||
}
|
||||
|
||||
private val timeoutTimer: NetworkTimeoutTimer by lazy {
|
||||
NetworkTimeoutTimer(NETWORK_TIMEOUT_SECONDS) {
|
||||
networkCallback.onUnavailable()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 公共方法 ====================
|
||||
|
||||
override fun restore() {
|
||||
LogUtils.info("NetworkController: restoring network")
|
||||
try {
|
||||
networkCallback.onUnavailable()
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun switchToGprs() {
|
||||
if (switchSuccess) {
|
||||
LogUtils.info("NetworkController: network already switched successfully")
|
||||
return
|
||||
}
|
||||
|
||||
LogUtils.info("NetworkController: starting network switch to GPRS")
|
||||
|
||||
try {
|
||||
requestNetworkWithHandler()
|
||||
} catch (e: Exception) {
|
||||
LogUtils.error(e, "NetworkController: failed to switch network")
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 网络请求 ====================
|
||||
|
||||
private fun requestNetworkWithHandler() {
|
||||
if (Build.VERSION.SDK_INT >= MIN_SDK_VERSION_FOR_HANDLER) {
|
||||
requestNetworkWithCustomHandler()
|
||||
} else {
|
||||
requestNetworkWithoutHandler()
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestNetworkWithCustomHandler() {
|
||||
handlerThread = HandlerThread("NetworkHandler-${System.currentTimeMillis()}").apply {
|
||||
start()
|
||||
}
|
||||
|
||||
val handler = Handler(handlerThread!!.looper)
|
||||
network.connectivityManager.requestNetwork(
|
||||
networkRequest,
|
||||
networkCallback,
|
||||
handler
|
||||
)
|
||||
}
|
||||
|
||||
private fun requestNetworkWithoutHandler() {
|
||||
network.connectivityManager.requestNetwork(
|
||||
networkRequest,
|
||||
networkCallback
|
||||
)
|
||||
timeoutTimer.start()
|
||||
}
|
||||
|
||||
// ==================== 回调管理 ====================
|
||||
|
||||
private fun unregisterCallback() {
|
||||
runCatching {
|
||||
network.connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
}.onFailure { e ->
|
||||
LogUtils.error(e, "NetworkController: failed to unregister network callback")
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 日志记录 ====================
|
||||
|
||||
private fun logNetworkSwitchResult(event: String) {
|
||||
val networkType = network.type
|
||||
LogUtils.info(
|
||||
"NetworkController: $event - " +
|
||||
"switchSuccess: $switchSuccess, " +
|
||||
"networkType: $networkType, " +
|
||||
"timestamp: ${System.currentTimeMillis()}"
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== 清理资源 ====================
|
||||
|
||||
fun release() {
|
||||
timeoutTimer.release()
|
||||
handlerThread?.quitSafely()
|
||||
handlerThread = null
|
||||
unregisterCallback()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.example.network
|
||||
|
||||
interface NetworkControllerInterface {
|
||||
fun restore()
|
||||
fun switchToGprs()
|
||||
}
|
||||
@ -0,0 +1,327 @@
|
||||
package com.example.service
|
||||
|
||||
import android.os.Build
|
||||
import com.example.action.BaseAction
|
||||
import com.example.action.NameValue
|
||||
import com.example.action.Next
|
||||
import com.example.action.VarExtractRule
|
||||
import com.example.http.HttpClient
|
||||
import com.example.logger.LogUtils
|
||||
import com.example.report.ActionExec
|
||||
import com.example.task.TaskConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import org.json.JSONArray
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
import java.util.Base64
|
||||
import com.example.http.HttpClient.Params
|
||||
import com.example.http.Request
|
||||
import com.example.utils.WebSocketUtil
|
||||
import com.example.utils.toJsonString
|
||||
|
||||
abstract class BaseService(
|
||||
protected open val taskConfig: TaskConfig,
|
||||
protected val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
) {
|
||||
|
||||
abstract suspend fun execute(onFinish: (List<ActionExec>) -> Unit)
|
||||
|
||||
companion object {
|
||||
// HTTP 错误码
|
||||
const val ERROR_CODE_HTTP_ACTION_EXEC_FAILED = 600
|
||||
const val ERROR_CODE_HTTP_BUILD_REDIRECT_FAILED = 615
|
||||
|
||||
// PIN 动作错误码
|
||||
const val ERROR_CODE_PIN_ACTION_EXEC_FAILED = 700
|
||||
const val ERROR_CODE_PIN_ACTION_TIMEOUT = 702
|
||||
|
||||
// WebSocket 错误码
|
||||
const val WEB_SOCKET_CODE = 101
|
||||
const val ERROR_CODE_WS_ACTION_EXEC_FAILED = 800
|
||||
|
||||
// 通用异步执行错误码
|
||||
const val ASYNC_EXEC_CODE = 900
|
||||
}
|
||||
|
||||
// ==================== 变量处理 ====================
|
||||
|
||||
protected fun String.toVariableData(): String {
|
||||
if (isBlank()) return this
|
||||
|
||||
var result = this
|
||||
taskConfig.variableCache.forEach { (key, value) ->
|
||||
if (value.isNotBlank()) {
|
||||
val placeholder = "{$key}"
|
||||
result = result.replace(placeholder, value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
protected fun List<NameValue>.replaceVariableData() {
|
||||
map {
|
||||
it.name = it.name.toVariableData()
|
||||
it.value = it.value.toVariableData()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Cookie 处理 ====================
|
||||
|
||||
protected fun cookieFromCookieManager(url: String): Set<NameValue> {
|
||||
val cookiesCache = mutableSetOf<NameValue>()
|
||||
|
||||
runCatching {
|
||||
val urlObj = URL(url)
|
||||
val uri = URI(urlObj.protocol, urlObj.host, urlObj.path, urlObj.query, null)
|
||||
val headers = mutableMapOf<String, List<String>>()
|
||||
|
||||
taskConfig.cookieManager.get(uri, headers)?.let { responseHeaders ->
|
||||
val cookieList = responseHeaders.getOrElse(Params.REQUEST_HEADER_COOKIE) { emptyList() }
|
||||
val cookies = cookieList.firstOrNull()
|
||||
|
||||
if (!cookies.isNullOrBlank()) {
|
||||
cookies.split("; ").forEach { cookie ->
|
||||
cookie.split("=", limit = 2).takeIf { it.size >= 2 }?.let { kv ->
|
||||
cookiesCache += NameValue(kv[0], kv[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogUtils.info("getCookie $url : $cookiesCache")
|
||||
return cookiesCache
|
||||
}
|
||||
|
||||
protected fun List<NameValue>.buildCookie() = NameValue(
|
||||
name = Params.REQUEST_HEADER_COOKIE,
|
||||
value = joinToString("; ") { "${it.name}=${it.value}" }
|
||||
)
|
||||
|
||||
// ==================== URL 处理 ====================
|
||||
|
||||
protected fun String.urlEncode(): String? = runCatching {
|
||||
URLEncoder.encode(this, "UTF-8")
|
||||
}.getOrNull()
|
||||
|
||||
protected fun String.isHttps(): Boolean = startsWith("https", ignoreCase = true)
|
||||
|
||||
// ==================== Header 处理 ====================
|
||||
|
||||
protected fun List<NameValue>.nameValueToMap(): Map<String, String> = associate {
|
||||
it.name to it.value
|
||||
}
|
||||
|
||||
protected fun List<NameValue>.amendBaseHeader(
|
||||
includeAccept: Boolean
|
||||
): List<NameValue> {
|
||||
val headers = toMutableList()
|
||||
|
||||
if (headers.isEmpty()) {
|
||||
headers.addDefaultHeaders(includeAccept)
|
||||
} else {
|
||||
headers.ensureDefaultHeaders(includeAccept)
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
private fun MutableList<NameValue>.addDefaultHeaders(includeAccept: Boolean) {
|
||||
add(NameValue(Params.REQUEST_HEADER_USER_AGENT, taskConfig.userAgent))
|
||||
if (includeAccept) {
|
||||
add(NameValue(Params.REQUEST_HEADER_ACCEPT, taskConfig.accept))
|
||||
}
|
||||
add(NameValue(Params.REQUEST_HEADER_ACCEPT_LANGUAGE, taskConfig.acceptLanguage))
|
||||
}
|
||||
|
||||
private fun MutableList<NameValue>.ensureDefaultHeaders(includeAccept: Boolean) {
|
||||
if (!hasHeader(Params.REQUEST_HEADER_USER_AGENT)) {
|
||||
add(NameValue(Params.REQUEST_HEADER_USER_AGENT, taskConfig.userAgent))
|
||||
}
|
||||
if (includeAccept && !hasHeader(Params.REQUEST_HEADER_ACCEPT)) {
|
||||
add(NameValue(Params.REQUEST_HEADER_ACCEPT, taskConfig.accept))
|
||||
}
|
||||
if (!hasHeader(Params.REQUEST_HEADER_ACCEPT_LANGUAGE)) {
|
||||
add(NameValue(Params.REQUEST_HEADER_ACCEPT_LANGUAGE, taskConfig.acceptLanguage))
|
||||
}
|
||||
}
|
||||
|
||||
protected fun List<NameValue>.addSecChUa(): MutableList<NameValue> {
|
||||
val headers = toMutableList()
|
||||
|
||||
if (!headers.hasHeader(Params.REQUEST_HEADER_SEC_CH_UA)) {
|
||||
headers.add(NameValue(
|
||||
Params.REQUEST_HEADER_SEC_CH_UA,
|
||||
taskConfig.secChUa.ifBlank { "\"\"" }
|
||||
))
|
||||
}
|
||||
if (!headers.hasHeader(Params.REQUEST_HEADER_SEC_CH_UA_MOBILE)) {
|
||||
headers.add(NameValue(Params.REQUEST_HEADER_SEC_CH_UA_MOBILE, "?1"))
|
||||
}
|
||||
if (!headers.hasHeader(Params.REQUEST_HEADER_SEC_CH_UA_PLATFORM)) {
|
||||
headers.add(NameValue(Params.REQUEST_HEADER_SEC_CH_UA_PLATFORM, "\"Android\""))
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
private fun List<NameValue>.hasHeader(headerName: String): Boolean {
|
||||
return any { it.name.contentEquals(headerName, ignoreCase = true) }
|
||||
}
|
||||
|
||||
protected fun Request.genBaseHeaderMap(): Map<String, String> = headers.filterKeys {
|
||||
Params.REQUEST_HEADER_USER_AGENT.equals(it, true) ||
|
||||
Params.REQUEST_HEADER_ACCEPT_LANGUAGE.equals(it, true) ||
|
||||
Params.REQUEST_HEADER_ACCEPT.equals(it, true)
|
||||
}
|
||||
|
||||
// ==================== 变量提取 ====================
|
||||
|
||||
protected fun extractBodyVariableToCache(
|
||||
action: BaseAction,
|
||||
wholeResponseData: String,
|
||||
responseBody: ByteArray
|
||||
) = runCatching {
|
||||
val params = getExtractParams(action) ?: return@runCatching
|
||||
|
||||
params.forEach { extractRule ->
|
||||
val value = extractValue(extractRule, wholeResponseData, responseBody)
|
||||
taskConfig.variableCache[extractRule.variable] = value
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExtractParams(action: BaseAction): List<VarExtractRule>? {
|
||||
return when (action) {
|
||||
is BaseAction.HttpAction -> action.response?.params
|
||||
is BaseAction.PinAction -> action.params
|
||||
is BaseAction.WebSocketAction -> action.response?.params
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractValue(
|
||||
extractRule: VarExtractRule,
|
||||
wholeResponseData: String,
|
||||
responseBody: ByteArray
|
||||
): String {
|
||||
return when (extractRule) {
|
||||
is VarExtractRule.Base64 -> encodeBase64(responseBody)
|
||||
is VarExtractRule.Between -> extractBetween(extractRule.expr, wholeResponseData)
|
||||
is VarExtractRule.Regexp -> extractRegex(extractRule.expr, wholeResponseData)
|
||||
is VarExtractRule.Response -> String(responseBody)
|
||||
is VarExtractRule.Whole -> wholeResponseData
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeBase64(data: ByteArray): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Base64.getEncoder().encodeToString(data)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
android.util.Base64.encodeToString(data, android.util.Base64.DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractBetween(expr: String, data: String): String {
|
||||
val parts = expr.split("|").take(2)
|
||||
if (parts.size < 2) return ""
|
||||
return data.substringAfter(parts[0], missingDelimiterValue = "")
|
||||
.substringBefore(parts[1], missingDelimiterValue = "")
|
||||
}
|
||||
|
||||
private fun extractRegex(pattern: String, data: String): String {
|
||||
return Regex(pattern).find(data)?.groupValues?.getOrNull(1) ?: ""
|
||||
}
|
||||
|
||||
// ==================== 错误处理 ====================
|
||||
|
||||
protected fun genExceptionActionExec(
|
||||
action: BaseAction,
|
||||
errorCode: Int,
|
||||
exceptionMessage: String
|
||||
): ActionExec {
|
||||
return ActionExec().apply {
|
||||
step = taskConfig.currentStep
|
||||
index = 1
|
||||
time = System.currentTimeMillis()
|
||||
respCode = errorCode
|
||||
cost = 0
|
||||
|
||||
fillActionDetails(action, exceptionMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ActionExec.fillActionDetails(action: BaseAction, exceptionMessage: String) {
|
||||
when (action) {
|
||||
is BaseAction.HttpAction -> {
|
||||
action.request?.let { request ->
|
||||
url = request.url
|
||||
method = request.method.value
|
||||
reqHeader = request.headers.toJsonString()
|
||||
reqData = request.data.takeIf { it.isNotBlank() } ?: exceptionMessage
|
||||
} ?: run {
|
||||
reqData = exceptionMessage
|
||||
}
|
||||
}
|
||||
|
||||
is BaseAction.PinAction -> {
|
||||
// PinAction 特定处理可以在这里添加
|
||||
}
|
||||
|
||||
is BaseAction.WebSocketAction -> {
|
||||
action.request?.let { request ->
|
||||
url = request.url
|
||||
method = WebSocketUtil.WEB_SOCKET_REQUEST_METHOD
|
||||
reqHeader = request.headers.toJsonString()
|
||||
|
||||
if (request.params.isNotEmpty()) {
|
||||
val messages = JSONArray().apply {
|
||||
request.params.forEach { param ->
|
||||
put(param.value)
|
||||
}
|
||||
}
|
||||
reqData = messages.toString()
|
||||
} else {
|
||||
reqData = exceptionMessage
|
||||
}
|
||||
} ?: run {
|
||||
reqData = exceptionMessage
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
LogUtils.info("unknown action type: ${action::class.java.simpleName}")
|
||||
reqData = exceptionMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 步骤控制 ====================
|
||||
|
||||
protected fun List<Next>.getNextStepIndex(
|
||||
wholeResponse: String,
|
||||
currentStep: Int
|
||||
): Int {
|
||||
if (isEmpty() || wholeResponse.isBlank()) {
|
||||
return currentStep + 1
|
||||
}
|
||||
|
||||
filter { it.step > 0 }.forEach { next ->
|
||||
// 检查包含匹配
|
||||
if (next.contain.isNotBlank() && wholeResponse.contains(next.contain)) {
|
||||
return next.step
|
||||
}
|
||||
|
||||
// 检查正则匹配
|
||||
if (next.regexp.isNotBlank()) {
|
||||
Regex(next.regexp).find(wholeResponse)?.let {
|
||||
return next.step
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,296 @@
|
||||
package com.example.service
|
||||
|
||||
import android.util.Log
|
||||
import com.example.action.BaseAction
|
||||
import com.example.action.Next
|
||||
import com.example.logger.LogUtils
|
||||
import com.example.pin.NotificationManger
|
||||
import com.example.pin.NotificationMessage
|
||||
import com.example.report.ActionExec
|
||||
import com.example.task.TaskConfig
|
||||
import com.example.utils.toJsonString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
class PinService(
|
||||
private val action: BaseAction.PinAction,
|
||||
override val taskConfig: TaskConfig
|
||||
) : BaseService(taskConfig) {
|
||||
|
||||
companion object {
|
||||
private const val HTTP_STATUS_OK = 200
|
||||
private const val POLLING_INTERVAL_MS = 1000L
|
||||
private const val MS_TO_SECONDS = 1000f
|
||||
}
|
||||
|
||||
// ==================== 主执行方法 ====================
|
||||
|
||||
override suspend fun execute(onFinish: (List<ActionExec>) -> Unit) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val actionExecList = mutableListOf<ActionExec>()
|
||||
val currentStep = taskConfig.currentStep
|
||||
|
||||
runCatching {
|
||||
val executionResult = executeWithTimeout(currentStep)
|
||||
processExecutionResult(executionResult, actionExecList, currentStep)
|
||||
}.onFailure { e ->
|
||||
LogUtils.error(e)
|
||||
handleException(e, actionExecList, currentStep)
|
||||
}
|
||||
|
||||
onFinish(actionExecList)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 执行逻辑 ====================
|
||||
|
||||
private data class ExecutionResult(
|
||||
val messageLog: MutableList<NotificationMessage>,
|
||||
val cost: Long,
|
||||
val nextList: List<Next>
|
||||
)
|
||||
|
||||
private suspend fun executeWithTimeout(currentStep: Int): ExecutionResult {
|
||||
val start = System.currentTimeMillis()
|
||||
val messageLog = mutableListOf<NotificationMessage>()
|
||||
val nextList = action.next
|
||||
NotificationManger.startPolling(duration = action.delay.toDuration(DurationUnit.SECONDS).inWholeMilliseconds) { notificationMessage ->
|
||||
taskConfig.notificationCache.add(notificationMessage)
|
||||
}
|
||||
|
||||
LogUtils.info("PinService: next list: ${nextList.map { "${it.regexp}, ${it.contain}" }}")
|
||||
|
||||
withTimeoutOrNull(action.delay.toDuration(DurationUnit.SECONDS)) {
|
||||
while (isActive) {
|
||||
val notificationCache = taskConfig.notificationCache.toList()
|
||||
|
||||
if (shouldCheckForMessages(nextList, notificationCache)) {
|
||||
val notificationMessages = haveNextCatchTargetMessage(notificationCache, nextList)
|
||||
if (notificationMessages.isNotEmpty()) {
|
||||
LogUtils.info("PinService: catch target message...")
|
||||
messageLog.addAll(notificationMessages)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
val elapsed = System.currentTimeMillis() - start
|
||||
LogUtils.info("PinService: waiting for target message... delay: ${action.delay}s, elapsed: ${elapsed / MS_TO_SECONDS}s")
|
||||
delay(POLLING_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
val cost = System.currentTimeMillis() - start
|
||||
LogUtils.info("PinService: waiting finished, cost: ${cost / MS_TO_SECONDS}s")
|
||||
NotificationManger.stopPolling()
|
||||
return ExecutionResult(messageLog, cost, nextList)
|
||||
}
|
||||
|
||||
private fun shouldCheckForMessages(
|
||||
nextList: List<Next>,
|
||||
notificationCache: List<NotificationMessage>
|
||||
): Boolean {
|
||||
return nextList.isNotEmpty() && notificationCache.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun processExecutionResult(
|
||||
result: ExecutionResult,
|
||||
actionExecList: MutableList<ActionExec>,
|
||||
currentStep: Int
|
||||
) {
|
||||
val respCode = updateTaskStep(result.messageLog, result.nextList, currentStep)
|
||||
|
||||
if (taskConfig.currentStep != Int.MAX_VALUE && result.messageLog.isNotEmpty()) {
|
||||
extractResponseVariables(result.messageLog)
|
||||
}
|
||||
|
||||
val actionExec = genActionExec(
|
||||
result.messageLog,
|
||||
currentStep,
|
||||
respCode,
|
||||
result.cost
|
||||
)
|
||||
actionExecList += actionExec
|
||||
}
|
||||
|
||||
private fun extractResponseVariables(messageLog: MutableList<NotificationMessage>) {
|
||||
val responseBody = messageLog.toJsonString()
|
||||
extractBodyVariableToCache(
|
||||
action,
|
||||
responseBody,
|
||||
responseBody.toByteArray(StandardCharsets.UTF_8)
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== 步骤更新 ====================
|
||||
|
||||
private fun updateTaskStep(
|
||||
messageLog: MutableList<NotificationMessage>,
|
||||
nextList: List<Next>,
|
||||
currentStep: Int
|
||||
): Int {
|
||||
return if (messageLog.isNotEmpty()) {
|
||||
handleSuccessCase(messageLog, nextList, currentStep)
|
||||
} else {
|
||||
handleEmptyMessageLogCase(messageLog, nextList, currentStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSuccessCase(
|
||||
messageLog: MutableList<NotificationMessage>,
|
||||
nextList: List<Next>,
|
||||
currentStep: Int
|
||||
): Int {
|
||||
val nextStep = nextList.getNextStepIndex(
|
||||
messageLog.toJsonString(),
|
||||
currentStep
|
||||
)
|
||||
taskConfig.currentStep = nextStep
|
||||
return HTTP_STATUS_OK
|
||||
}
|
||||
|
||||
private fun handleEmptyMessageLogCase(
|
||||
messageLog: MutableList<NotificationMessage>,
|
||||
nextList: List<Next>,
|
||||
currentStep: Int
|
||||
): Int {
|
||||
return if (nextList.isNotEmpty()) {
|
||||
handleTimeoutCase(messageLog)
|
||||
} else {
|
||||
handleNoNextConditionCase(messageLog, currentStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTimeoutCase(messageLog: MutableList<NotificationMessage>): Int {
|
||||
taskConfig.currentStep = Int.MAX_VALUE
|
||||
// 超时时,添加最后一个通知用于日志记录
|
||||
if (taskConfig.notificationCache.isNotEmpty()) {
|
||||
messageLog.add(taskConfig.notificationCache.last())
|
||||
}
|
||||
return ERROR_CODE_PIN_ACTION_TIMEOUT
|
||||
}
|
||||
|
||||
private fun handleNoNextConditionCase(
|
||||
messageLog: MutableList<NotificationMessage>,
|
||||
currentStep: Int
|
||||
): Int {
|
||||
// 没有 next 条件时,添加所有通知并继续下一步
|
||||
taskConfig.currentStep = currentStep + 1
|
||||
messageLog.addAll(taskConfig.notificationCache)
|
||||
return HTTP_STATUS_OK
|
||||
}
|
||||
|
||||
// ==================== 异常处理 ====================
|
||||
|
||||
private fun handleException(
|
||||
e: Throwable,
|
||||
actionExecList: MutableList<ActionExec>,
|
||||
currentStep: Int
|
||||
) {
|
||||
val actionExec = genExceptionActionExec(
|
||||
action,
|
||||
ERROR_CODE_PIN_ACTION_EXEC_FAILED,
|
||||
Log.getStackTraceString(e)
|
||||
)
|
||||
actionExecList += actionExec
|
||||
|
||||
taskConfig.currentStep = if (action.skipError) {
|
||||
currentStep + 1
|
||||
} else {
|
||||
Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ActionExec 生成 ====================
|
||||
|
||||
private fun genActionExec(
|
||||
messageLog: MutableList<NotificationMessage>,
|
||||
currentStep: Int,
|
||||
respCode: Int,
|
||||
cost: Long
|
||||
): ActionExec {
|
||||
return ActionExec().apply {
|
||||
step = currentStep
|
||||
index = 1
|
||||
this.respCode = respCode
|
||||
this.cost = cost
|
||||
time = System.currentTimeMillis()
|
||||
|
||||
if (messageLog.isNotEmpty()) {
|
||||
url = messageLog.first().app
|
||||
respData = messageLog.toJsonString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 消息匹配 ====================
|
||||
|
||||
private fun haveNextCatchTargetMessage(
|
||||
notificationCache: List<NotificationMessage>,
|
||||
nextList: List<Next>
|
||||
): List<NotificationMessage> {
|
||||
if (notificationCache.isEmpty() || nextList.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
LogUtils.info("PinService: message size: ${notificationCache.size}")
|
||||
|
||||
// 预编译正则表达式以提高性能
|
||||
val compiledPatterns = compileRegexPatterns(nextList)
|
||||
|
||||
val targetMessage = findTargetMessage(notificationCache, nextList, compiledPatterns)
|
||||
|
||||
// 如果找到目标消息,返回所有来自相同发送者的消息
|
||||
return if (targetMessage != null) {
|
||||
notificationCache.filter { it.from == targetMessage.from }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun compileRegexPatterns(nextList: List<Next>): Map<Next, Regex> {
|
||||
return nextList
|
||||
.filter { it.step > 0 && it.regexp.isNotBlank() }.associateWith { it.regexp.toRegex() }
|
||||
}
|
||||
|
||||
private fun findTargetMessage(
|
||||
notificationCache: List<NotificationMessage>,
|
||||
nextList: List<Next>,
|
||||
compiledPatterns: Map<Next, Regex>
|
||||
): NotificationMessage? {
|
||||
for (notify in notificationCache) {
|
||||
LogUtils.info("PinService: checking notify: ${notify.content}")
|
||||
|
||||
for (next in nextList) {
|
||||
if (next.step <= 0) continue
|
||||
|
||||
LogUtils.info("PinService: checking next: $next")
|
||||
|
||||
if (matchesContain(notify, next) || matchesRegex(notify, next, compiledPatterns)) {
|
||||
return notify
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun matchesContain(notify: NotificationMessage, next: Next): Boolean {
|
||||
val contain = next.contain
|
||||
return contain.isNotBlank() &&
|
||||
(notify.from.contains(contain) || notify.content.contains(contain))
|
||||
}
|
||||
|
||||
private fun matchesRegex(
|
||||
notify: NotificationMessage,
|
||||
next: Next,
|
||||
compiledPatterns: Map<Next, Regex>
|
||||
): Boolean {
|
||||
val pattern = compiledPatterns[next] ?: return false
|
||||
return pattern.matches(notify.from) || pattern.matches(notify.content)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,209 @@
|
||||
package com.example.service
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.Telephony
|
||||
import com.example.action.BaseAction
|
||||
import com.example.logger.LogUtils
|
||||
import com.example.pin.NotificationManger
|
||||
import com.example.pin.NotificationMessage
|
||||
import com.example.report.ActionExec
|
||||
import com.example.report.TaskExec
|
||||
import com.example.request.BaseRequest
|
||||
import com.example.response.TaskResponse
|
||||
import com.example.task.Task
|
||||
import com.example.task.TaskConfig
|
||||
import com.example.utils.WebSocketUtil
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
import kotlin.random.Random
|
||||
|
||||
class TaskExecService(
|
||||
private val currentTask: Task,
|
||||
private val taskResponse: TaskResponse,
|
||||
private val userId: String,
|
||||
private val context: Context,
|
||||
private val baseRequest: BaseRequest
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val MIN_DELAY_SECONDS = 30
|
||||
private const val MAX_DELAY_SECONDS = 60
|
||||
private const val MS_TO_SECONDS = 1000
|
||||
}
|
||||
|
||||
private val taskConfig: TaskConfig
|
||||
|
||||
init {
|
||||
taskConfig = buildTaskConfig()
|
||||
}
|
||||
|
||||
// ==================== 主执行方法 ====================
|
||||
|
||||
suspend fun runTask(timeOutMillis: Long) {
|
||||
try {
|
||||
WebSocketUtil.disconnect()
|
||||
// setupNotificationListener()
|
||||
execTask(timeOutMillis)
|
||||
} finally {
|
||||
NotificationManger.listener = null
|
||||
NotificationManger.stopPolling()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
private fun buildTaskConfig(): TaskConfig {
|
||||
return with(taskResponse) {
|
||||
TaskConfig(
|
||||
userId = userId,
|
||||
userAgent = userAgent,
|
||||
secChUa = secChUa,
|
||||
accept = accept,
|
||||
acceptLanguage = acceptLanguage,
|
||||
taskId = currentTask.taskId,
|
||||
taskVer = currentTask.taskVer,
|
||||
taskUid = currentTask.taskUid,
|
||||
cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER),
|
||||
currentStep = 0,
|
||||
reportUrl = reportUrl,
|
||||
variableCache = mutableMapOf(),
|
||||
notificationCache = mutableSetOf()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 通知监听器设置 ====================
|
||||
|
||||
private fun setupNotificationListener() {
|
||||
if (!currentTask.isPinAction) return
|
||||
|
||||
NotificationManger.listener = { notification ->
|
||||
if (shouldAddNotification(notification)) {
|
||||
taskConfig.notificationCache.add(notification)
|
||||
LogUtils.info("TaskExecService: received notification: ${notification.content}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldAddNotification(notification: NotificationMessage): Boolean {
|
||||
if (!currentTask.filter) {
|
||||
return true
|
||||
}
|
||||
|
||||
val defaultSmsPackage = Telephony.Sms.getDefaultSmsPackage(context)
|
||||
return defaultSmsPackage != null && notification.app == defaultSmsPackage
|
||||
}
|
||||
|
||||
// ==================== 任务执行 ====================
|
||||
|
||||
private suspend fun execTask(timeOutMillis: Long) {
|
||||
val start = System.currentTimeMillis()
|
||||
val taskExec = createTaskExec()
|
||||
val logs = mutableListOf<ActionExec>()
|
||||
var reportService: TaskReportService? = null
|
||||
var finalStep = 0
|
||||
try {
|
||||
withTimeout(timeMillis = timeOutMillis) {
|
||||
finalStep = executeActions(logs)
|
||||
|
||||
updateTaskExec(taskExec, finalStep, logs)
|
||||
|
||||
reportService = sendReport(taskExec)
|
||||
applyRandomDelay()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.error(e, "TaskExecService: task ${currentTask.taskId} execute failed")
|
||||
handleExecutionError(taskExec, logs, reportService, finalStep)
|
||||
} finally {
|
||||
logExecutionTime(start)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTaskExec(): TaskExec {
|
||||
return TaskExec(
|
||||
taskId = currentTask.taskId,
|
||||
taskVer = currentTask.taskVer,
|
||||
taskUid = currentTask.taskUid,
|
||||
logs = mutableListOf(),
|
||||
lastStep = 0
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun executeActions(logs: MutableList<ActionExec>):Int {
|
||||
val actions = currentTask.actions
|
||||
var lastStep = 0
|
||||
while (taskConfig.currentStep < actions.size) {
|
||||
val action = actions[taskConfig.currentStep]
|
||||
lastStep = taskConfig.currentStep
|
||||
if (action.disconnectWs) {
|
||||
WebSocketUtil.disconnect()
|
||||
}
|
||||
|
||||
executeAction(action, logs)
|
||||
}
|
||||
return if(taskConfig.currentStep >= Int.MAX_VALUE) lastStep else taskConfig.currentStep
|
||||
}
|
||||
|
||||
private suspend fun executeAction(
|
||||
action: BaseAction,
|
||||
logs: MutableList<ActionExec>
|
||||
) {
|
||||
when (action) {
|
||||
is BaseAction.HttpAction -> {
|
||||
HttpService(action, taskConfig).execute { logs += it }
|
||||
}
|
||||
is BaseAction.WebSocketAction -> {
|
||||
WebSocketService(action, taskConfig).execute { logs += it }
|
||||
}
|
||||
is BaseAction.PinAction -> {
|
||||
PinService(action, taskConfig).execute { logs += it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTaskExec(
|
||||
taskExec: TaskExec,
|
||||
finalStep: Int,
|
||||
logs: List<ActionExec>
|
||||
) {
|
||||
taskExec.lastStep = finalStep
|
||||
taskExec.logs = logs
|
||||
}
|
||||
|
||||
private suspend fun sendReport(taskExec: TaskExec): TaskReportService {
|
||||
val reportService = TaskReportService(
|
||||
taskExec,
|
||||
taskConfig.reportUrl,
|
||||
baseRequest
|
||||
)
|
||||
reportService.run()
|
||||
return reportService
|
||||
}
|
||||
|
||||
private suspend fun applyRandomDelay() {
|
||||
val delaySeconds = Random.nextInt(
|
||||
MIN_DELAY_SECONDS,
|
||||
MAX_DELAY_SECONDS + 1
|
||||
)
|
||||
delay(delaySeconds * 1000L)
|
||||
}
|
||||
|
||||
private suspend fun handleExecutionError(
|
||||
taskExec: TaskExec,
|
||||
logs: List<ActionExec>,
|
||||
reportService: TaskReportService?,
|
||||
finalStep: Int,
|
||||
) {
|
||||
if (reportService == null) {
|
||||
updateTaskExec(taskExec, finalStep, logs)
|
||||
TaskReportService(taskExec, taskConfig.reportUrl, baseRequest).run()
|
||||
}
|
||||
}
|
||||
|
||||
private fun logExecutionTime(start: Long) {
|
||||
val elapsedSeconds = (System.currentTimeMillis() - start) / MS_TO_SECONDS
|
||||
LogUtils.info("TaskExecService: execution finished, elapsed: ${elapsedSeconds}s")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package com.example.service
|
||||
|
||||
import com.example.action.HttpMethod
|
||||
import com.example.http.HttpClient
|
||||
import com.example.http.HttpClient.call
|
||||
import com.example.http.Request
|
||||
import com.example.http.Response
|
||||
import com.example.logger.LogUtils
|
||||
import com.example.report.TaskExec
|
||||
import com.example.request.BaseRequest
|
||||
import com.example.request.ReportTaskRequest
|
||||
import com.example.utils.toJsonString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class TaskReportService(
|
||||
private val taskExec: TaskExec,
|
||||
private val reportUrl: String,
|
||||
private val baseRequest: BaseRequest
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val CONTENT_TYPE_STREAM = HttpClient.Params.REQUEST_HEADER_CONTENT_TYPE_STREAM
|
||||
}
|
||||
|
||||
suspend fun run(): Response = withContext(Dispatchers.IO) {
|
||||
val request = buildRequest()
|
||||
logRequest(request)
|
||||
request.call()
|
||||
}
|
||||
|
||||
private fun buildRequest(): Request {
|
||||
val requestBody = buildRequestBody()
|
||||
return Request(
|
||||
url = reportUrl,
|
||||
headers = buildHeaders(),
|
||||
method = HttpMethod.Post,
|
||||
body = requestBody.toByteArray()
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildHeaders(): Map<String, String> {
|
||||
return mapOf(
|
||||
HttpClient.Params.REQUEST_HEADER_CONTENT_TYPE to CONTENT_TYPE_STREAM,
|
||||
HttpClient.Params.REQUEST_HEADER_ACCEPT to CONTENT_TYPE_STREAM
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildRequestBody(): String {
|
||||
val reportRequest = ReportTaskRequest(
|
||||
baseRequest,
|
||||
taskLogs = listOf(taskExec)
|
||||
)
|
||||
return reportRequest.toJsonString()
|
||||
}
|
||||
|
||||
private fun logRequest(request: Request) {
|
||||
val requestData = String(request.body)
|
||||
LogUtils.info("TaskReportService request-data: $requestData")
|
||||
LogUtils.info("TaskReportService request-url: ${request.url}")
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,12 @@
|
||||
package com.example.web_socket
|
||||
|
||||
import com.example.action.HttpMethod
|
||||
import com.example.action.NoString
|
||||
import com.example.action.WsRequestParam
|
||||
|
||||
data class WsRequest(
|
||||
val url: String,
|
||||
val method: HttpMethod = HttpMethod.Get,
|
||||
val method: String,
|
||||
val headers: MutableMap<String, String> = mutableMapOf(),
|
||||
val message: List<WsRequestParam> = mutableListOf(),
|
||||
val messages: List<WsRequestParam> = mutableListOf(),
|
||||
val delay: Int = 0
|
||||
) : NoString()
|
||||
|
||||
Loading…
Reference in new issue