mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-12 05:07:46 +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:
@ -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();
|
||||
|
@ -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",
|
||||
"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"
|
||||
|
@ -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"
|
||||
}
|
@ -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)!!
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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!!)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
Reference in New Issue
Block a user