package me.eternal.purrfect.core.features.impl import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import me.eternal.purrfect.common.data.MixerStoryType import me.eternal.purrfect.common.data.StoryData import me.eternal.purrfect.common.util.protobuf.ProtoEditor import me.eternal.purrfect.core.event.events.impl.NetworkApiRequestEvent import me.eternal.purrfect.core.features.Feature import java.nio.ByteBuffer import kotlin.coroutines.suspendCoroutine import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi class MixerStories : Feature("MixerStories") { @OptIn(ExperimentalEncodingApi::class) override fun init() { val disableDiscoverSections by context.config.global.disableStorySections fun canRemoveDiscoverSection(id: Int): Boolean { val storyType = MixerStoryType.fromIndex(id) return (storyType == MixerStoryType.SUBSCRIPTIONS && disableDiscoverSections.contains("following")) || (storyType == MixerStoryType.DISCOVER && disableDiscoverSections.contains("discover")) || (storyType == MixerStoryType.FRIENDS && disableDiscoverSections.contains("friends")) } context.event.subscribe(NetworkApiRequestEvent::class) { event -> fun cancelRequest() { runBlocking { suspendCoroutine { context.httpServer.ensureServerStarted()?.let { server -> event.url = "http://127.0.0.1:${server.port}" it.resumeWith(Result.success(Unit)) } ?: run { event.canceled = true it.resumeWith(Result.success(Unit)) } } } } if (event.url.endsWith("readreceipt-indexer/batchuploadreadreceipts")) { if (context.config.messaging.anonymousStoryViewing.get()) { cancelRequest() return@subscribe } if (!context.config.messaging.preventStoryRewatchIndicator.get()) return@subscribe event.hookRequestBuffer { buffer -> ProtoEditor(buffer).apply { edit { getOrNull(2)?.removeIf { (it.toReader().getVarInt(7, 4) ?: 0L) > 1L } } }.toByteArray() } return@subscribe } if (event.url.endsWith("df-mixer-prod/stories") || event.url.endsWith("df-mixer-prod/batch_stories") || event.url.endsWith("df-mixer-prod/soma/stories") || event.url.endsWith("df-mixer-prod/soma/batch_stories") ) { event.onSuccess { buffer -> val editor = ProtoEditor(buffer ?: return@onSuccess) editor.edit { editEach(3) { val sectionType = firstOrNull(10)?.toReader()?.getVarInt(1)?.toInt() ?: return@editEach edit(3) { removeIf(3) { wire -> val reader = wire.toReader() val storySubType = reader.getVarInt(23) val isSuggested = storySubType == 39L if (!isSuggested && sectionType == MixerStoryType.FRIENDS.index && context.config.experimental.storyLogger.get()) { val storyMap = mutableMapOf>() reader.followPath(36) { eachBuffer(1) data@{ val userId = getString(8, 1) ?: return@data storyMap.getOrPut(userId) { mutableListOf() }.add(StoryData( url = getString(2, 2)?.substringBefore("?") ?: return@data, postedAt = getVarInt(3) ?: -1L, createdAt = getVarInt(27) ?: -1L, key = Base64.decode(getString(2, 5) ?: return@data), iv = Base64.decode(getString(2, 4) ?: return@data) )) } } context.coroutineScope.launch { storyMap.forEach { (userId, stories) -> stories.forEach { story -> runCatching { context.bridgeClient.getMessageLogger().addStory(userId, story.url, story.postedAt, story.createdAt, story.key, story.iv) }.onFailure { context.log.error("Failed to log story", it) } } } } } isSuggested && disableDiscoverSections.contains("suggested_stories") } } if (canRemoveDiscoverSection(sectionType)) { remove(3) addBuffer(3, byteArrayOf()) } } } setArg(2, ByteBuffer.wrap(editor.toByteArray())) } return@subscribe } } } }