mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-13 05:37:48 +02:00
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:
@ -109,19 +109,6 @@ class BridgeService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLoggedMessageIds(conversationId: String, limit: Int) =
|
|
||||||
messageLoggerWrapper.getMessageIds(conversationId, limit).toLongArray()
|
|
||||||
|
|
||||||
override fun getMessageLoggerMessage(conversationId: String, id: Long) =
|
|
||||||
messageLoggerWrapper.getMessage(conversationId, id).second
|
|
||||||
|
|
||||||
override fun addMessageLoggerMessage(conversationId: String, id: Long, message: ByteArray) {
|
|
||||||
messageLoggerWrapper.addMessage(conversationId, id, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteMessageLoggerMessage(conversationId: String, id: Long) =
|
|
||||||
messageLoggerWrapper.deleteMessage(conversationId, id)
|
|
||||||
|
|
||||||
override fun getApplicationApkPath(): String = applicationInfo.publicSourceDir
|
override fun getApplicationApkPath(): String = applicationInfo.publicSourceDir
|
||||||
|
|
||||||
override fun fetchLocales(userLocale: String) =
|
override fun fetchLocales(userLocale: String) =
|
||||||
@ -189,6 +176,7 @@ class BridgeService : Service() {
|
|||||||
override fun getScriptingInterface() = remoteSideContext.scriptManager
|
override fun getScriptingInterface() = remoteSideContext.scriptManager
|
||||||
|
|
||||||
override fun getE2eeInterface() = remoteSideContext.e2eeImplementation
|
override fun getE2eeInterface() = remoteSideContext.e2eeImplementation
|
||||||
|
override fun getMessageLogger() = messageLoggerWrapper
|
||||||
|
|
||||||
override fun openSettingsOverlay() {
|
override fun openSettingsOverlay() {
|
||||||
runCatching {
|
runCatching {
|
||||||
|
@ -5,6 +5,7 @@ import me.rhunk.snapenhance.bridge.DownloadCallback;
|
|||||||
import me.rhunk.snapenhance.bridge.SyncCallback;
|
import me.rhunk.snapenhance.bridge.SyncCallback;
|
||||||
import me.rhunk.snapenhance.bridge.scripting.IScripting;
|
import me.rhunk.snapenhance.bridge.scripting.IScripting;
|
||||||
import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface;
|
import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface;
|
||||||
|
import me.rhunk.snapenhance.bridge.MessageLoggerInterface;
|
||||||
|
|
||||||
interface BridgeInterface {
|
interface BridgeInterface {
|
||||||
/**
|
/**
|
||||||
@ -18,27 +19,6 @@ interface BridgeInterface {
|
|||||||
*/
|
*/
|
||||||
byte[] fileOperation(int action, int fileType, in @nullable byte[] content);
|
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)
|
* Get the application APK path (assets for the conversation exporter)
|
||||||
*/
|
*/
|
||||||
@ -97,6 +77,8 @@ interface BridgeInterface {
|
|||||||
|
|
||||||
E2eeInterface getE2eeInterface();
|
E2eeInterface getE2eeInterface();
|
||||||
|
|
||||||
|
MessageLoggerInterface getMessageLogger();
|
||||||
|
|
||||||
void openSettingsOverlay();
|
void openSettingsOverlay();
|
||||||
|
|
||||||
void closeSettingsOverlay();
|
void closeSettingsOverlay();
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -548,6 +548,10 @@
|
|||||||
"name": "Scripting",
|
"name": "Scripting",
|
||||||
"description": "Run custom scripts to extend SnapEnhance",
|
"description": "Run custom scripts to extend SnapEnhance",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"developer_mode": {
|
||||||
|
"name": "Developer Mode",
|
||||||
|
"description": "Shows debug info on Snapchat's UI"
|
||||||
|
},
|
||||||
"module_folder": {
|
"module_folder": {
|
||||||
"name": "Module Folder",
|
"name": "Module Folder",
|
||||||
"description": "The folder where the scripts are located"
|
"description": "The folder where the scripts are located"
|
||||||
|
@ -4,7 +4,6 @@ object Constants {
|
|||||||
const val SNAPCHAT_PACKAGE_NAME = "com.snapchat.android"
|
const val SNAPCHAT_PACKAGE_NAME = "com.snapchat.android"
|
||||||
|
|
||||||
val ARROYO_MEDIA_CONTAINER_PROTO_PATH = intArrayOf(4, 4)
|
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"
|
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"
|
||||||
}
|
}
|
@ -58,6 +58,8 @@ class ModContext {
|
|||||||
val native = NativeLib()
|
val native = NativeLib()
|
||||||
val scriptRuntime by lazy { CoreScriptRuntime(log, androidContext.classLoader) }
|
val scriptRuntime by lazy { CoreScriptRuntime(log, androidContext.classLoader) }
|
||||||
|
|
||||||
|
val isDeveloper by lazy { config.scripting.developerMode.get() }
|
||||||
|
|
||||||
fun <T : Feature> feature(featureClass: KClass<T>): T {
|
fun <T : Feature> feature(featureClass: KClass<T>): T {
|
||||||
return features.get(featureClass)!!
|
return features.get(featureClass)!!
|
||||||
}
|
}
|
||||||
|
@ -111,14 +111,6 @@ class BridgeClient(
|
|||||||
|
|
||||||
fun isFileExists(fileType: BridgeFileType) = service.fileOperation(FileActionType.EXISTS.ordinal, fileType.value, null).isNotEmpty()
|
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 {
|
fun fetchLocales(userLocale: String) = service.fetchLocales(userLocale).map {
|
||||||
LocalePair(it.key, it.value)
|
LocalePair(it.key, it.value)
|
||||||
}
|
}
|
||||||
@ -148,6 +140,8 @@ class BridgeClient(
|
|||||||
|
|
||||||
fun getE2eeInterface(): E2eeInterface = service.getE2eeInterface()
|
fun getE2eeInterface(): E2eeInterface = service.getE2eeInterface()
|
||||||
|
|
||||||
|
fun getMessageLogger() = service.messageLogger
|
||||||
|
|
||||||
fun openSettingsOverlay() = service.openSettingsOverlay()
|
fun openSettingsOverlay() = service.openSettingsOverlay()
|
||||||
fun closeSettingsOverlay() = service.closeSettingsOverlay()
|
fun closeSettingsOverlay() = service.closeSettingsOverlay()
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,15 @@ package me.rhunk.snapenhance.core.bridge.wrapper
|
|||||||
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import me.rhunk.snapenhance.bridge.MessageLoggerInterface
|
||||||
import me.rhunk.snapenhance.core.util.SQLiteDatabaseHelper
|
import me.rhunk.snapenhance.core.util.SQLiteDatabaseHelper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class MessageLoggerWrapper(
|
class MessageLoggerWrapper(
|
||||||
private val databaseFile: File
|
private val databaseFile: File
|
||||||
) {
|
): MessageLoggerInterface.Stub() {
|
||||||
|
private lateinit var database: SQLiteDatabase
|
||||||
lateinit var database: SQLiteDatabase
|
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
database = SQLiteDatabase.openDatabase(databaseFile.absolutePath, null, SQLiteDatabase.CREATE_IF_NECESSARY or SQLiteDatabase.OPEN_READWRITE)
|
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) {
|
override fun getLoggedIds(conversationId: Array<String>, limit: Int): LongArray {
|
||||||
database.execSQL("DELETE FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString()))
|
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 cursor = database.rawQuery("SELECT message_id FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString()))
|
||||||
val state = cursor.moveToFirst()
|
val state = cursor.moveToFirst()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
@ -42,29 +69,11 @@ class MessageLoggerWrapper(
|
|||||||
return true
|
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() {
|
fun clearMessages() {
|
||||||
database.execSQL("DELETE FROM messages")
|
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()))
|
||||||
|
}
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ import me.rhunk.snapenhance.core.config.ConfigContainer
|
|||||||
import me.rhunk.snapenhance.core.config.ConfigFlag
|
import me.rhunk.snapenhance.core.config.ConfigFlag
|
||||||
|
|
||||||
class Scripting : ConfigContainer() {
|
class Scripting : ConfigContainer() {
|
||||||
|
val developerMode = boolean("developer_mode", false)
|
||||||
val moduleFolder = string("module_folder", "modules") { addFlags(ConfigFlag.FOLDER) }
|
val moduleFolder = string("module_folder", "modules") { addFlags(ConfigFlag.FOLDER) }
|
||||||
val hotReload = boolean("hot_reload", false)
|
val hotReload = boolean("hot_reload", false)
|
||||||
}
|
}
|
@ -40,11 +40,4 @@ data class ConversationMessage(
|
|||||||
senderId = getStringOrNull("sender_id")
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package me.rhunk.snapenhance.data
|
package me.rhunk.snapenhance.data
|
||||||
|
|
||||||
|
import me.rhunk.snapenhance.core.util.protobuf.ProtoReader
|
||||||
|
|
||||||
enum class MessageState {
|
enum class MessageState {
|
||||||
PREPARING, SENDING, COMMITTED, FAILED, CANCELING
|
PREPARING, SENDING, COMMITTED, FAILED, CANCELING
|
||||||
}
|
}
|
||||||
@ -63,6 +65,20 @@ enum class ContentType(val id: Int) {
|
|||||||
fun fromId(i: Int): ContentType {
|
fun fromId(i: Int): ContentType {
|
||||||
return values().firstOrNull { it.id == i } ?: UNKNOWN
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 friendInfo: FriendInfo = context.database.getFriendInfo(message.senderId!!) ?: throw Exception("Friend not found in database")
|
||||||
val authorName = friendInfo.usernameForSorting!!
|
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(it.getAsJsonObject("mMessageContent"))
|
||||||
} ?: MessageDecoder.decode(
|
} ?: MessageDecoder.decode(
|
||||||
protoReader = ProtoReader(message.messageContent!!)
|
protoReader = ProtoReader(message.messageContent!!)
|
||||||
|
@ -284,7 +284,6 @@ class EndToEndEncryption : MessagingRuleFeature(
|
|||||||
|
|
||||||
if (messageTypeId == ENCRYPTED_MESSAGE_ID) {
|
if (messageTypeId == ENCRYPTED_MESSAGE_ID) {
|
||||||
runCatching {
|
runCatching {
|
||||||
replaceMessageText("Cannot find a key to decrypt this message.")
|
|
||||||
eachBuffer(2) {
|
eachBuffer(2) {
|
||||||
val participantIdHash = getByteArray(1) ?: return@eachBuffer
|
val participantIdHash = getByteArray(1) ?: return@eachBuffer
|
||||||
val iv = getByteArray(2) ?: return@eachBuffer
|
val iv = getByteArray(2) ?: return@eachBuffer
|
||||||
|
@ -8,6 +8,7 @@ import android.os.DeadObjectException
|
|||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import me.rhunk.snapenhance.core.event.events.impl.BindViewEvent
|
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.ContentType
|
||||||
import me.rhunk.snapenhance.data.MessageState
|
import me.rhunk.snapenhance.data.MessageState
|
||||||
import me.rhunk.snapenhance.data.wrapper.impl.Message
|
import me.rhunk.snapenhance.data.wrapper.impl.Message
|
||||||
@ -39,39 +40,55 @@ class MessageLogger : Feature("MessageLogger",
|
|||||||
const val DELETED_MESSAGE_COLOR = 0x2Eb71c1c
|
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)
|
private val threadPool = Executors.newFixedThreadPool(10)
|
||||||
|
|
||||||
//two level of cache to avoid querying the database
|
private val cachedIdLinks = mutableMapOf<Long, Long>() // client id -> server id
|
||||||
private val fetchedMessages = mutableListOf<Long>()
|
private val fetchedMessages = mutableListOf<Long>() // list of unique message ids
|
||||||
private val deletedMessageCache = mutableMapOf<Long, JsonObject>()
|
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) {
|
fun deleteMessage(conversationId: String, clientMessageId: Long) {
|
||||||
val serverMessageId = getServerMessageIdentifier(conversationId, clientMessageId) ?: return
|
val uniqueMessageId = makeUniqueIdentifier(conversationId, clientMessageId) ?: return
|
||||||
fetchedMessages.remove(serverMessageId)
|
fetchedMessages.remove(uniqueMessageId)
|
||||||
deletedMessageCache.remove(serverMessageId)
|
deletedMessageCache.remove(uniqueMessageId)
|
||||||
context.bridgeClient.deleteMessageLoggerMessage(conversationId, serverMessageId)
|
messageLoggerInterface.deleteMessage(conversationId, uniqueMessageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMessageObject(conversationId: String, orderKey: Long): JsonObject? {
|
fun getMessageObject(conversationId: String, clientMessageId: Long): JsonObject? {
|
||||||
val messageIdentifier = computeMessageIdentifier(conversationId, orderKey)
|
val uniqueMessageId = makeUniqueIdentifier(conversationId, clientMessageId) ?: return null
|
||||||
if (deletedMessageCache.containsKey(messageIdentifier)) {
|
if (deletedMessageCache.containsKey(uniqueMessageId)) {
|
||||||
return deletedMessageCache[messageIdentifier]
|
return deletedMessageCache[uniqueMessageId]
|
||||||
}
|
}
|
||||||
return context.bridgeClient.getMessageLoggerMessage(conversationId, messageIdentifier)?.let {
|
return messageLoggerInterface.getMessage(conversationId, uniqueMessageId)?.let {
|
||||||
JsonParser.parseString(it.toString(Charsets.UTF_8)).asJsonObject
|
JsonParser.parseString(it.toString(Charsets.UTF_8)).asJsonObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun computeMessageIdentifier(conversationId: String, orderKey: Long) = (orderKey.toString() + conversationId).longHashCode()
|
fun getMessageProto(conversationId: String, clientMessageId: Long): ProtoReader? {
|
||||||
private fun getServerMessageIdentifier(conversationId: String, clientMessageId: Long): Long? {
|
return getMessageObject(conversationId, clientMessageId)?.let { message ->
|
||||||
val serverMessageId = context.database.getConversationMessageFromId(clientMessageId)?.serverMessageId?.toLong() ?: return run {
|
ProtoReader(message.getAsJsonObject("mMessageContent").getAsJsonArray("mContent")
|
||||||
context.log.error("Failed to get server message id for $conversationId $clientMessageId")
|
.map { it.asByte }
|
||||||
null
|
.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)
|
return computeMessageIdentifier(conversationId, serverMessageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,9 +99,9 @@ class MessageLogger : Feature("MessageLogger",
|
|||||||
}
|
}
|
||||||
|
|
||||||
measureTime {
|
measureTime {
|
||||||
context.database.getFeedEntries(PREFETCH_FEED_COUNT).forEach { friendFeedInfo ->
|
val conversationIds = context.database.getFeedEntries(PREFETCH_FEED_COUNT).map { it.key!! }
|
||||||
fetchedMessages.addAll(context.bridgeClient.getLoggedMessageIds(friendFeedInfo.key!!, PREFETCH_MESSAGE_COUNT).toList())
|
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") }
|
}.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
|
if (message.messageState != MessageState.COMMITTED) return
|
||||||
|
|
||||||
|
cachedIdLinks[message.messageDescriptor.messageId] = message.orderKey
|
||||||
|
val conversationId = message.messageDescriptor.conversationId.toString()
|
||||||
//exclude messages sent by me
|
//exclude messages sent by me
|
||||||
if (message.senderId.toString() == context.database.myUserId) return
|
if (message.senderId.toString() == context.database.myUserId) return
|
||||||
|
|
||||||
val conversationId = message.messageDescriptor.conversationId.toString()
|
val uniqueMessageIdentifier = computeMessageIdentifier(conversationId, message.orderKey)
|
||||||
val serverIdentifier = computeMessageIdentifier(conversationId, message.orderKey)
|
|
||||||
|
|
||||||
if (message.messageContent.contentType != ContentType.STATUS) {
|
if (message.messageContent.contentType != ContentType.STATUS) {
|
||||||
if (fetchedMessages.contains(serverIdentifier)) return
|
if (fetchedMessages.contains(uniqueMessageIdentifier)) return
|
||||||
fetchedMessages.add(serverIdentifier)
|
fetchedMessages.add(uniqueMessageIdentifier)
|
||||||
|
|
||||||
threadPool.execute {
|
threadPool.execute {
|
||||||
try {
|
try {
|
||||||
context.bridgeClient.getMessageLoggerMessage(conversationId, serverIdentifier)?.let {
|
messageLoggerInterface.addMessage(conversationId, uniqueMessageIdentifier, context.gson.toJson(messageInstance).toByteArray(Charsets.UTF_8))
|
||||||
return@execute
|
|
||||||
}
|
|
||||||
context.bridgeClient.addMessageLoggerMessage(conversationId, serverIdentifier, context.gson.toJson(messageInstance).toByteArray(Charsets.UTF_8))
|
|
||||||
} catch (ignored: DeadObjectException) {}
|
} catch (ignored: DeadObjectException) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,10 +131,10 @@ class MessageLogger : Feature("MessageLogger",
|
|||||||
}
|
}
|
||||||
|
|
||||||
//query the deleted message
|
//query the deleted message
|
||||||
val deletedMessageObject: JsonObject = if (deletedMessageCache.containsKey(serverIdentifier))
|
val deletedMessageObject: JsonObject = if (deletedMessageCache.containsKey(uniqueMessageIdentifier))
|
||||||
deletedMessageCache[serverIdentifier]
|
deletedMessageCache[uniqueMessageIdentifier]
|
||||||
else {
|
else {
|
||||||
context.bridgeClient.getMessageLoggerMessage(conversationId, serverIdentifier)?.let {
|
messageLoggerInterface.getMessage(conversationId, uniqueMessageIdentifier)?.let {
|
||||||
JsonParser.parseString(it.toString(Charsets.UTF_8)).asJsonObject
|
JsonParser.parseString(it.toString(Charsets.UTF_8)).asJsonObject
|
||||||
}
|
}
|
||||||
} ?: return
|
} ?: return
|
||||||
@ -134,19 +149,13 @@ class MessageLogger : Feature("MessageLogger",
|
|||||||
//serialize all properties of messageJsonObject and put in the message object
|
//serialize all properties of messageJsonObject and put in the message object
|
||||||
messageInstance.javaClass.declaredFields.forEach { field ->
|
messageInstance.javaClass.declaredFields.forEach { field ->
|
||||||
field.isAccessible = true
|
field.isAccessible = true
|
||||||
|
if (field.name == "mDescriptor") return@forEach // prevent the client message id from being overwritten
|
||||||
messageJsonObject[field.name]?.let { fieldValue ->
|
messageJsonObject[field.name]?.let { fieldValue ->
|
||||||
field.set(messageInstance, context.gson.fromJson(fieldValue, field.type))
|
field.set(messageInstance, context.gson.fromJson(fieldValue, field.type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//set the message state to PREPARING for visibility
|
deletedMessageCache[uniqueMessageIdentifier] = deletedMessageObject
|
||||||
with(message.messageContent.contentType) {
|
|
||||||
if (this != ContentType.SNAP && this != ContentType.EXTERNAL_MEDIA) {
|
|
||||||
message.messageState = MessageState.PREPARING
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
deletedMessageCache[serverIdentifier] = deletedMessageObject
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun init() {
|
override fun init() {
|
||||||
@ -161,7 +170,7 @@ class MessageLogger : Feature("MessageLogger",
|
|||||||
context.event.subscribe(BindViewEvent::class) { event ->
|
context.event.subscribe(BindViewEvent::class) { event ->
|
||||||
event.chatMessage { conversationId, messageId ->
|
event.chatMessage { conversationId, messageId ->
|
||||||
event.view.removeForegroundDrawable("deletedMessage")
|
event.view.removeForegroundDrawable("deletedMessage")
|
||||||
getServerMessageIdentifier(conversationId, messageId.toLong())?.let { serverMessageId ->
|
makeUniqueIdentifier(conversationId, messageId.toLong())?.let { serverMessageId ->
|
||||||
if (!deletedMessageCache.contains(serverMessageId)) return@chatMessage
|
if (!deletedMessageCache.contains(serverMessageId)) return@chatMessage
|
||||||
} ?: return@chatMessage
|
} ?: return@chatMessage
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE,
|
|||||||
|
|
||||||
private fun saveMessage(conversationId: SnapUUID, message: Message) {
|
private fun saveMessage(conversationId: SnapUUID, message: Message) {
|
||||||
val messageId = message.messageDescriptor.messageId
|
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
|
if (message.messageState != MessageState.COMMITTED) return
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
|
@ -220,7 +220,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
|||||||
val snapMessage = messages.firstOrNull { message -> message.orderKey == messageId } ?: return
|
val snapMessage = messages.firstOrNull { message -> message.orderKey == messageId } ?: return
|
||||||
val senderUsername by lazy {
|
val senderUsername by lazy {
|
||||||
context.database.getFriendInfo(snapMessage.senderId.toString())?.let {
|
context.database.getFriendInfo(snapMessage.senderId.toString())?.let {
|
||||||
it.displayName ?: it.username
|
it.displayName ?: it.mutableUsername
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package me.rhunk.snapenhance.ui.menu.impl
|
package me.rhunk.snapenhance.ui.menu.impl
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -8,21 +9,87 @@ import android.view.ViewGroup
|
|||||||
import android.view.ViewGroup.MarginLayoutParams
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
import me.rhunk.snapenhance.Constants
|
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.Messaging
|
||||||
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
|
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
|
||||||
import me.rhunk.snapenhance.features.impl.spying.MessageLogger
|
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.ViewTagState
|
||||||
import me.rhunk.snapenhance.ui.applyTheme
|
import me.rhunk.snapenhance.ui.applyTheme
|
||||||
import me.rhunk.snapenhance.ui.menu.AbstractMenu
|
import me.rhunk.snapenhance.ui.menu.AbstractMenu
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("DiscouragedApi")
|
||||||
class ChatActionMenu : AbstractMenu() {
|
class ChatActionMenu : AbstractMenu() {
|
||||||
private val viewTagState = ViewTagState()
|
private val viewTagState = ViewTagState()
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n", "DiscouragedApi")
|
private val defaultGap by lazy {
|
||||||
fun inject(viewGroup: ViewGroup) {
|
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
|
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
|
if (viewTagState[parent]) return
|
||||||
//close the action menu using a touch event
|
//close the action menu using a touch event
|
||||||
val closeActionMenu = {
|
val closeActionMenu = {
|
||||||
@ -38,40 +105,10 @@ class ChatActionMenu : AbstractMenu() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val defaultGap = viewGroup.resources.getDimensionPixelSize(
|
val messaging = context.feature(Messaging::class)
|
||||||
viewGroup.resources.getIdentifier(
|
val messageLogger = context.feature(MessageLogger::class)
|
||||||
"default_gap",
|
|
||||||
"dimen",
|
|
||||||
Constants.SNAPCHAT_PACKAGE_NAME
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val chatActionMenuItemMargin = viewGroup.resources.getDimensionPixelSize(
|
val buttonContainer = createContainer(viewGroup)
|
||||||
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 injectButton = { button: Button ->
|
val injectButton = { button: Button ->
|
||||||
if (buttonContainer.childCount > 0) {
|
if (buttonContainer.childCount > 0) {
|
||||||
@ -125,14 +162,66 @@ class ChatActionMenu : AbstractMenu() {
|
|||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
closeActionMenu()
|
closeActionMenu()
|
||||||
this@ChatActionMenu.context.executeAsync {
|
this@ChatActionMenu.context.executeAsync {
|
||||||
feature(Messaging::class).apply {
|
messageLogger.deleteMessage(messaging.openedConversationUUID.toString(), messaging.lastFocusedMessageId)
|
||||||
feature(MessageLogger::class).deleteMessage(openedConversationUUID.toString(), 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)
|
parent.addView(buttonContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,12 @@ import android.widget.Switch
|
|||||||
import me.rhunk.snapenhance.core.database.objects.ConversationMessage
|
import me.rhunk.snapenhance.core.database.objects.ConversationMessage
|
||||||
import me.rhunk.snapenhance.core.database.objects.FriendInfo
|
import me.rhunk.snapenhance.core.database.objects.FriendInfo
|
||||||
import me.rhunk.snapenhance.core.database.objects.UserConversationLink
|
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.core.util.snap.BitmojiSelfie
|
||||||
import me.rhunk.snapenhance.data.ContentType
|
import me.rhunk.snapenhance.data.ContentType
|
||||||
import me.rhunk.snapenhance.data.FriendLinkType
|
import me.rhunk.snapenhance.data.FriendLinkType
|
||||||
import me.rhunk.snapenhance.features.impl.Messaging
|
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.ViewAppearanceHelper
|
||||||
import me.rhunk.snapenhance.ui.applyTheme
|
import me.rhunk.snapenhance.ui.applyTheme
|
||||||
import me.rhunk.snapenhance.ui.menu.AbstractMenu
|
import me.rhunk.snapenhance.ui.menu.AbstractMenu
|
||||||
@ -102,15 +104,11 @@ class FriendFeedInfoMenu : AbstractMenu() {
|
|||||||
|
|
||||||
private fun showPreview(userId: String?, conversationId: String, androidCtx: Context?) {
|
private fun showPreview(userId: String?, conversationId: String, androidCtx: Context?) {
|
||||||
//query message
|
//query message
|
||||||
val messages: List<ConversationMessage>? = context.database.getMessagesFromConversationId(
|
val messageLogger = context.feature(MessageLogger::class)
|
||||||
|
val messages: List<ConversationMessage> = context.database.getMessagesFromConversationId(
|
||||||
conversationId,
|
conversationId,
|
||||||
context.config.messaging.messagePreviewLength.get()
|
context.config.messaging.messagePreviewLength.get()
|
||||||
)?.reversed()
|
)?.reversed() ?: emptyList()
|
||||||
|
|
||||||
if (messages == null) {
|
|
||||||
context.longToast("Can't fetch messages")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val participants: Map<String, FriendInfo> = context.database.getConversationParticipants(conversationId)!!
|
val participants: Map<String, FriendInfo> = context.database.getConversationParticipants(conversationId)!!
|
||||||
.map { context.database.getFriendInfo(it)!! }
|
.map { context.database.getFriendInfo(it)!! }
|
||||||
@ -119,19 +117,26 @@ class FriendFeedInfoMenu : AbstractMenu() {
|
|||||||
val messageBuilder = StringBuilder()
|
val messageBuilder = StringBuilder()
|
||||||
|
|
||||||
messages.forEach { message ->
|
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) {
|
if (contentType == ContentType.SNAP) {
|
||||||
val readTimeStamp: Long = message.readTimestamp
|
|
||||||
messageString = "\uD83D\uDFE5" //red square
|
messageString = "\uD83D\uDFE5" //red square
|
||||||
if (readTimeStamp > 0) {
|
if (message.readTimestamp > 0) {
|
||||||
messageString += " \uD83D\uDC40 " //eyes
|
messageString += " \uD83D\uDC40 " //eyes
|
||||||
messageString += DateFormat.getDateTimeInstance(
|
messageString += DateFormat.getDateTimeInstance(
|
||||||
DateFormat.SHORT,
|
DateFormat.SHORT,
|
||||||
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")
|
messageBuilder.append(displayUsername).append(": ").append(messageString).append("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
val targetPerson: FriendInfo? =
|
val targetPerson = if (userId == null) null else participants[userId]
|
||||||
if (userId == null) null else participants[userId]
|
|
||||||
|
|
||||||
targetPerson?.streakExpirationTimestamp?.takeIf { it > 0 }?.let {
|
targetPerson?.streakExpirationTimestamp?.takeIf { it > 0 }?.let {
|
||||||
val timeSecondDiff = ((it - System.currentTimeMillis()) / 1000 / 60).toInt()
|
val timeSecondDiff = ((it - System.currentTimeMillis()) / 1000 / 60).toInt()
|
||||||
|
Reference in New Issue
Block a user