many fixes & improvements

This commit is contained in:
particle-box
2026-03-08 17:19:38 +05:30
parent 4adcff36e3
commit 07d4a4f833
13 changed files with 419 additions and 65 deletions

View File

@@ -83,16 +83,37 @@ class BridgeService : Service() {
callback.syncGroup(id)
}
} ?: run {
if (updateOnly) {
when (scope) {
SocialScope.FRIEND -> database.deleteFriend(id)
SocialScope.GROUP -> database.deleteGroup(id)
}
return
}
remoteSideContext.log.warn("Failed to sync $scope $id")
return
}
when (scope) {
SocialScope.FRIEND -> {
toParcelable<MessagingFriendInfo>(syncedObject)?.let { database.syncFriend(it) }
toParcelable<MessagingFriendInfo>(syncedObject)?.let { database.syncFriend(it) } ?: run {
if (updateOnly) {
database.deleteFriend(id)
return
}
remoteSideContext.log.warn("Failed to sync $scope $id")
return
}
}
SocialScope.GROUP -> {
toParcelable<MessagingGroupInfo>(syncedObject)?.let { database.syncGroupInfo(it) }
toParcelable<MessagingGroupInfo>(syncedObject)?.let { database.syncGroupInfo(it) } ?: run {
if (updateOnly) {
database.deleteGroup(id)
return
}
remoteSideContext.log.warn("Failed to sync $scope $id")
return
}
}
}
}.onFailure {
@@ -218,10 +239,10 @@ class BridgeService : Service() {
friends: List<String>
) {
remoteSideContext.log.verbose("Received ${groups.size} groups and ${friends.size} friends")
remoteSideContext.database.receiveMessagingDataCallback(
friends.mapNotNull { toParcelable<MessagingFriendInfo>(it) },
groups.mapNotNull { toParcelable<MessagingGroupInfo>(it) }
)
val parsedFriends = friends.mapNotNull { toParcelable<MessagingFriendInfo>(it) }
val parsedGroups = groups.mapNotNull { toParcelable<MessagingGroupInfo>(it) }
remoteSideContext.database.replaceMessagingData(parsedFriends, parsedGroups)
remoteSideContext.database.receiveMessagingDataCallback(parsedFriends, parsedGroups)
}
override fun getScopeNotes(id: String): String? {

View File

@@ -86,6 +86,76 @@ fun AppDatabase.syncFriend(friend: MessagingFriendInfo) {
}
}
fun AppDatabase.replaceMessagingData(
friends: List<MessagingFriendInfo>,
groups: List<MessagingGroupInfo>
) {
executeAsync {
database.beginTransaction()
try {
val friendIds = friends.map { it.userId }.toSet()
val groupIds = groups.map { it.conversationId }.toSet()
getFriends().forEach { friend ->
if (friend.userId !in friendIds) {
database.execSQL("DELETE FROM friends WHERE userId = ?", arrayOf(friend.userId))
database.execSQL("DELETE FROM streaks WHERE id = ?", arrayOf(friend.userId))
database.execSQL("DELETE FROM rules WHERE targetUuid = ?", arrayOf(friend.userId))
}
}
getGroups().forEach { group ->
if (group.conversationId !in groupIds) {
database.execSQL("DELETE FROM groups WHERE conversationId = ?", arrayOf(group.conversationId))
database.execSQL("DELETE FROM rules WHERE targetUuid = ?", arrayOf(group.conversationId))
}
}
friends.forEach { friend ->
database.execSQL(
"INSERT OR REPLACE INTO friends (userId, dmConversationId, displayName, mutableUsername, bitmojiId, selfieId) VALUES (?, ?, ?, ?, ?, ?)",
arrayOf<Any?>(
friend.userId,
friend.dmConversationId,
friend.displayName,
friend.mutableUsername,
friend.bitmojiId,
friend.selfieId
)
)
friend.streaks?.takeIf { it.length > 0 }?.also {
val streaks = getFriendStreaks(friend.userId)
database.execSQL(
"INSERT OR REPLACE INTO streaks (id, notify, expirationTimestamp, length) VALUES (?, ?, ?, ?)",
arrayOf<Any?>(
friend.userId,
streaks?.notify != false,
it.expirationTimestamp,
it.length
)
)
} ?: database.execSQL("DELETE FROM streaks WHERE id = ?", arrayOf(friend.userId))
}
groups.forEach { group ->
database.execSQL(
"INSERT OR REPLACE INTO groups (conversationId, name, participantsCount) VALUES (?, ?, ?)",
arrayOf<Any?>(
group.conversationId,
group.name,
group.participantsCount
)
)
}
database.setTransactionSuccessful()
} finally {
database.endTransaction()
}
}
}
fun AppDatabase.getRules(targetUuid: String): List<MessagingRuleType> {
return database.rawQuery(
"SELECT type FROM rules WHERE targetUuid = ?", arrayOf(targetUuid)

View File

@@ -37,11 +37,13 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavBackStackEntry
import kotlinx.coroutines.launch
import me.eternal.purrfectsnap.R
import me.eternal.purrfectsnap.common.ReceiversConfig
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
import me.eternal.purrfectsnap.common.data.MessagingGroupInfo
import me.eternal.purrfectsnap.common.data.SocialScope
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie
import me.eternal.purrfectsnap.common.util.snap.SnapWidgetBroadcastReceiverHelper
import me.eternal.purrfectsnap.storage.*
import me.eternal.purrfectsnap.ui.manager.Routes
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
@@ -58,6 +60,16 @@ class SocialRootSection : Routes.Route() {
}
}
private fun requestLatestSnapshot() {
runCatching {
context.androidContext.sendBroadcast(
SnapWidgetBroadcastReceiverHelper.create(ReceiversConfig.BRIDGE_SYNC_ACTION) {}
)
}.onFailure {
context.log.error("Failed to request latest social snapshot", it)
}
}
@Composable
private fun ScopeList(
scope: SocialScope,
@@ -196,7 +208,17 @@ class SocialRootSection : Routes.Route() {
var searchActive by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
context.database.receiveMessagingDataCallback = { friends, groups ->
friendList = friends
groupList = groups
}
updateScopeLists()
requestLatestSnapshot()
}
DisposableEffect(Unit) {
onDispose {
context.database.receiveMessagingDataCallback = { _, _ -> }
}
}
val normalizedQuery = remember(searchQuery) { searchQuery.trim() }
val filteredFriends = remember(friendList, normalizedQuery) {

View File

@@ -54,6 +54,9 @@ class MappingsWrapper(
if (!exists()) {
throw Exception("Mappings file does not exist")
}
mappers.values.forEach { mapper ->
mapper.classLoader = context.classLoader
}
val mappingsObject = JsonParser.parseString(readBytes().toString(Charsets.UTF_8)).asJsonObject.also {
mappingUniqueHash = it["unique_hash"].asLong
}
@@ -61,7 +64,6 @@ class MappingsWrapper(
mappingsObject.entrySet().forEach { (key, value) ->
mappers.values.firstOrNull { it.mapperName == key }?.let { mapper ->
mapper.readFromJson(value.asJsonObject)
mapper.classLoader = context.classLoader
}
}
isMappingsLoaded = true
@@ -74,6 +76,9 @@ class MappingsWrapper(
fileHandleManager.value.getFileHandle(FileHandleScope.INTERNAL.key, InternalFileHandleType.NATIVE_SIG_CACHE.key).delete()
val classMapper = ClassMapper(*mappers.values.toTypedArray())
mappers.values.forEach { mapper ->
mapper.classLoader = context.classLoader
}
runCatching {
classMapper.loadApk(getSnapchatPackageInfo()?.applicationInfo?.sourceDir ?: throw Exception("Failed to get APK"))
@@ -88,6 +93,8 @@ class MappingsWrapper(
writeBytes(result.toString().toByteArray())
}
loadCached()
return classMapper.getWarns()
}
@@ -99,4 +106,4 @@ class MappingsWrapper(
AbstractLogger.directError("Mapper ${type.simpleName} is not registered", Throwable())
}
}
}
}

View File

@@ -17,6 +17,7 @@ import me.eternal.purrfectsnap.bridge.SyncCallback
import me.eternal.purrfectsnap.common.Constants
import me.eternal.purrfectsnap.common.ReceiversConfig
import me.eternal.purrfectsnap.common.action.EnumAction
import me.eternal.purrfectsnap.common.data.FriendLinkType
import me.eternal.purrfectsnap.common.bridge.FileHandleScope
import me.eternal.purrfectsnap.common.bridge.InternalFileHandleType
import me.eternal.purrfectsnap.common.bridge.toWrapper
@@ -454,21 +455,29 @@ class PurrfectSnap {
)
}
val friends = feedEntries.filter { it.conversationType == 0 }.mapNotNull {
val friendUserId = it.friendUserId ?: it.participants?.firstOrNull { it != appContext.database.myUserId }
?: return@mapNotNull null
val friend = appContext.database.getFriendInfo(friendUserId) ?: return@mapNotNull null
MessagingFriendInfo(
friendUserId,
it.key,
friend.displayName,
friend.mutableUsername ?: friend.usernameForSorting!!,
friend.bitmojiAvatarId,
friend.bitmojiSelfieId,
streaks = null
)
}
val friends = appContext.database.getAllFriends()
.asSequence()
.filter { friend ->
friend.userId != null && when (FriendLinkType.fromValue(friend.friendLinkType)) {
FriendLinkType.DELETED,
FriendLinkType.BLOCKED,
FriendLinkType.SUGGESTED -> false
else -> true
}
}
.mapNotNull { friend ->
val userId = friend.userId ?: return@mapNotNull null
MessagingFriendInfo(
userId = userId,
dmConversationId = appContext.database.getDMConversationId(userId),
displayName = friend.displayName,
mutableUsername = friend.mutableUsername ?: friend.usernameForSorting ?: return@mapNotNull null,
bitmojiId = friend.bitmojiAvatarId,
selfieId = friend.bitmojiSelfieId,
streaks = null
)
}
.toList()
appContext.bridgeClient.passGroupsAndFriends(groups, friends)
}
@@ -485,9 +494,15 @@ class PurrfectSnap {
appContext.bridgeClient.sync(object : SyncCallback.Stub() {
override fun syncFriend(uuid: String): String? {
return appContext.database.getFriendInfo(uuid)?.let {
if (FriendLinkType.fromValue(it.friendLinkType) in setOf(
FriendLinkType.DELETED,
FriendLinkType.BLOCKED,
FriendLinkType.SUGGESTED
)
) return@let null
MessagingFriendInfo(
userId = it.userId!!,
dmConversationId = null,
dmConversationId = appContext.database.getDMConversationId(it.userId!!),
displayName = it.displayName,
mutableUsername = it.mutableUsername!!,
bitmojiId = it.bitmojiAvatarId,

View File

@@ -27,7 +27,9 @@ abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleTyp
).contains(ruleType) && getRuleState() != null
fun canUseRule(conversationId: String): Boolean {
if (getRuleState() == null) return false
if (ruleType.key == "translation" && context.config.messaging.instantTranslation.globalState != true) {
return false
}
val state = getState(conversationId)
if (context.config.rules.getRuleState(ruleType) == RuleState.BLACKLIST) {
return !state

View File

@@ -4,13 +4,31 @@ import me.eternal.purrfectsnap.common.data.MessagingRuleType
import me.eternal.purrfectsnap.core.features.MessagingRuleFeature
import me.eternal.purrfectsnap.core.util.hook.HookStage
import me.eternal.purrfectsnap.core.util.hook.hook
import me.eternal.purrfectsnap.core.wrapper.impl.SnapUUID
class HideTypingIndicator : MessagingRuleFeature("Hide Typing Indicator", MessagingRuleType.HIDE_TYPING_INDICATOR) {
private val messaging: Messaging by lazy { context.feature(Messaging::class) }
private fun shouldHideTypingIndicator(conversationId: String?): Boolean {
return conversationId?.let { canUseRule(it) } ?: false
}
private fun currentConversationId(): String? {
return messaging.openedConversationUUID?.toString()
}
override fun init() {
context.classCache.presenceSession.hook("processTypingActivity", HookStage.BEFORE, {
messaging.openedConversationUUID?.toString()?.let { canUseRule(it) } ?: false
shouldHideTypingIndicator(currentConversationId())
}) {
it.setResult(null)
}
context.classCache.conversationManager.hook("sendTypingNotification", HookStage.BEFORE, { param ->
val conversationId = currentConversationId() ?: param.argNullable<Any>(0)?.let {
runCatching { SnapUUID(it).toString() }.getOrNull()
}
shouldHideTypingIndicator(conversationId)
}) {
it.setResult(null)
}

View File

@@ -17,6 +17,7 @@ import me.eternal.purrfectsnap.core.util.ktx.getObjectFieldOrNull
import me.eternal.purrfectsnap.core.wrapper.impl.*
import me.eternal.purrfectsnap.mapper.impl.CallbackMapper
import me.eternal.purrfectsnap.mapper.impl.FriendsFeedEventDispatcherMapper
import me.eternal.purrfectsnap.mapper.impl.PlatformPresenceActionWrapperMapper
import java.util.UUID
import java.util.concurrent.Future
@@ -52,8 +53,37 @@ class Messaging : Feature("Messaging") {
lastFocusedConversationType = -1
}
private fun currentConversationId(): String? = openedConversationUUID?.toString()
private fun shouldHideBitmojiPresence(stealthMode: StealthMode): Boolean {
return context.config.messaging.hideBitmojiPresence.get() ||
currentConversationId()?.let { stealthMode.canUseRule(it) } == true
}
private fun shouldHideTyping(stealthMode: StealthMode, hideTypingIndicator: HideTypingIndicator): Boolean {
return context.config.messaging.hideTypingNotifications.get() ||
currentConversationId()?.let { stealthMode.canUseRule(it) || hideTypingIndicator.canUseRule(it) } == true
}
private fun shouldHidePeek(stealthMode: StealthMode): Boolean {
return context.config.messaging.hidePeekAPeek.get() ||
currentConversationId()?.let { stealthMode.canUseRule(it) } == true
}
private fun clearField(instance: Any, typeNamePart: String, shouldClear: Boolean) {
if (!shouldClear) return
instance.javaClass.declaredFields.forEach { field ->
if (field.type.name.contains(typeNamePart)) {
field.isAccessible = true
field.set(instance, null)
}
}
}
override fun init() {
val stealthMode = context.feature(StealthMode::class)
val hideTypingIndicator = context.feature(HideTypingIndicator::class)
context.classCache.conversationManager.hookConstructor(HookStage.BEFORE) { param ->
synchronized(conversationManagerReadyListeners) {
conversationManager = ConversationManager(context, param.thisObject())
@@ -98,20 +128,100 @@ class Messaging : Feature("Messaging") {
}
defer {
arrayOf("activate", "deactivate").forEach { hook ->
arrayOf("activate", "deactivate", "processTypingActivity").forEach { hook ->
context.classCache.presenceSession.hook(hook, HookStage.BEFORE, {
val conversationId = openedConversationUUID?.toString() ?: return@hook false
context.config.messaging.hideBitmojiPresence.get() || stealthMode.canUseRule(conversationId)
shouldHideBitmojiPresence(stealthMode)
}) {
it.setResult(null)
}
}
context.classCache.presenceSession.hook("startPeeking", HookStage.BEFORE, {
val conversationId = openedConversationUUID?.toString() ?: return@hook false
context.config.messaging.hidePeekAPeek.get() || stealthMode.canUseRule(conversationId)
shouldHidePeek(stealthMode)
}) { it.setResult(null) }
context.classCache.conversationManager.hook("sendTypingNotification", HookStage.BEFORE, {
shouldHideTyping(stealthMode, hideTypingIndicator)
}) {
it.setResult(null)
}
context.mappings.useMapper(PlatformPresenceActionWrapperMapper::class) {
classLoader = context.androidContext.classLoader
if (classReference.getAsClass() == null) {
runCatching { context.mappings.refresh() }.onFailure {
context.log.error("Failed to refresh mappings for PlatformPresenceActionWrapper", it)
}
}
classReference.getAsClass()?.let { wrapperClass ->
val bitmojiMethodNames = mutableSetOf<String>()
val typingMethodNames = mutableSetOf<String>()
val peekingMethodNames = mutableSetOf<String>()
wrapperClass.methods.forEach { method ->
val parameterTypes = method.parameterTypes
if (parameterTypes.any { parameterType ->
listOf(
"PlatformChatVisibleAction",
"PlatformChatHiddenAction",
"PlatformViewingChatMediaAction",
"PlatformUsingReplyCameraAction"
).any { parameterType.name.contains(it) }
}) {
bitmojiMethodNames.add(method.name)
}
if (parameterTypes.any { parameterType ->
parameterType.name.contains("PlatformTypingAction")
}) {
typingMethodNames.add(method.name)
}
if (parameterTypes.any { parameterType ->
parameterType.name.contains("PlatformStartPeekingAction")
}) {
peekingMethodNames.add(method.name)
}
}
bitmojiMethodNames.forEach { methodName ->
wrapperClass.hook(methodName, HookStage.BEFORE, {
shouldHideBitmojiPresence(stealthMode)
}) {
it.setResult(null)
}
}
typingMethodNames.forEach { methodName ->
wrapperClass.hook(methodName, HookStage.BEFORE, {
shouldHideTyping(stealthMode, hideTypingIndicator)
}) {
it.setResult(null)
}
}
peekingMethodNames.forEach { methodName ->
wrapperClass.hook(methodName, HookStage.BEFORE, {
shouldHidePeek(stealthMode)
}) {
it.setResult(null)
}
}
wrapperClass.hookConstructor(HookStage.AFTER) { param ->
val instance = param.thisObject<Any>()
clearField(instance, "PlatformChatVisibleAction", shouldHideBitmojiPresence(stealthMode))
clearField(instance, "PlatformChatHiddenAction", shouldHideBitmojiPresence(stealthMode))
clearField(instance, "PlatformViewingChatMediaAction", shouldHideBitmojiPresence(stealthMode))
clearField(instance, "PlatformUsingReplyCameraAction", shouldHideBitmojiPresence(stealthMode))
clearField(instance, "PlatformTypingAction", shouldHideTyping(stealthMode, hideTypingIndicator))
clearField(instance, "PlatformStartPeekingAction", shouldHidePeek(stealthMode))
}
}
}
//get last opened snap for media downloader
context.event.subscribe(OnSnapInteractionEvent::class) { event ->
openedConversationUUID = event.conversationId

View File

@@ -22,7 +22,6 @@ class StealthMode : MessagingRuleFeature("StealthMode", MessagingRuleType.STEALT
override fun init() {
if (getRuleState() == null) return
val isConversationInStealthMode: (SnapUUID) -> Boolean = { canUseRule(it.toString()) }
arrayOf("mediaMessagesDisplayed", "displayedMessages").forEach { methodName: String ->

View File

@@ -27,16 +27,18 @@ class CameraTweaks : Feature("Camera Tweaks") {
override fun init() {
val config = context.config.camera
config.startupDefaultCamera.getNullable()?.let { defaultCamera ->
context.database.setCameraType(if (defaultCamera == "back") "BACK_FACING" else "FRONT_FACING")
}
val frontCameraId by lazy {
runCatching { context.androidContext.getSystemService(CameraManager::class.java).run {
cameraIdList.firstOrNull { getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT }
} }.getOrNull()
}
val backCameraId by lazy {
runCatching { context.androidContext.getSystemService(CameraManager::class.java).run {
cameraIdList.firstOrNull { getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK }
} }.getOrNull()
}
if (config.disableCameras.get().isNotEmpty() && frontCameraId != null) {
ContextWrapper::class.java.hook("checkPermission", HookStage.BEFORE) { param ->
val permission = param.arg<String>(0)
@@ -47,11 +49,24 @@ class CameraTweaks : Feature("Camera Tweaks") {
}
var isLastCameraFront = false
var startupCameraApplied = false
CameraManager::class.java.hook("openCamera", HookStage.BEFORE) { param ->
val cameraManager = param.thisObject() as? CameraManager ?: return@hook
val cameraId = param.arg<String>(0)
var cameraId = param.arg<String>(0)
val disabledCameras = config.disableCameras.get()
val startupDefaultCamera = config.startupDefaultCamera.getNullable()
if (startupDefaultCamera != null && !startupCameraApplied) {
val preferredCameraId = if (startupDefaultCamera == "back") backCameraId else frontCameraId
if (preferredCameraId != null) {
if (preferredCameraId != cameraId) {
param.setArg(0, preferredCameraId)
cameraId = preferredCameraId
}
startupCameraApplied = true
}
}
if (disabledCameras.size >= 2) {
param.setResult(null)

View File

@@ -31,41 +31,97 @@ class HideFriendFeedEntry : MessagingRuleFeature("HideFriendFeedEntry", ruleType
}
}
private fun hookCallbackMethod(
hookedCallbacks: MutableSet<String>,
callbackClassName: String,
methodName: String,
block: (param: me.eternal.purrfectsnap.core.util.hook.HookAdapter) -> Unit
) {
val hookKey = "$callbackClassName#$methodName"
if (!hookedCallbacks.add(hookKey)) return
runCatching {
findClass(callbackClassName).hook(methodName, HookStage.BEFORE) { param ->
block(param)
}
}.onFailure {
context.log.warn("Failed to hook $methodName on $callbackClassName")
}
}
override fun init() {
if (!context.config.userInterface.hideFriendFeedEntry.get()) return
context.mappings.useMapper(CallbackMapper::class) {
arrayOf(
"FetchAndSyncFeedWithConversationIdsCallback" to "onFetchAndSyncFeedComplete",
"FetchFeedCallback" to "onFetchFeedComplete",
"FetchFeedEntriesCallback" to "onFetchFeedEntriesComplete",
"QueryFeedCallback" to "onQueryFeedComplete",
"FeedManagerDelegate" to "onFeedEntriesUpdated",
"FeedManagerDelegate" to "onInternalSyncFeed",
).forEach { (callbackName, methodName) ->
findClass(callbacks.get()!![callbackName] ?: return@forEach).hook(methodName, HookStage.BEFORE) { param ->
filterFriendFeed(param.arg(0))
classLoader = context.androidContext.classLoader
val hasFetchAndSyncCallback = callbacks.getAsMap()?.entries?.any {
it.key.startsWith("FetchAndSyncFeed") && it.key.endsWith("Callback")
} == true
if (callbacks.getClass("SyncFeedCallback") == null || !hasFetchAndSyncCallback) {
runCatching { context.mappings.refresh() }.onFailure {
context.log.error("Failed to refresh mappings for HideFriendFeedEntry callbacks", it)
}
classLoader = context.androidContext.classLoader
}
val callbackMap = callbacks.getAsMap().orEmpty()
val hookedCallbacks = mutableSetOf<String>()
callbackMap.entries.forEach { (callbackName, callbackClassName) ->
when {
callbackName.startsWith("FetchAndSyncFeed") && callbackName.endsWith("Callback") -> {
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFetchAndSyncFeedComplete") { param ->
val deletedConversations: ArrayList<Any> = param.arg(2)
filterFriendFeed(param.arg(0), deletedConversations)
if (deletedConversations.any {
val uuid = SnapUUID(it.getObjectField("mFeedEntryIdentifier")?.getObjectField("mConversationId")).toString()
context.database.getFeedEntryByConversationId(uuid) != null
}) {
param.setArg(4, true)
}
}
}
callbackName.contains("SyncFeed") && callbackName.endsWith("Callback") -> {
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onSyncFeedComplete") { param ->
filterFriendFeed(param.arg(0), param.argNullable(2))
}
}
callbackName == "FetchFeedCallback" || callbackName.contains("FetchFeedCallback") -> {
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFetchFeedComplete") { param ->
filterFriendFeed(param.arg(0))
}
}
callbackName == "FetchFeedEntriesCallback" || callbackName.contains("FetchFeedEntriesCallback") -> {
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFetchFeedEntriesComplete") { param ->
filterFriendFeed(param.arg(0))
}
}
callbackName == "QueryFeedCallback" || callbackName.contains("QueryFeedCallback") -> {
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onQueryFeedComplete") { param ->
filterFriendFeed(param.arg(0))
}
}
callbackName == "FeedManagerDelegate" -> {
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onFeedEntriesUpdated") { param ->
filterFriendFeed(param.arg(0))
}
hookCallbackMethod(hookedCallbacks, callbackClassName ?: return@forEach, "onInternalSyncFeed") { param ->
filterFriendFeed(param.arg(0))
}
}
}
}
callbacks.getAsMap()?.entries?.firstOrNull { it.key.startsWith("FetchAndSyncFeed") && it.key.endsWith("Callback") }
?.value
?.let { findClass(it) }
?.hook("onFetchAndSyncFeedComplete", HookStage.BEFORE) { param ->
val deletedConversations: ArrayList<Any> = param.arg(2)
filterFriendFeed(param.arg(0), deletedConversations)
if (deletedConversations.any {
val uuid = SnapUUID(it.getObjectField("mFeedEntryIdentifier")?.getObjectField("mConversationId")).toString()
context.database.getFeedEntryByConversationId(uuid) != null
}) {
param.setArg(4, true)
}
} ?: context.log.warn("Failed to hook FetchAndSyncFeedCallback")
callbacks.getClass("SyncFeedCallback")
?.hook("onSyncFeedComplete", HookStage.BEFORE) { param ->
filterFriendFeed(param.arg(0), param.arg(2))
} ?: context.log.warn("Failed to hook SyncFeedCallback")
if (callbackMap.entries.none { it.key.startsWith("FetchAndSyncFeed") && it.key.endsWith("Callback") }) {
context.log.warn("Failed to hook FetchAndSyncFeedCallback")
}
if (callbackMap.entries.none { it.key.contains("SyncFeed") && it.key.endsWith("Callback") }) {
context.log.warn("Failed to hook SyncFeedCallback")
}
}
}

View File

@@ -41,6 +41,7 @@ class ClassMapper(
FoldingLayoutMapper(),
PlatformClientAttestationMapper(),
ChatMediaDrawerMapper(),
PlatformPresenceActionWrapperMapper(),
)
}

View File

@@ -0,0 +1,18 @@
package me.eternal.purrfectsnap.mapper.impl
import me.eternal.purrfectsnap.mapper.AbstractClassMapper
import me.eternal.purrfectsnap.mapper.ext.getClassName
class PlatformPresenceActionWrapperMapper : AbstractClassMapper("PlatformPresenceActionWrapper") {
val classReference = classReference("class")
init {
mapper {
classes.firstOrNull { classDef ->
classDef.fields.any { field ->
field.type == "Lcom/snap/presence/PlatformChatVisibleAction;"
}
}?.let { classReference.set(it.getClassName()) }
}
}
}