fix: harden captcha and long poll parsing

This commit is contained in:
Codex
2026-05-18 20:45:41 +03:00
parent 255a194c25
commit c18a7963bf
5 changed files with 124 additions and 167 deletions
@@ -21,12 +21,10 @@ class VkGroupsMap(
else map[abs(convo.id)] else map[abs(convo.id)]
fun messageActionGroup(message: VkMessage): VkGroupDomain? = fun messageActionGroup(message: VkMessage): VkGroupDomain? =
if (message.actionMemberId == null || message.actionMemberId!! >= 0) null message.actionMemberId?.takeIf { it < 0 }?.let { map[abs(it)] }
else map[abs(message.actionMemberId!!)]
fun messageActionGroup(message: VkMessageData): VkGroupDomain? = fun messageActionGroup(message: VkMessageData): VkGroupDomain? =
if (message.action?.memberId == null || message.action!!.memberId!! >= 0) null message.action?.memberId?.takeIf { it < 0 }?.let { map[abs(it)] }
else map[abs(message.action!!.memberId!!)]
fun messageGroup(message: VkMessage): VkGroupDomain? = fun messageGroup(message: VkMessage): VkGroupDomain? =
if (!message.isGroup()) null if (!message.isGroup()) null
@@ -20,12 +20,10 @@ class VkUsersMap(
else map[convo.id] else map[convo.id]
fun messageActionUser(message: VkMessage): VkUser? = fun messageActionUser(message: VkMessage): VkUser? =
if (message.actionMemberId == null || message.actionMemberId!! <= 0) null message.actionMemberId?.takeIf { it > 0 }?.let(map::get)
else map[message.actionMemberId]
fun messageActionUser(message: VkMessageData): VkUser? = fun messageActionUser(message: VkMessageData): VkUser? =
if (message.action?.memberId == null || message.action!!.memberId!! <= 0) null message.action?.memberId?.takeIf { it > 0 }?.let(map::get)
else map[message.action!!.memberId]
fun messageUser(message: VkMessage): VkUser? = fun messageUser(message: VkMessage): VkUser? =
if (!message.isUser()) null if (!message.isUser()) null
@@ -21,8 +21,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
internal class LongPollEventParser( internal class LongPollEventParser(
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
@@ -35,7 +35,7 @@ internal class LongPollEventParser(
val eventId = event.first().asInt() val eventId = event.first().asInt()
when (val eventType = ApiEvent.parseOrNull(eventId)) { when (val eventType = ApiEvent.parseOrNull(eventId)) {
null -> Log.d("LongPollEventParser", "parseNextUpdate: unknownEvent: $event") null -> Unit
ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event) ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event)
ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event) ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event)
@@ -62,8 +62,6 @@ internal class LongPollEventParser(
} }
private fun parseMessageSetFlags(eventType: ApiEvent, event: List<Any>) { private fun parseMessageSetFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType: $event")
val cmId = event[1].asLong() val cmId = event[1].asLong()
val flags = event[2].asInt() val flags = event[2].asInt()
val peerId = event[3].asLong() val peerId = event[3].asLong()
@@ -128,8 +126,6 @@ internal class LongPollEventParser(
} }
private fun parseMessageClearFlags(eventType: ApiEvent, event: List<Any>) { private fun parseMessageClearFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType: $event")
val cmId = event[1].asLong() val cmId = event[1].asLong()
val flags = event[2].asInt() val flags = event[2].asInt()
val peerId = event[3].asLong() val peerId = event[3].asLong()
@@ -192,8 +188,6 @@ internal class LongPollEventParser(
} }
private fun parseMessageNew(eventType: ApiEvent, event: List<Any>) { private fun parseMessageNew(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType: $event")
val cmId = event[1].asLong() val cmId = event[1].asLong()
val peerId = event[4].asLong() val peerId = event[4].asLong()
@@ -223,8 +217,6 @@ internal class LongPollEventParser(
} }
private fun parseMessageEdit(eventType: ApiEvent, event: List<Any>) { private fun parseMessageEdit(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType: $event")
val cmId = event[1].asLong() val cmId = event[1].asLong()
val peerId = event[3].asLong() val peerId = event[3].asLong()
@@ -239,40 +231,28 @@ internal class LongPollEventParser(
} }
private fun parseMessageReadIncoming(eventType: ApiEvent, event: List<Any>) { private fun parseMessageReadIncoming(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType: $event") dispatchMessageRead(
val peerId = event[1].asLong() longPollEvent = LongPollEvent.INCOMING_MESSAGE_READ,
val cmId = event[2].asLong() parsedEvent = LongPollParsedEvent.IncomingMessageRead(
val unreadCount = event[3].asInt() peerId = event[1].asLong(),
cmId = event[2].asLong(),
dispatch( unreadCount = event[3].asInt()
LongPollEvent.INCOMING_MESSAGE_READ,
LongPollParsedEvent.IncomingMessageRead(
peerId = peerId,
cmId = cmId,
unreadCount = unreadCount
) )
) )
} }
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List<Any>) { private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType: $event") dispatchMessageRead(
val peerId = event[1].asLong() longPollEvent = LongPollEvent.OUTGOING_MESSAGE_READ,
val cmId = event[2].asLong() parsedEvent = LongPollParsedEvent.OutgoingMessageRead(
val unreadCount = event[3].asInt() peerId = event[1].asLong(),
cmId = event[2].asLong(),
dispatch( unreadCount = event[3].asInt()
LongPollEvent.OUTGOING_MESSAGE_READ,
LongPollParsedEvent.OutgoingMessageRead(
peerId = peerId,
cmId = cmId,
unreadCount = unreadCount
) )
) )
} }
private fun parseChatClearFlags(eventType: ApiEvent, event: List<Any>) { private fun parseChatClearFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType: $event")
val peerId = event[1].asLong() val peerId = event[1].asLong()
val flags = event[2].asInt() val flags = event[2].asInt()
@@ -284,23 +264,11 @@ internal class LongPollEventParser(
parsedFlags.forEach { flag -> parsedFlags.forEach { flag ->
when (flag) { when (flag) {
ConvoFlags.ARCHIVED -> { ConvoFlags.ARCHIVED -> {
val convo = loadConvo( handleArchivedChat(
peerId = peerId, peerId = peerId,
extended = true, archived = false,
fields = VkConstants.ALL_FIELDS eventsToSend = eventsToSend
) ?: return@forEach
val message = loadMessage(
peerId = peerId,
cmId = convo.lastCmId
) )
val eventToSend = LongPollParsedEvent.ChatArchived(
convo = convo.copy(lastMessage = message),
archived = false
)
eventsToSend += eventToSend
dispatch(LongPollEvent.CHAT_ARCHIVED, eventToSend)
} }
else -> Unit else -> Unit
@@ -312,8 +280,6 @@ internal class LongPollEventParser(
} }
private fun parseChatSetFlags(eventType: ApiEvent, event: List<Any>) { private fun parseChatSetFlags(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType: $event")
val peerId = event[1].asLong() val peerId = event[1].asLong()
val flags = event[2].asInt() val flags = event[2].asInt()
@@ -325,23 +291,11 @@ internal class LongPollEventParser(
parsedFlags.forEach { flag -> parsedFlags.forEach { flag ->
when (flag) { when (flag) {
ConvoFlags.ARCHIVED -> { ConvoFlags.ARCHIVED -> {
val convo = loadConvo( handleArchivedChat(
peerId = peerId, peerId = peerId,
extended = true, archived = true,
fields = VkConstants.ALL_FIELDS eventsToSend = eventsToSend
) ?: return@forEach
val message = loadMessage(
peerId = peerId,
cmId = convo.lastCmId
) )
val eventToSend = LongPollParsedEvent.ChatArchived(
convo = convo.copy(lastMessage = message),
archived = true
)
eventsToSend += eventToSend
dispatch(LongPollEvent.CHAT_ARCHIVED, eventToSend)
} }
else -> Unit else -> Unit
@@ -353,8 +307,6 @@ internal class LongPollEventParser(
} }
private fun parseMessagesDeleted(eventType: ApiEvent, event: List<Any>) { private fun parseMessagesDeleted(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType: $event")
val peerId = event[1].asLong() val peerId = event[1].asLong()
val cmId = event[2].asLong() val cmId = event[2].asLong()
@@ -368,8 +320,6 @@ internal class LongPollEventParser(
} }
private fun parseChatMajorChanged(eventType: ApiEvent, event: List<Any>) { private fun parseChatMajorChanged(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType: $event")
val peerId = event[1].asLong() val peerId = event[1].asLong()
val majorId = event[2].asInt() val majorId = event[2].asInt()
@@ -383,8 +333,6 @@ internal class LongPollEventParser(
} }
private fun parseChatMinorChanged(eventType: ApiEvent, event: List<Any>) { private fun parseChatMinorChanged(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType: $event")
val peerId = event[1].asLong() val peerId = event[1].asLong()
val minorId = event[2].asInt() val minorId = event[2].asInt()
@@ -398,8 +346,6 @@ internal class LongPollEventParser(
} }
private fun parseInteraction(eventType: ApiEvent, event: List<Any>) { private fun parseInteraction(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType: $event")
val interactionType = when (eventType) { val interactionType = when (eventType) {
ApiEvent.TYPING -> InteractionType.Typing ApiEvent.TYPING -> InteractionType.Typing
ApiEvent.AUDIO_MESSAGE_RECORDING -> InteractionType.VoiceMessage ApiEvent.AUDIO_MESSAGE_RECORDING -> InteractionType.VoiceMessage
@@ -438,8 +384,6 @@ internal class LongPollEventParser(
} }
private fun parseUnreadCounterUpdate(eventType: ApiEvent, event: List<Any>) { private fun parseUnreadCounterUpdate(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType $event")
val unreadCount = event[1].asInt() val unreadCount = event[1].asInt()
val unreadUnmutedCount = event[2].asInt() val unreadUnmutedCount = event[2].asInt()
val showOnlyMuted = event[3].asInt() == 1 val showOnlyMuted = event[3].asInt() == 1
@@ -463,8 +407,6 @@ internal class LongPollEventParser(
} }
private fun parseMessageUpdated(eventType: ApiEvent, event: List<Any>) { private fun parseMessageUpdated(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType $event")
val cmId = event[1].asLong() val cmId = event[1].asLong()
val peerId = event[4].asLong() val peerId = event[4].asLong()
@@ -479,8 +421,6 @@ internal class LongPollEventParser(
} }
private fun parseMessageCacheClear(eventType: ApiEvent, event: List<Any>) { private fun parseMessageCacheClear(eventType: ApiEvent, event: List<Any>) {
Log.d("LongPollEventParser", "$eventType $event")
val messageId = event[1].asLong() val messageId = event[1].asLong()
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
@@ -497,10 +437,10 @@ internal class LongPollEventParser(
peerId: Long? = null, peerId: Long? = null,
cmId: Long? = null, cmId: Long? = null,
messageId: Long? = null messageId: Long? = null
): VkMessage? = suspendCoroutine { continuation -> ): VkMessage? = suspendCancellableCoroutine { continuation ->
require((peerId != null && cmId != null) || messageId != null) require((peerId != null && cmId != null) || messageId != null)
coroutineScope.launch(Dispatchers.IO) { val job = coroutineScope.launch(Dispatchers.IO) {
messagesUseCase.getById( messagesUseCase.getById(
peerCmIds = null, peerCmIds = null,
peerId = peerId, peerId = peerId,
@@ -525,14 +465,18 @@ internal class LongPollEventParser(
) )
} }
} }
continuation.invokeOnCancellation {
job.cancel()
}
} }
private suspend fun loadConvo( private suspend fun loadConvo(
peerId: Long, peerId: Long,
extended: Boolean = false, extended: Boolean = false,
fields: String? = null fields: String? = null
): VkConvo? = suspendCoroutine { continuation -> ): VkConvo? = suspendCancellableCoroutine { continuation ->
coroutineScope.launch(Dispatchers.IO) { val job = coroutineScope.launch(Dispatchers.IO) {
convoUseCase.getById( convoUseCase.getById(
peerIds = listOf(peerId), peerIds = listOf(peerId),
extended = extended, extended = extended,
@@ -554,5 +498,40 @@ internal class LongPollEventParser(
) )
} }
} }
continuation.invokeOnCancellation {
job.cancel()
}
}
private suspend fun handleArchivedChat(
peerId: Long,
archived: Boolean,
eventsToSend: MutableList<LongPollParsedEvent>
) {
val convo = loadConvo(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
) ?: return
val message = loadMessage(
peerId = peerId,
cmId = convo.lastCmId
)
val eventToSend = LongPollParsedEvent.ChatArchived(
convo = convo.copy(lastMessage = message),
archived = archived
)
eventsToSend += eventToSend
dispatch(LongPollEvent.CHAT_ARCHIVED, eventToSend)
}
private fun dispatchMessageRead(
longPollEvent: LongPollEvent,
parsedEvent: LongPollParsedEvent
) {
dispatch(longPollEvent, parsedEvent)
} }
} }
@@ -22,22 +22,35 @@ class OAuthUseCaseImpl(
): Flow<State<AuthInfo>> = flow { ): Flow<State<AuthInfo>> = flow {
emit(State.Loading) emit(State.Loading)
val newState = oAuthRepository.auth( val newState = when (val authResult = oAuthRepository.auth(
login = login, login = login,
password = password, password = password,
forceSms = forceSms, forceSms = forceSms,
validationCode = validationCode, validationCode = validationCode,
captchaSid = captchaSid, captchaSid = captchaSid,
captchaKey = captchaKey captchaKey = captchaKey
).asState( )) {
successMapper = { is com.slack.eithernet.ApiResult.Success -> {
AuthInfo( val value = authResult.value
userId = it.userId!!, val userId = value.userId
accessToken = it.accessToken!!, val accessToken = value.accessToken
validationHash = it.validationHash!! val validationHash = value.validationHash
)
if (userId == null || accessToken == null || validationHash == null) {
State.Error.InternalError
} else {
State.Success(
AuthInfo(
userId = userId,
accessToken = accessToken,
validationHash = validationHash
)
)
}
} }
)
else -> authResult.asState()
}
emit(newState) emit(newState)
} }
@@ -1,90 +1,62 @@
package dev.meloda.fast.network.interceptor package dev.meloda.fast.network.interceptor
import android.util.Log
import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.datastore.CaptchaTokenResult import dev.meloda.fast.datastore.CaptchaTokenResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.json.JSONObject import org.json.JSONObject
import java.util.concurrent.Executors import java.io.IOException
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import kotlin.time.Duration.Companion.minutes
class Error14HandlingInterceptor( class Error14HandlingInterceptor : Interceptor {
// private val domains: Set<String> = emptySet(),
) : Interceptor {
private val cookie = AtomicReference<String?>(null) private val cookie = AtomicReference<String?>(null)
private val captchaMutex = Mutex()
private companion object {
private const val CAPTCHA_ERROR_CODE = 14
private const val CAPTCHA_ERROR_KIND = "need_captcha"
private val executor = Executors.newSingleThreadExecutor()
}
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().withCookie() val request = chain.request().withCookie()
val response = chain.proceed(request) val response = chain.proceed(request)
response.parseCookie() response.parseCookie()
if (request.shouldSkipCaptcha()) return response if (request.shouldSkipCaptcha()) return response
val redirectUri = response.getRedirectUri() ?: return response val redirectUri = response.getRedirectUri() ?: return response
val token = passCaptchaAndGetToken(redirectUri) val token = awaitCaptchaToken(redirectUri)
return chain.proceed(chain.request().withCookie().withSuccessToken(token)) return chain.proceed(chain.request().withCookie().withSuccessToken(token))
} }
private fun passCaptchaAndGetToken(redirectUri: String): String = synchronized(this) { private fun awaitCaptchaToken(redirectUri: String): String = runBlocking(Dispatchers.IO) {
val tokenResult = AtomicReference<Result<String>>(Result.failure(Exception("No result"))) captchaMutex.withLock {
AppSettings.setCaptchaResult(CaptchaTokenResult.Initial)
executor.submit {
AppSettings.setCaptchaRedirectUri(redirectUri) AppSettings.setCaptchaRedirectUri(redirectUri)
Log.d("Error14Interceptor", "passCaptchaAndGetToken: $redirectUri")
var job: Job? = null try {
job = AppSettings.getCaptchaResultFlow() withTimeout(CAPTCHA_TIMEOUT) {
.listenValue(CoroutineScope(Dispatchers.IO)) { AppSettings.getCaptchaResultFlow()
Log.d("Error14Interceptor", "passCaptchaAndGetToken: $it") .first { it != CaptchaTokenResult.Initial }
if (it != CaptchaTokenResult.Initial) { .toToken()
synchronized(tokenResult) {
Log.d(
"Error14Interceptor",
"passCaptchaAndGetToken: SYNCHRONIZED: $it"
)
tokenResult.set(wrapResult(it))
tokenResult.notifyAll()
job?.cancel()
Log.d(
"Error14Interceptor",
"passCaptchaAndGetToken: NULL RESULT"
)
AppSettings.setCaptchaResult(CaptchaTokenResult.Initial)
AppSettings.setCaptchaRedirectUri(null)
}
}
} }
} } finally {
synchronized(tokenResult) { AppSettings.setCaptchaResult(CaptchaTokenResult.Initial)
if (tokenResult.get().getOrNull() == null) { AppSettings.setCaptchaRedirectUri(null)
tokenResult.wait()
} }
Log.d("Error14Interceptor", "passCaptchaAndGetToken: GET VALUE")
tokenResult.get().getOrThrow()
} }
} }
private fun wrapResult(result: CaptchaTokenResult): Result<String> { private fun CaptchaTokenResult.toToken(): String = when (this) {
return when (result) { is CaptchaTokenResult.Success -> token
// TODO: 03/05/2026, Danil Nikolaev: check again? CaptchaTokenResult.Cancelled -> throw IOException("Captcha cancelled")
CaptchaTokenResult.Null -> Result.success("") CaptchaTokenResult.Null -> throw IOException("Captcha result is empty")
CaptchaTokenResult.Initial -> throw IllegalStateException("Captcha result not ready")
CaptchaTokenResult.Cancelled, CaptchaTokenResult.Initial -> Result.success("")
is CaptchaTokenResult.Success -> Result.success(result.token)
}
} }
private fun Request.withSuccessToken(token: String): Request { private fun Request.withSuccessToken(token: String): Request {
@@ -133,13 +105,10 @@ class Error14HandlingInterceptor(
private fun Request.withCookie(): Request { private fun Request.withCookie(): Request {
return newBuilder().apply { cookie.get()?.let { addHeader("Cookie", it) } }.build() return newBuilder().apply { cookie.get()?.let { addHeader("Cookie", it) } }.build()
} }
private companion object {
private const val CAPTCHA_ERROR_CODE = 14
private const val CAPTCHA_ERROR_KIND = "need_captcha"
private val CAPTCHA_TIMEOUT = 10.minutes
}
} }
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE")
private inline fun Any.wait() = (this as Object).wait()
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE")
private inline fun Any.notify() = (this as Object).notify()
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE")
private inline fun Any.notifyAll() = (this as Object).notifyAll()