commit 5fd6aec2566b11ebc0581b1e036beafb11409027 parent bc015d5d6057ff2425ca0ff9798c46edf1352fb3 Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sun, 1 Oct 2023 02:06:05 +0200 refactor: event bus priority - message logger & e2ee optimization Diffstat:
11 files changed, 146 insertions(+), 124 deletions(-)
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventBus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventBus.kt @@ -15,18 +15,19 @@ interface IListener<T> { class EventBus( val context: ModContext ) { - private val subscribers = mutableMapOf<KClass<out Event>, MutableList<IListener<out Event>>>() + private val subscribers = mutableMapOf<KClass<out Event>, MutableMap<Int, IListener<out Event>>>() - fun <T : Event> subscribe(event: KClass<T>, listener: IListener<T>) { + fun <T : Event> subscribe(event: KClass<T>, listener: IListener<T>, priority: Int? = null) { if (!subscribers.containsKey(event)) { - subscribers[event] = mutableListOf() + subscribers[event] = sortedMapOf() } - subscribers[event]!!.add(listener) + val lastSubscriber = subscribers[event]?.keys?.lastOrNull() ?: 0 + subscribers[event]?.put(priority ?: (lastSubscriber + 1), listener) } - inline fun <T : Event> subscribe(event: KClass<T>, crossinline listener: (T) -> Unit) = subscribe(event, { true }, listener) + inline fun <T : Event> subscribe(event: KClass<T>, priority: Int? = null, crossinline listener: (T) -> Unit) = subscribe(event, { true }, priority, listener) - inline fun <T : Event> subscribe(event: KClass<T>, crossinline filter: (T) -> Boolean, crossinline listener: (T) -> Unit): () -> Unit { + inline fun <T : Event> subscribe(event: KClass<T>, crossinline filter: (T) -> Boolean, priority: Int? = null, crossinline listener: (T) -> Unit): () -> Unit { val obj = object : IListener<T> { override fun handle(event: T) { if (!filter(event)) return @@ -37,15 +38,12 @@ class EventBus( } } } - subscribe(event, obj) + subscribe(event, obj, priority) return { unsubscribe(event, obj) } } fun <T : Event> unsubscribe(event: KClass<T>, listener: IListener<T>) { - if (!subscribers.containsKey(event)) { - return - } - subscribers[event]!!.remove(listener) + subscribers[event]?.values?.remove(listener) } fun <T : Event> post(event: T, afterBlock: T.() -> Unit = {}): T? { @@ -55,7 +53,7 @@ class EventBus( event.context = context - subscribers[event::class]?.toTypedArray()?.forEach { listener -> + subscribers[event::class]?.toSortedMap()?.forEach { (_, listener) -> @Suppress("UNCHECKED_CAST") runCatching { (listener as IListener<T>).handle(event) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventDispatcher.kt @@ -9,6 +9,7 @@ import me.rhunk.snapenhance.core.event.events.impl.* import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.ktx.setObjectField import me.rhunk.snapenhance.core.util.snap.SnapWidgetBroadcastReceiverHelper +import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.data.wrapper.impl.MessageContent import me.rhunk.snapenhance.data.wrapper.impl.MessageDestinations import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID @@ -143,6 +144,14 @@ class EventDispatcher( } } + context.classCache.message.hookConstructor(HookStage.AFTER) { param -> + context.event.post( + BuildMessageEvent( + message = Message(param.thisObject()) + ) + ) + } + hookViewBinder() } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/BuildMessageEvent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/events/impl/BuildMessageEvent.kt @@ -0,0 +1,8 @@ +package me.rhunk.snapenhance.core.event.events.impl + +import me.rhunk.snapenhance.core.event.Event +import me.rhunk.snapenhance.data.wrapper.impl.Message + +class BuildMessageEvent( + val message: Message +): Event()+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/util/EvictingMap.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/util/EvictingMap.kt @@ -0,0 +1,5 @@ +package me.rhunk.snapenhance.core.util + +class EvictingMap<K, V>(private val maxSize: Int) : LinkedHashMap<K, V>(maxSize, 0.75f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>) = size > maxSize +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt @@ -66,17 +66,20 @@ enum class ContentType(val id: Int) { return values().firstOrNull { it.id == i } ?: UNKNOWN } - fun fromMessageContainer(protoReader: ProtoReader?): ContentType { - if (protoReader == null) return UNKNOWN - return when { - protoReader.containsPath(2) -> CHAT - protoReader.containsPath(11) -> SNAP - protoReader.containsPath(6) -> NOTE - protoReader.containsPath(3) -> EXTERNAL_MEDIA - protoReader.containsPath(4) -> STICKER - protoReader.containsPath(5) -> SHARE - protoReader.containsPath(7) -> EXTERNAL_MEDIA// story replies - else -> UNKNOWN + fun fromMessageContainer(protoReader: ProtoReader?): ContentType? { + if (protoReader == null) return null + return protoReader.run { + when { + contains(8) -> STATUS + contains(2) -> CHAT + contains(11) -> SNAP + contains(6) -> NOTE + contains(3) -> EXTERNAL_MEDIA + contains(4) -> STICKER + contains(5) -> SHARE + contains(7) -> EXTERNAL_MEDIA // story replies + else -> null + } } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/EndToEndEncryption.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/EndToEndEncryption.kt @@ -3,7 +3,6 @@ package me.rhunk.snapenhance.features.impl.experiments import android.annotation.SuppressLint import android.graphics.Canvas import android.graphics.Paint -import android.graphics.Rect import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.Shape import android.view.View @@ -14,23 +13,22 @@ import android.widget.Button import android.widget.TextView import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent +import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.event.events.impl.SendMessageWithContentEvent import me.rhunk.snapenhance.core.event.events.impl.UnaryCallEvent import me.rhunk.snapenhance.core.messaging.MessagingRuleType import me.rhunk.snapenhance.core.messaging.RuleState +import me.rhunk.snapenhance.core.util.EvictingMap import me.rhunk.snapenhance.core.util.ktx.getObjectField import me.rhunk.snapenhance.core.util.protobuf.ProtoEditor import me.rhunk.snapenhance.core.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.util.protobuf.ProtoWriter import me.rhunk.snapenhance.data.ContentType -import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.data.wrapper.impl.MessageContent import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.MessagingRuleFeature import me.rhunk.snapenhance.features.impl.Messaging -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.hookConstructor import me.rhunk.snapenhance.ui.ViewAppearanceHelper import me.rhunk.snapenhance.ui.addForegroundDrawable import me.rhunk.snapenhance.ui.removeForegroundDrawable @@ -51,6 +49,8 @@ class EndToEndEncryption : MessagingRuleFeature( const val ENCRYPTED_MESSAGE_ID = 3 } + private val decryptedMessageCache = EvictingMap<Long, Pair<ContentType, ByteArray>>(100) + private val pkRequests = mutableMapOf<Long, ByteArray>() private val secretResponses = mutableMapOf<Long, ByteArray>() private val encryptedMessages = mutableListOf<Long>() @@ -258,17 +258,8 @@ class EndToEndEncryption : MessagingRuleFeature( } } - private fun fixContentType(contentType: ContentType, message: ProtoReader): ContentType { - return when { - contentType == ContentType.EXTERNAL_MEDIA && message.containsPath(11) -> { - ContentType.SNAP - } - contentType == ContentType.SHARE && message.containsPath(2) -> { - ContentType.CHAT - } - else -> contentType - } - } + private fun fixContentType(contentType: ContentType?, message: ProtoReader) + = ContentType.fromMessageContainer(message) ?: contentType private fun hashParticipantId(participantId: String, salt: ByteArray): ByteArray { return MessageDigest.getInstance("SHA-256").apply { @@ -278,7 +269,21 @@ class EndToEndEncryption : MessagingRuleFeature( } private fun messageHook(conversationId: String, messageId: Long, senderId: String, messageContent: MessageContent) { + if (messageContent.contentType != ContentType.STATUS && decryptedMessageCache.containsKey(messageId)) { + val (contentType, buffer) = decryptedMessageCache[messageId]!! + messageContent.contentType = contentType + messageContent.content = buffer + return + } + val reader = ProtoReader(messageContent.content) + messageContent.contentType = fixContentType(messageContent.contentType!!, reader) + + fun setMessageContent(buffer: ByteArray) { + messageContent.content = buffer + messageContent.contentType = fixContentType(messageContent.contentType, ProtoReader(buffer)) + decryptedMessageCache[messageId] = messageContent.contentType!! to buffer + } fun replaceMessageText(text: String) { messageContent.content = ProtoWriter().apply { @@ -306,14 +311,18 @@ class EndToEndEncryption : MessagingRuleFeature( if (isMe) { if (conversationParticipants.isEmpty()) return@eachBuffer val participantId = conversationParticipants.firstOrNull { participantIdHash.contentEquals(hashParticipantId(it, iv)) } ?: return@eachBuffer - messageContent.content = e2eeInterface.decryptMessage(participantId, ciphertext, iv) + setMessageContent( + e2eeInterface.decryptMessage(participantId, ciphertext, iv) + ) encryptedMessages.add(messageId) return@eachBuffer } if (!participantIdHash.contentEquals(hashParticipantId(context.database.myUserId, iv))) return@eachBuffer - messageContent.content = e2eeInterface.decryptMessage(senderId, ciphertext, iv) + setMessageContent( + e2eeInterface.decryptMessage(senderId, ciphertext, iv) + ) encryptedMessages.add(messageId) } }.onFailure { @@ -414,7 +423,10 @@ class EndToEndEncryption : MessagingRuleFeature( } event.buffer = ProtoEditor(event.buffer).apply { - val contentType = fixContentType(ContentType.fromId(messageReader.getVarInt(2)?.toInt() ?: -1), messageReader.followPath(4) ?: return@apply) + val contentType = fixContentType( + ContentType.fromId(messageReader.getVarInt(2)?.toInt() ?: -1), + messageReader.followPath(4) ?: return@apply + ) ?: return@apply val messageContent = messageReader.getByteArray(4) ?: return@apply runCatching { @@ -460,8 +472,8 @@ class EndToEndEncryption : MessagingRuleFeature( override fun init() { if (!isEnabled) return - context.classCache.message.hookConstructor(HookStage.AFTER) { param -> - val message = Message(param.thisObject()) + context.event.subscribe(BuildMessageEvent::class, priority = 0) { event -> + val message = event.message val conversationId = message.messageDescriptor.conversationId.toString() messageHook( conversationId = conversationId, @@ -470,10 +482,6 @@ class EndToEndEncryption : MessagingRuleFeature( messageContent = message.messageContent ) - message.messageContent.contentType?.also { - message.messageContent.contentType = fixContentType(it, ProtoReader(message.messageContent.content)) - } - message.messageContent.instanceNonNull() .getObjectField("mQuotedMessage") ?.getObjectField("mContent") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt @@ -8,14 +8,13 @@ import android.os.DeadObjectException import com.google.gson.JsonObject import com.google.gson.JsonParser import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent +import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent +import me.rhunk.snapenhance.core.util.EvictingMap import me.rhunk.snapenhance.core.util.protobuf.ProtoReader import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.MessageState -import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker import me.rhunk.snapenhance.ui.addForegroundDrawable import me.rhunk.snapenhance.ui.removeForegroundDrawable import java.util.concurrent.Executors @@ -48,7 +47,7 @@ class MessageLogger : Feature("MessageLogger", private val cachedIdLinks = mutableMapOf<Long, Long>() // client id -> server id private val fetchedMessages = mutableListOf<Long>() // list of unique message ids - private val deletedMessageCache = mutableMapOf<Long, JsonObject>() // unique message id -> message json object + private val deletedMessageCache = EvictingMap<Long, JsonObject>(200) // unique message id -> message json object fun isMessageDeleted(conversationId: String, clientMessageId: Long) = makeUniqueIdentifier(conversationId, clientMessageId)?.let { deletedMessageCache.containsKey(it) } ?: false @@ -105,62 +104,57 @@ class MessageLogger : Feature("MessageLogger", }.also { context.log.verbose("Loaded ${fetchedMessages.size} cached messages in $it") } } - private fun processSnapMessage(messageInstance: Any) { - val message = Message(messageInstance) + override fun init() { + context.event.subscribe(BuildMessageEvent::class, { isEnabled }, priority = 1) { event -> + val messageInstance = event.message.instanceNonNull() + if (event.message.messageState != MessageState.COMMITTED) return@subscribe - if (message.messageState != MessageState.COMMITTED) return + cachedIdLinks[event.message.messageDescriptor.messageId] = event.message.orderKey + val conversationId = event.message.messageDescriptor.conversationId.toString() + //exclude messages sent by me + if (event.message.senderId.toString() == context.database.myUserId) return@subscribe - cachedIdLinks[message.messageDescriptor.messageId] = message.orderKey - val conversationId = message.messageDescriptor.conversationId.toString() - //exclude messages sent by me - if (message.senderId.toString() == context.database.myUserId) return + val uniqueMessageIdentifier = computeMessageIdentifier(conversationId, event.message.orderKey) - val uniqueMessageIdentifier = computeMessageIdentifier(conversationId, message.orderKey) + if (event.message.messageContent.contentType != ContentType.STATUS) { + if (fetchedMessages.contains(uniqueMessageIdentifier)) return@subscribe + fetchedMessages.add(uniqueMessageIdentifier) - if (message.messageContent.contentType != ContentType.STATUS) { - if (fetchedMessages.contains(uniqueMessageIdentifier)) return - fetchedMessages.add(uniqueMessageIdentifier) + threadPool.execute { + try { + messageLoggerInterface.addMessage(conversationId, uniqueMessageIdentifier, context.gson.toJson(messageInstance).toByteArray(Charsets.UTF_8)) + } catch (ignored: DeadObjectException) {} + } - threadPool.execute { - try { - messageLoggerInterface.addMessage(conversationId, uniqueMessageIdentifier, context.gson.toJson(messageInstance).toByteArray(Charsets.UTF_8)) - } catch (ignored: DeadObjectException) {} + return@subscribe } - return - } - - //query the deleted message - val deletedMessageObject: JsonObject = if (deletedMessageCache.containsKey(uniqueMessageIdentifier)) - deletedMessageCache[uniqueMessageIdentifier] - else { - messageLoggerInterface.getMessage(conversationId, uniqueMessageIdentifier)?.let { - JsonParser.parseString(it.toString(Charsets.UTF_8)).asJsonObject - } - } ?: return + //query the deleted message + val deletedMessageObject: JsonObject = if (deletedMessageCache.containsKey(uniqueMessageIdentifier)) + deletedMessageCache[uniqueMessageIdentifier] + else { + messageLoggerInterface.getMessage(conversationId, uniqueMessageIdentifier)?.let { + JsonParser.parseString(it.toString(Charsets.UTF_8)).asJsonObject + } + } ?: return@subscribe - val messageJsonObject = deletedMessageObject.asJsonObject + val messageJsonObject = deletedMessageObject.asJsonObject - //if the message is a snap make it playable - if (messageJsonObject["mMessageContent"]?.asJsonObject?.get("mContentType")?.asString == "SNAP") { - messageJsonObject["mMetadata"].asJsonObject.addProperty("mPlayableSnapState", "PLAYABLE") - } - - //serialize all properties of messageJsonObject and put in the message object - messageInstance.javaClass.declaredFields.forEach { field -> - field.isAccessible = true - if (field.name == "mDescriptor") return@forEach // prevent the client message id from being overwritten - messageJsonObject[field.name]?.let { fieldValue -> - field.set(messageInstance, context.gson.fromJson(fieldValue, field.type)) + //if the message is a snap make it playable + if (messageJsonObject["mMessageContent"]?.asJsonObject?.get("mContentType")?.asString == "SNAP") { + messageJsonObject["mMetadata"].asJsonObject.addProperty("mPlayableSnapState", "PLAYABLE") } - } - deletedMessageCache[uniqueMessageIdentifier] = deletedMessageObject - } + //serialize all properties of messageJsonObject and put in the message object + messageInstance.javaClass.declaredFields.forEach { field -> + field.isAccessible = true + if (field.name == "mDescriptor") return@forEach // prevent the client message id from being overwritten + messageJsonObject[field.name]?.let { fieldValue -> + field.set(messageInstance, context.gson.fromJson(fieldValue, field.type)) + } + } - override fun init() { - Hooker.hookConstructor(context.classCache.message, HookStage.AFTER, { isEnabled }) { param -> - processSnapMessage(param.thisObject()) + deletedMessageCache[uniqueMessageIdentifier] = deletedMessageObject } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/SnapToChatMedia.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/SnapToChatMedia.kt @@ -1,24 +1,21 @@ package me.rhunk.snapenhance.features.impl.spying +import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.util.protobuf.ProtoWriter import me.rhunk.snapenhance.data.ContentType -import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.hookConstructor class SnapToChatMedia : Feature("SnapToChatMedia", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { override fun onActivityCreate() { if (!context.config.messaging.snapToChatMedia.get()) return - context.classCache.message.hookConstructor(HookStage.AFTER) { param -> - val message = Message(param.thisObject()) - if (message.messageContent.contentType != ContentType.SNAP) return@hookConstructor + context.event.subscribe(BuildMessageEvent::class, priority = 100) { event -> + if (event.message.messageContent.contentType != ContentType.SNAP) return@subscribe - val snapMessageContent = ProtoReader(message.messageContent.content).followPath(11)?.getBuffer() ?: return@hookConstructor - message.messageContent.content = ProtoWriter().apply { + val snapMessageContent = ProtoReader(event.message.messageContent.content).followPath(11)?.getBuffer() ?: return@subscribe + event.message.messageContent.content = ProtoWriter().apply { from(3) { addBuffer(3, snapMessageContent) } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt @@ -1,34 +1,32 @@ package me.rhunk.snapenhance.features.impl.tweaks +import me.rhunk.snapenhance.core.event.events.impl.BuildMessageEvent import me.rhunk.snapenhance.core.util.protobuf.ProtoEditor import me.rhunk.snapenhance.core.util.protobuf.ProtoReader import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.MessageState -import me.rhunk.snapenhance.data.wrapper.impl.Message import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.hookConstructor class UnlimitedSnapViewTime : - Feature("UnlimitedSnapViewTime", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { - override fun onActivityCreate() { + Feature("UnlimitedSnapViewTime", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + override fun asyncOnActivityCreate() { val state by context.config.messaging.unlimitedSnapViewTime - context.classCache.message.hookConstructor(HookStage.AFTER, { state }) { param -> - val message = Message(param.thisObject()) - if (message.messageState != MessageState.COMMITTED) return@hookConstructor - if (message.messageContent.contentType != ContentType.SNAP) return@hookConstructor - with(message.messageContent) { - val mediaAttributes = ProtoReader(this.content).followPath(11, 5, 2) ?: return@hookConstructor - if (mediaAttributes.contains(6)) return@hookConstructor - this.content = ProtoEditor(this.content).apply { - edit(11, 5, 2) { - remove(8) - addBuffer(6, byteArrayOf()) - } - }.toByteArray() - } + context.event.subscribe(BuildMessageEvent::class, { state }, priority = 101) { event -> + if (event.message.messageState != MessageState.COMMITTED) return@subscribe + if (event.message.messageContent.contentType != ContentType.SNAP) return@subscribe + + val messageContent = event.message.messageContent + + val mediaAttributes = ProtoReader(messageContent.content).followPath(11, 5, 2) ?: return@subscribe + if (mediaAttributes.contains(6)) return@subscribe + messageContent.content = ProtoEditor(messageContent.content).apply { + edit(11, 5, 2) { + remove(8) + addBuffer(6, byteArrayOf()) + } + }.toByteArray() } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/ChatActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/ChatActionMenu.kt @@ -189,7 +189,7 @@ class ChatActionMenu : AbstractMenu() { append("arroyo_content_type: ${ContentType.fromId(arroyoMessage.contentType)} (${arroyoMessage.contentType})\n") append("parsed_content_type: ${ContentType.fromMessageContainer( ProtoReader(arroyoMessage.messageContent!!).followPath(4, 4) - ).let { "$it (${it.id})" }}\n") + ).let { "$it (${it?.id})" }}\n") append("creation_timestamp: ${arroyoMessage.creationTimestamp} (${Instant.ofEpochMilli(arroyoMessage.creationTimestamp)})\n") append("read_timestamp: ${arroyoMessage.readTimestamp} (${Instant.ofEpochMilli(arroyoMessage.readTimestamp)})\n") append("is_messagelogger_deleted: ${messageLogger.isMessageDeleted(arroyoMessage.clientConversationId!!, arroyoMessage.clientMessageId.toLong())}\n") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt @@ -122,7 +122,7 @@ class FriendFeedInfoMenu : AbstractMenu() { messageLogger.takeIf { it.isEnabled }?.getMessageProto(conversationId, message.clientMessageId.toLong()) ?: ProtoReader(message.messageContent ?: return@forEach).followPath(4, 4) ) ?: return@forEach - val contentType = ContentType.fromMessageContainer(protoReader) + val contentType = ContentType.fromMessageContainer(protoReader) ?: ContentType.fromId(message.contentType) var messageString = if (contentType == ContentType.CHAT) { protoReader.getString(2, 1) ?: return@forEach } else {