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