fix(messagelogger): message unique identifier

- refactor bridge
- developer mode (shows additional info about messages)
- add ability to see deleted messages in ff preview
- fix notification username
This commit is contained in:
rhunk
2023-09-30 16:08:05 +02:00
parent 476de8dc38
commit 1e2c71403e
18 changed files with 290 additions and 177 deletions

View File

@ -5,6 +5,7 @@ import me.rhunk.snapenhance.bridge.DownloadCallback;
import me.rhunk.snapenhance.bridge.SyncCallback;
import me.rhunk.snapenhance.bridge.scripting.IScripting;
import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface;
import me.rhunk.snapenhance.bridge.MessageLoggerInterface;
interface BridgeInterface {
/**
@ -18,27 +19,6 @@ interface BridgeInterface {
*/
byte[] fileOperation(int action, int fileType, in @nullable byte[] content);
/**
* Get the content of a logged message from the database
* @return message ids that are logged
*/
long[] getLoggedMessageIds(String conversationId, int limit);
/**
* Get the content of a logged message from the database
*/
@nullable byte[] getMessageLoggerMessage(String conversationId, long id);
/**
* Add a message to the message logger database
*/
void addMessageLoggerMessage(String conversationId, long id, in byte[] message);
/**
* Delete a message from the message logger database
*/
void deleteMessageLoggerMessage(String conversationId, long id);
/**
* Get the application APK path (assets for the conversation exporter)
*/
@ -97,6 +77,8 @@ interface BridgeInterface {
E2eeInterface getE2eeInterface();
MessageLoggerInterface getMessageLogger();
void openSettingsOverlay();
void closeSettingsOverlay();

View File

@ -0,0 +1,24 @@
package me.rhunk.snapenhance.bridge;
interface MessageLoggerInterface {
/**
* Get the ids of the messages that are logged
* @return message ids that are logged
*/
long[] getLoggedIds(in String[] conversationIds, int limit);
/**
* Get the content of a logged message from the database
*/
@nullable byte[] getMessage(String conversationId, long id);
/**
* Add a message to the message logger database if it is not already there
*/
boolean addMessage(String conversationId, long id, in byte[] message);
/**
* Delete a message from the message logger database
*/
void deleteMessage(String conversationId, long id);
}

View File

@ -548,6 +548,10 @@
"name": "Scripting",
"description": "Run custom scripts to extend SnapEnhance",
"properties": {
"developer_mode": {
"name": "Developer Mode",
"description": "Shows debug info on Snapchat's UI"
},
"module_folder": {
"name": "Module Folder",
"description": "The folder where the scripts are located"

View File

@ -4,7 +4,6 @@ object Constants {
const val SNAPCHAT_PACKAGE_NAME = "com.snapchat.android"
val ARROYO_MEDIA_CONTAINER_PROTO_PATH = intArrayOf(4, 4)
val ARROYO_STRING_CHAT_MESSAGE_PROTO = ARROYO_MEDIA_CONTAINER_PROTO_PATH + intArrayOf(2, 1)
const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}

View File

@ -58,6 +58,8 @@ class ModContext {
val native = NativeLib()
val scriptRuntime by lazy { CoreScriptRuntime(log, androidContext.classLoader) }
val isDeveloper by lazy { config.scripting.developerMode.get() }
fun <T : Feature> feature(featureClass: KClass<T>): T {
return features.get(featureClass)!!
}

View File

@ -111,14 +111,6 @@ class BridgeClient(
fun isFileExists(fileType: BridgeFileType) = service.fileOperation(FileActionType.EXISTS.ordinal, fileType.value, null).isNotEmpty()
fun getLoggedMessageIds(conversationId: String, limit: Int): LongArray = service.getLoggedMessageIds(conversationId, limit)
fun getMessageLoggerMessage(conversationId: String, id: Long): ByteArray? = service.getMessageLoggerMessage(conversationId, id)
fun addMessageLoggerMessage(conversationId: String, id: Long, message: ByteArray) = service.addMessageLoggerMessage(conversationId, id, message)
fun deleteMessageLoggerMessage(conversationId: String, id: Long) = service.deleteMessageLoggerMessage(conversationId, id)
fun fetchLocales(userLocale: String) = service.fetchLocales(userLocale).map {
LocalePair(it.key, it.value)
}
@ -148,6 +140,8 @@ class BridgeClient(
fun getE2eeInterface(): E2eeInterface = service.getE2eeInterface()
fun getMessageLogger() = service.messageLogger
fun openSettingsOverlay() = service.openSettingsOverlay()
fun closeSettingsOverlay() = service.closeSettingsOverlay()
}

View File

@ -2,14 +2,15 @@ package me.rhunk.snapenhance.core.bridge.wrapper
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import me.rhunk.snapenhance.bridge.MessageLoggerInterface
import me.rhunk.snapenhance.core.util.SQLiteDatabaseHelper
import java.io.File
import java.util.UUID
class MessageLoggerWrapper(
private val databaseFile: File
) {
lateinit var database: SQLiteDatabase
): MessageLoggerInterface.Stub() {
private lateinit var database: SQLiteDatabase
fun init() {
database = SQLiteDatabase.openDatabase(databaseFile.absolutePath, null, SQLiteDatabase.CREATE_IF_NECESSARY or SQLiteDatabase.OPEN_READWRITE)
@ -23,11 +24,37 @@ class MessageLoggerWrapper(
))
}
fun deleteMessage(conversationId: String, messageId: Long) {
database.execSQL("DELETE FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString()))
override fun getLoggedIds(conversationId: Array<String>, limit: Int): LongArray {
if (conversationId.any {
runCatching { UUID.fromString(it) }.isFailure
}) return longArrayOf()
val cursor = database.rawQuery("SELECT message_id FROM messages WHERE conversation_id IN (${
conversationId.joinToString(
","
) { "'$it'" }
}) ORDER BY message_id DESC LIMIT $limit", null)
val ids = mutableListOf<Long>()
while (cursor.moveToNext()) {
ids.add(cursor.getLong(0))
}
cursor.close()
return ids.toLongArray()
}
fun addMessage(conversationId: String, messageId: Long, serializedMessage: ByteArray): Boolean {
override fun getMessage(conversationId: String?, id: Long): ByteArray? {
val cursor = database.rawQuery("SELECT message_data FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, id.toString()))
val message: ByteArray? = if (cursor.moveToFirst()) {
cursor.getBlob(0)
} else {
null
}
cursor.close()
return message
}
override fun addMessage(conversationId: String, messageId: Long, serializedMessage: ByteArray): Boolean {
val cursor = database.rawQuery("SELECT message_id FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString()))
val state = cursor.moveToFirst()
cursor.close()
@ -42,29 +69,11 @@ class MessageLoggerWrapper(
return true
}
fun getMessage(conversationId: String, messageId: Long): Pair<Boolean, ByteArray?> {
val cursor = database.rawQuery("SELECT message_data FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString()))
val state = cursor.moveToFirst()
val message: ByteArray? = if (state) {
cursor.getBlob(0)
} else {
null
}
cursor.close()
return Pair(state, message)
}
fun getMessageIds(conversationId: String, limit: Int): List<Long> {
val cursor = database.rawQuery("SELECT message_id FROM messages WHERE conversation_id = ? ORDER BY message_id DESC LIMIT ?", arrayOf(conversationId, limit.toString()))
val messageIds = mutableListOf<Long>()
while (cursor.moveToNext()) {
messageIds.add(cursor.getLong(0))
}
cursor.close()
return messageIds
}
fun clearMessages() {
database.execSQL("DELETE FROM messages")
}
override fun deleteMessage(conversationId: String, messageId: Long) {
database.execSQL("DELETE FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString()))
}
}

View File

@ -4,6 +4,7 @@ import me.rhunk.snapenhance.core.config.ConfigContainer
import me.rhunk.snapenhance.core.config.ConfigFlag
class Scripting : ConfigContainer() {
val developerMode = boolean("developer_mode", false)
val moduleFolder = string("module_folder", "modules") { addFlags(ConfigFlag.FOLDER) }
val hotReload = boolean("hot_reload", false)
}

View File

@ -40,11 +40,4 @@ data class ConversationMessage(
senderId = getStringOrNull("sender_id")
}
}
fun getMessageAsString(): String? {
return when (ContentType.fromId(contentType)) {
ContentType.CHAT -> messageContent?.let { ProtoReader(it).getString(*Constants.ARROYO_STRING_CHAT_MESSAGE_PROTO) }
else -> null
}
}
}

View File

@ -1,5 +1,7 @@
package me.rhunk.snapenhance.data
import me.rhunk.snapenhance.core.util.protobuf.ProtoReader
enum class MessageState {
PREPARING, SENDING, COMMITTED, FAILED, CANCELING
}
@ -63,6 +65,20 @@ enum class ContentType(val id: Int) {
fun fromId(i: Int): ContentType {
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
}
}
}
}

View File

@ -526,7 +526,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp
val friendInfo: FriendInfo = context.database.getFriendInfo(message.senderId!!) ?: throw Exception("Friend not found in database")
val authorName = friendInfo.usernameForSorting!!
val decodedAttachments = messageLogger.getMessageObject(message.clientConversationId!!, message.serverMessageId.toLong())?.let {
val decodedAttachments = messageLogger.takeIf { it.isEnabled }?.getMessageObject(message.clientConversationId!!, message.clientMessageId.toLong())?.let {
MessageDecoder.decode(it.getAsJsonObject("mMessageContent"))
} ?: MessageDecoder.decode(
protoReader = ProtoReader(message.messageContent!!)

View File

@ -284,7 +284,6 @@ class EndToEndEncryption : MessagingRuleFeature(
if (messageTypeId == ENCRYPTED_MESSAGE_ID) {
runCatching {
replaceMessageText("Cannot find a key to decrypt this message.")
eachBuffer(2) {
val participantIdHash = getByteArray(1) ?: return@eachBuffer
val iv = getByteArray(2) ?: return@eachBuffer

View File

@ -8,6 +8,7 @@ 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.util.protobuf.ProtoReader
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.MessageState
import me.rhunk.snapenhance.data.wrapper.impl.Message
@ -39,39 +40,55 @@ class MessageLogger : Feature("MessageLogger",
const val DELETED_MESSAGE_COLOR = 0x2Eb71c1c
}
private val isEnabled get() = context.config.messaging.messageLogger.get()
private val messageLoggerInterface by lazy { context.bridgeClient.getMessageLogger() }
val isEnabled get() = context.config.messaging.messageLogger.get()
private val threadPool = Executors.newFixedThreadPool(10)
//two level of cache to avoid querying the database
private val fetchedMessages = mutableListOf<Long>()
private val deletedMessageCache = mutableMapOf<Long, JsonObject>()
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
fun isMessageRemoved(conversationId: String, orderKey: Long) = deletedMessageCache.containsKey(computeMessageIdentifier(conversationId, orderKey))
fun isMessageDeleted(conversationId: String, clientMessageId: Long)
= makeUniqueIdentifier(conversationId, clientMessageId)?.let { deletedMessageCache.containsKey(it) } ?: false
fun deleteMessage(conversationId: String, clientMessageId: Long) {
val serverMessageId = getServerMessageIdentifier(conversationId, clientMessageId) ?: return
fetchedMessages.remove(serverMessageId)
deletedMessageCache.remove(serverMessageId)
context.bridgeClient.deleteMessageLoggerMessage(conversationId, serverMessageId)
val uniqueMessageId = makeUniqueIdentifier(conversationId, clientMessageId) ?: return
fetchedMessages.remove(uniqueMessageId)
deletedMessageCache.remove(uniqueMessageId)
messageLoggerInterface.deleteMessage(conversationId, uniqueMessageId)
}
fun getMessageObject(conversationId: String, orderKey: Long): JsonObject? {
val messageIdentifier = computeMessageIdentifier(conversationId, orderKey)
if (deletedMessageCache.containsKey(messageIdentifier)) {
return deletedMessageCache[messageIdentifier]
fun getMessageObject(conversationId: String, clientMessageId: Long): JsonObject? {
val uniqueMessageId = makeUniqueIdentifier(conversationId, clientMessageId) ?: return null
if (deletedMessageCache.containsKey(uniqueMessageId)) {
return deletedMessageCache[uniqueMessageId]
}
return context.bridgeClient.getMessageLoggerMessage(conversationId, messageIdentifier)?.let {
return messageLoggerInterface.getMessage(conversationId, uniqueMessageId)?.let {
JsonParser.parseString(it.toString(Charsets.UTF_8)).asJsonObject
}
}
private fun computeMessageIdentifier(conversationId: String, orderKey: Long) = (orderKey.toString() + conversationId).longHashCode()
private fun getServerMessageIdentifier(conversationId: String, clientMessageId: Long): Long? {
val serverMessageId = context.database.getConversationMessageFromId(clientMessageId)?.serverMessageId?.toLong() ?: return run {
context.log.error("Failed to get server message id for $conversationId $clientMessageId")
null
fun getMessageProto(conversationId: String, clientMessageId: Long): ProtoReader? {
return getMessageObject(conversationId, clientMessageId)?.let { message ->
ProtoReader(message.getAsJsonObject("mMessageContent").getAsJsonArray("mContent")
.map { it.asByte }
.toByteArray())
}
}
private fun computeMessageIdentifier(conversationId: String, orderKey: Long) = (orderKey.toString() + conversationId).longHashCode()
private fun makeUniqueIdentifier(conversationId: String, clientMessageId: Long): Long? {
val serverMessageId = cachedIdLinks[clientMessageId] ?:
context.database.getConversationMessageFromId(clientMessageId)?.serverMessageId?.toLong()?.also {
cachedIdLinks[clientMessageId] = it
}
?: return run {
context.log.error("Failed to get server message id for $conversationId $clientMessageId")
null
}
return computeMessageIdentifier(conversationId, serverMessageId)
}
@ -82,9 +99,9 @@ class MessageLogger : Feature("MessageLogger",
}
measureTime {
context.database.getFeedEntries(PREFETCH_FEED_COUNT).forEach { friendFeedInfo ->
fetchedMessages.addAll(context.bridgeClient.getLoggedMessageIds(friendFeedInfo.key!!, PREFETCH_MESSAGE_COUNT).toList())
}
val conversationIds = context.database.getFeedEntries(PREFETCH_FEED_COUNT).map { it.key!! }
if (conversationIds.isEmpty()) return@measureTime
fetchedMessages.addAll(messageLoggerInterface.getLoggedIds(conversationIds.toTypedArray(), PREFETCH_MESSAGE_COUNT).toList())
}.also { context.log.verbose("Loaded ${fetchedMessages.size} cached messages in $it") }
}
@ -93,22 +110,20 @@ class MessageLogger : Feature("MessageLogger",
if (message.messageState != MessageState.COMMITTED) return
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 conversationId = message.messageDescriptor.conversationId.toString()
val serverIdentifier = computeMessageIdentifier(conversationId, message.orderKey)
val uniqueMessageIdentifier = computeMessageIdentifier(conversationId, message.orderKey)
if (message.messageContent.contentType != ContentType.STATUS) {
if (fetchedMessages.contains(serverIdentifier)) return
fetchedMessages.add(serverIdentifier)
if (fetchedMessages.contains(uniqueMessageIdentifier)) return
fetchedMessages.add(uniqueMessageIdentifier)
threadPool.execute {
try {
context.bridgeClient.getMessageLoggerMessage(conversationId, serverIdentifier)?.let {
return@execute
}
context.bridgeClient.addMessageLoggerMessage(conversationId, serverIdentifier, context.gson.toJson(messageInstance).toByteArray(Charsets.UTF_8))
messageLoggerInterface.addMessage(conversationId, uniqueMessageIdentifier, context.gson.toJson(messageInstance).toByteArray(Charsets.UTF_8))
} catch (ignored: DeadObjectException) {}
}
@ -116,10 +131,10 @@ class MessageLogger : Feature("MessageLogger",
}
//query the deleted message
val deletedMessageObject: JsonObject = if (deletedMessageCache.containsKey(serverIdentifier))
deletedMessageCache[serverIdentifier]
val deletedMessageObject: JsonObject = if (deletedMessageCache.containsKey(uniqueMessageIdentifier))
deletedMessageCache[uniqueMessageIdentifier]
else {
context.bridgeClient.getMessageLoggerMessage(conversationId, serverIdentifier)?.let {
messageLoggerInterface.getMessage(conversationId, uniqueMessageIdentifier)?.let {
JsonParser.parseString(it.toString(Charsets.UTF_8)).asJsonObject
}
} ?: return
@ -134,19 +149,13 @@ class MessageLogger : Feature("MessageLogger",
//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))
}
}
/*//set the message state to PREPARING for visibility
with(message.messageContent.contentType) {
if (this != ContentType.SNAP && this != ContentType.EXTERNAL_MEDIA) {
message.messageState = MessageState.PREPARING
}
}*/
deletedMessageCache[serverIdentifier] = deletedMessageObject
deletedMessageCache[uniqueMessageIdentifier] = deletedMessageObject
}
override fun init() {
@ -161,7 +170,7 @@ class MessageLogger : Feature("MessageLogger",
context.event.subscribe(BindViewEvent::class) { event ->
event.chatMessage { conversationId, messageId ->
event.view.removeForegroundDrawable("deletedMessage")
getServerMessageIdentifier(conversationId, messageId.toLong())?.let { serverMessageId ->
makeUniqueIdentifier(conversationId, messageId.toLong())?.let { serverMessageId ->
if (!deletedMessageCache.contains(serverMessageId)) return@chatMessage
} ?: return@chatMessage

View File

@ -36,7 +36,7 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE,
private fun saveMessage(conversationId: SnapUUID, message: Message) {
val messageId = message.messageDescriptor.messageId
if (messageLogger.isMessageRemoved(conversationId.toString(), message.orderKey)) return
if (messageLogger.takeIf { it.isEnabled }?.isMessageDeleted(conversationId.toString(), message.messageDescriptor.messageId) == true) return
if (message.messageState != MessageState.COMMITTED) return
runCatching {

View File

@ -220,7 +220,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
val snapMessage = messages.firstOrNull { message -> message.orderKey == messageId } ?: return
val senderUsername by lazy {
context.database.getFriendInfo(snapMessage.senderId.toString())?.let {
it.displayName ?: it.username
it.displayName ?: it.mutableUsername
}
}

View File

@ -1,6 +1,7 @@
package me.rhunk.snapenhance.ui.menu.impl
import android.annotation.SuppressLint
import android.content.Context
import android.os.SystemClock
import android.view.MotionEvent
import android.view.View
@ -8,21 +9,87 @@ import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.core.util.protobuf.ProtoReader
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.ui.ViewTagState
import me.rhunk.snapenhance.ui.applyTheme
import me.rhunk.snapenhance.ui.menu.AbstractMenu
import java.time.Instant
@SuppressLint("DiscouragedApi")
class ChatActionMenu : AbstractMenu() {
private val viewTagState = ViewTagState()
@SuppressLint("SetTextI18n", "DiscouragedApi")
fun inject(viewGroup: ViewGroup) {
private val defaultGap by lazy {
context.androidContext.resources.getDimensionPixelSize(
context.androidContext.resources.getIdentifier(
"default_gap",
"dimen",
Constants.SNAPCHAT_PACKAGE_NAME
)
)
}
private val chatActionMenuItemMargin by lazy {
context.androidContext.resources.getDimensionPixelSize(
context.androidContext.resources.getIdentifier(
"chat_action_menu_item_margin",
"dimen",
Constants.SNAPCHAT_PACKAGE_NAME
)
)
}
private val actionMenuItemHeight by lazy {
context.androidContext.resources.getDimensionPixelSize(
context.androidContext.resources.getIdentifier(
"action_menu_item_height",
"dimen",
Constants.SNAPCHAT_PACKAGE_NAME
)
)
}
private fun createContainer(viewGroup: ViewGroup): LinearLayout {
val parent = viewGroup.parent.parent as ViewGroup
return LinearLayout(viewGroup.context).apply layout@{
orientation = LinearLayout.VERTICAL
layoutParams = MarginLayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
applyTheme(parent.width, true)
setMargins(chatActionMenuItemMargin, 0, chatActionMenuItemMargin, defaultGap)
}
}
}
private fun copyAlertDialog(context: Context, title: String, text: String) {
ViewAppearanceHelper.newAlertDialogBuilder(context).apply {
setTitle(title)
setMessage(text)
setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
setNegativeButton("Copy") { _, _ ->
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
clipboardManager.setPrimaryClip(android.content.ClipData.newPlainText("debug", text))
}
}.show()
}
private val lastFocusedMessage
get() = context.database.getConversationMessageFromId(context.feature(Messaging::class).lastFocusedMessageId)
@SuppressLint("SetTextI18n", "DiscouragedApi", "ClickableViewAccessibility")
fun inject(viewGroup: ViewGroup) {
val parent = viewGroup.parent.parent as? ViewGroup ?: return
if (viewTagState[parent]) return
//close the action menu using a touch event
val closeActionMenu = {
@ -38,40 +105,10 @@ class ChatActionMenu : AbstractMenu() {
)
}
val defaultGap = viewGroup.resources.getDimensionPixelSize(
viewGroup.resources.getIdentifier(
"default_gap",
"dimen",
Constants.SNAPCHAT_PACKAGE_NAME
)
)
val messaging = context.feature(Messaging::class)
val messageLogger = context.feature(MessageLogger::class)
val chatActionMenuItemMargin = viewGroup.resources.getDimensionPixelSize(
viewGroup.resources.getIdentifier(
"chat_action_menu_item_margin",
"dimen",
Constants.SNAPCHAT_PACKAGE_NAME
)
)
val actionMenuItemHeight = viewGroup.resources.getDimensionPixelSize(
viewGroup.resources.getIdentifier(
"action_menu_item_height",
"dimen",
Constants.SNAPCHAT_PACKAGE_NAME
)
)
val buttonContainer = LinearLayout(viewGroup.context).apply layout@{
orientation = LinearLayout.VERTICAL
layoutParams = MarginLayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
applyTheme(parent.width, true)
setMargins(chatActionMenuItemMargin, 0, chatActionMenuItemMargin, defaultGap)
}
}
val buttonContainer = createContainer(viewGroup)
val injectButton = { button: Button ->
if (buttonContainer.childCount > 0) {
@ -125,14 +162,66 @@ class ChatActionMenu : AbstractMenu() {
setOnClickListener {
closeActionMenu()
this@ChatActionMenu.context.executeAsync {
feature(Messaging::class).apply {
feature(MessageLogger::class).deleteMessage(openedConversationUUID.toString(), lastFocusedMessageId)
}
messageLogger.deleteMessage(messaging.openedConversationUUID.toString(), messaging.lastFocusedMessageId)
}
}
})
}
if (context.isDeveloper) {
parent.addView(createContainer(viewGroup).apply {
val debugText = StringBuilder()
setOnClickListener {
val clipboardManager = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
clipboardManager.setPrimaryClip(android.content.ClipData.newPlainText("debug", debugText.toString()))
}
addView(TextView(viewGroup.context).apply {
setPadding(20, 20, 20, 20)
textSize = 10f
addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
val arroyoMessage = lastFocusedMessage ?: return@addOnLayoutChangeListener
text = debugText.apply {
runCatching {
clear()
append("sender_id: ${arroyoMessage.senderId}\n")
append("client_id: ${arroyoMessage.clientMessageId}, server_id: ${arroyoMessage.serverMessageId}\n")
append("conversation_id: ${arroyoMessage.clientConversationId}\n")
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")
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")
append("is_messagelogger_stored: ${messageLogger.getMessageObject(arroyoMessage.clientConversationId!!, arroyoMessage.clientMessageId.toLong()) != null}\n")
}.onFailure {
debugText.append("Error: $it\n")
}
}.toString().trimEnd()
}
})
// action buttons
addView(LinearLayout(viewGroup.context).apply {
orientation = LinearLayout.HORIZONTAL
addView(Button(viewGroup.context).apply {
text = "Show Deleted Message Object"
setOnClickListener {
val message = lastFocusedMessage ?: return@setOnClickListener
copyAlertDialog(
viewGroup.context,
"Deleted Message Object",
messageLogger.getMessageObject(message.clientConversationId!!, message.clientMessageId.toLong())?.toString()
?: "null"
)
}
})
})
})
}
parent.addView(buttonContainer)
}
}

View File

@ -14,10 +14,12 @@ import android.widget.Switch
import me.rhunk.snapenhance.core.database.objects.ConversationMessage
import me.rhunk.snapenhance.core.database.objects.FriendInfo
import me.rhunk.snapenhance.core.database.objects.UserConversationLink
import me.rhunk.snapenhance.core.util.protobuf.ProtoReader
import me.rhunk.snapenhance.core.util.snap.BitmojiSelfie
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.FriendLinkType
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.ui.applyTheme
import me.rhunk.snapenhance.ui.menu.AbstractMenu
@ -102,15 +104,11 @@ class FriendFeedInfoMenu : AbstractMenu() {
private fun showPreview(userId: String?, conversationId: String, androidCtx: Context?) {
//query message
val messages: List<ConversationMessage>? = context.database.getMessagesFromConversationId(
val messageLogger = context.feature(MessageLogger::class)
val messages: List<ConversationMessage> = context.database.getMessagesFromConversationId(
conversationId,
context.config.messaging.messagePreviewLength.get()
)?.reversed()
if (messages == null) {
context.longToast("Can't fetch messages")
return
}
)?.reversed() ?: emptyList()
val participants: Map<String, FriendInfo> = context.database.getConversationParticipants(conversationId)!!
.map { context.database.getFriendInfo(it)!! }
@ -119,19 +117,26 @@ class FriendFeedInfoMenu : AbstractMenu() {
val messageBuilder = StringBuilder()
messages.forEach { message ->
val sender: FriendInfo? = participants[message.senderId]
val sender = participants[message.senderId]
val protoReader = (
messageLogger.takeIf { it.isEnabled }?.getMessageProto(conversationId, message.clientMessageId.toLong()) ?: ProtoReader(message.messageContent ?: return@forEach).followPath(4, 4)
) ?: return@forEach
var messageString: String = message.getMessageAsString() ?: ContentType.fromId(message.contentType).name
val contentType = ContentType.fromMessageContainer(protoReader)
var messageString = if (contentType == ContentType.CHAT) {
protoReader.getString(2, 1) ?: return@forEach
} else {
contentType.name
}
if (message.contentType == ContentType.SNAP.id) {
val readTimeStamp: Long = message.readTimestamp
if (contentType == ContentType.SNAP) {
messageString = "\uD83D\uDFE5" //red square
if (readTimeStamp > 0) {
if (message.readTimestamp > 0) {
messageString += " \uD83D\uDC40 " //eyes
messageString += DateFormat.getDateTimeInstance(
DateFormat.SHORT,
DateFormat.SHORT
).format(Date(readTimeStamp))
).format(Date(message.readTimestamp))
}
}
@ -144,8 +149,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
messageBuilder.append(displayUsername).append(": ").append(messageString).append("\n")
}
val targetPerson: FriendInfo? =
if (userId == null) null else participants[userId]
val targetPerson = if (userId == null) null else participants[userId]
targetPerson?.streakExpirationTimestamp?.takeIf { it > 0 }?.let {
val timeSecondDiff = ((it - System.currentTimeMillis()) / 1000 / 60).toInt()