many fixes & improvements
This commit is contained in:
@@ -83,16 +83,37 @@ class BridgeService : Service() {
|
|||||||
callback.syncGroup(id)
|
callback.syncGroup(id)
|
||||||
}
|
}
|
||||||
} ?: run {
|
} ?: run {
|
||||||
|
if (updateOnly) {
|
||||||
|
when (scope) {
|
||||||
|
SocialScope.FRIEND -> database.deleteFriend(id)
|
||||||
|
SocialScope.GROUP -> database.deleteGroup(id)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
remoteSideContext.log.warn("Failed to sync $scope $id")
|
remoteSideContext.log.warn("Failed to sync $scope $id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
when (scope) {
|
when (scope) {
|
||||||
SocialScope.FRIEND -> {
|
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 -> {
|
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 {
|
}.onFailure {
|
||||||
@@ -218,10 +239,10 @@ class BridgeService : Service() {
|
|||||||
friends: List<String>
|
friends: List<String>
|
||||||
) {
|
) {
|
||||||
remoteSideContext.log.verbose("Received ${groups.size} groups and ${friends.size} friends")
|
remoteSideContext.log.verbose("Received ${groups.size} groups and ${friends.size} friends")
|
||||||
remoteSideContext.database.receiveMessagingDataCallback(
|
val parsedFriends = friends.mapNotNull { toParcelable<MessagingFriendInfo>(it) }
|
||||||
friends.mapNotNull { toParcelable<MessagingFriendInfo>(it) },
|
val parsedGroups = groups.mapNotNull { toParcelable<MessagingGroupInfo>(it) }
|
||||||
groups.mapNotNull { toParcelable<MessagingGroupInfo>(it) }
|
remoteSideContext.database.replaceMessagingData(parsedFriends, parsedGroups)
|
||||||
)
|
remoteSideContext.database.receiveMessagingDataCallback(parsedFriends, parsedGroups)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getScopeNotes(id: String): String? {
|
override fun getScopeNotes(id: String): String? {
|
||||||
|
|||||||
@@ -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> {
|
fun AppDatabase.getRules(targetUuid: String): List<MessagingRuleType> {
|
||||||
return database.rawQuery(
|
return database.rawQuery(
|
||||||
"SELECT type FROM rules WHERE targetUuid = ?", arrayOf(targetUuid)
|
"SELECT type FROM rules WHERE targetUuid = ?", arrayOf(targetUuid)
|
||||||
|
|||||||
@@ -37,11 +37,13 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.eternal.purrfectsnap.R
|
import me.eternal.purrfectsnap.R
|
||||||
|
import me.eternal.purrfectsnap.common.ReceiversConfig
|
||||||
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
|
import me.eternal.purrfectsnap.common.data.MessagingFriendInfo
|
||||||
import me.eternal.purrfectsnap.common.data.MessagingGroupInfo
|
import me.eternal.purrfectsnap.common.data.MessagingGroupInfo
|
||||||
import me.eternal.purrfectsnap.common.data.SocialScope
|
import me.eternal.purrfectsnap.common.data.SocialScope
|
||||||
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState
|
import me.eternal.purrfectsnap.common.ui.rememberAsyncMutableState
|
||||||
import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie
|
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.storage.*
|
||||||
import me.eternal.purrfectsnap.ui.manager.Routes
|
import me.eternal.purrfectsnap.ui.manager.Routes
|
||||||
import me.eternal.purrfectsnap.ui.manager.theme.PurrfectPalette
|
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
|
@Composable
|
||||||
private fun ScopeList(
|
private fun ScopeList(
|
||||||
scope: SocialScope,
|
scope: SocialScope,
|
||||||
@@ -196,7 +208,17 @@ class SocialRootSection : Routes.Route() {
|
|||||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
context.database.receiveMessagingDataCallback = { friends, groups ->
|
||||||
|
friendList = friends
|
||||||
|
groupList = groups
|
||||||
|
}
|
||||||
updateScopeLists()
|
updateScopeLists()
|
||||||
|
requestLatestSnapshot()
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
context.database.receiveMessagingDataCallback = { _, _ -> }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val normalizedQuery = remember(searchQuery) { searchQuery.trim() }
|
val normalizedQuery = remember(searchQuery) { searchQuery.trim() }
|
||||||
val filteredFriends = remember(friendList, normalizedQuery) {
|
val filteredFriends = remember(friendList, normalizedQuery) {
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ class MappingsWrapper(
|
|||||||
if (!exists()) {
|
if (!exists()) {
|
||||||
throw Exception("Mappings file does not exist")
|
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 {
|
val mappingsObject = JsonParser.parseString(readBytes().toString(Charsets.UTF_8)).asJsonObject.also {
|
||||||
mappingUniqueHash = it["unique_hash"].asLong
|
mappingUniqueHash = it["unique_hash"].asLong
|
||||||
}
|
}
|
||||||
@@ -61,7 +64,6 @@ class MappingsWrapper(
|
|||||||
mappingsObject.entrySet().forEach { (key, value) ->
|
mappingsObject.entrySet().forEach { (key, value) ->
|
||||||
mappers.values.firstOrNull { it.mapperName == key }?.let { mapper ->
|
mappers.values.firstOrNull { it.mapperName == key }?.let { mapper ->
|
||||||
mapper.readFromJson(value.asJsonObject)
|
mapper.readFromJson(value.asJsonObject)
|
||||||
mapper.classLoader = context.classLoader
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isMappingsLoaded = true
|
isMappingsLoaded = true
|
||||||
@@ -74,6 +76,9 @@ class MappingsWrapper(
|
|||||||
fileHandleManager.value.getFileHandle(FileHandleScope.INTERNAL.key, InternalFileHandleType.NATIVE_SIG_CACHE.key).delete()
|
fileHandleManager.value.getFileHandle(FileHandleScope.INTERNAL.key, InternalFileHandleType.NATIVE_SIG_CACHE.key).delete()
|
||||||
|
|
||||||
val classMapper = ClassMapper(*mappers.values.toTypedArray())
|
val classMapper = ClassMapper(*mappers.values.toTypedArray())
|
||||||
|
mappers.values.forEach { mapper ->
|
||||||
|
mapper.classLoader = context.classLoader
|
||||||
|
}
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
classMapper.loadApk(getSnapchatPackageInfo()?.applicationInfo?.sourceDir ?: throw Exception("Failed to get APK"))
|
classMapper.loadApk(getSnapchatPackageInfo()?.applicationInfo?.sourceDir ?: throw Exception("Failed to get APK"))
|
||||||
@@ -88,6 +93,8 @@ class MappingsWrapper(
|
|||||||
writeBytes(result.toString().toByteArray())
|
writeBytes(result.toString().toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadCached()
|
||||||
|
|
||||||
return classMapper.getWarns()
|
return classMapper.getWarns()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,4 +106,4 @@ class MappingsWrapper(
|
|||||||
AbstractLogger.directError("Mapper ${type.simpleName} is not registered", Throwable())
|
AbstractLogger.directError("Mapper ${type.simpleName} is not registered", Throwable())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import me.eternal.purrfectsnap.bridge.SyncCallback
|
|||||||
import me.eternal.purrfectsnap.common.Constants
|
import me.eternal.purrfectsnap.common.Constants
|
||||||
import me.eternal.purrfectsnap.common.ReceiversConfig
|
import me.eternal.purrfectsnap.common.ReceiversConfig
|
||||||
import me.eternal.purrfectsnap.common.action.EnumAction
|
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.FileHandleScope
|
||||||
import me.eternal.purrfectsnap.common.bridge.InternalFileHandleType
|
import me.eternal.purrfectsnap.common.bridge.InternalFileHandleType
|
||||||
import me.eternal.purrfectsnap.common.bridge.toWrapper
|
import me.eternal.purrfectsnap.common.bridge.toWrapper
|
||||||
@@ -454,21 +455,29 @@ class PurrfectSnap {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val friends = feedEntries.filter { it.conversationType == 0 }.mapNotNull {
|
val friends = appContext.database.getAllFriends()
|
||||||
val friendUserId = it.friendUserId ?: it.participants?.firstOrNull { it != appContext.database.myUserId }
|
.asSequence()
|
||||||
?: return@mapNotNull null
|
.filter { friend ->
|
||||||
val friend = appContext.database.getFriendInfo(friendUserId) ?: return@mapNotNull null
|
friend.userId != null && when (FriendLinkType.fromValue(friend.friendLinkType)) {
|
||||||
|
FriendLinkType.DELETED,
|
||||||
MessagingFriendInfo(
|
FriendLinkType.BLOCKED,
|
||||||
friendUserId,
|
FriendLinkType.SUGGESTED -> false
|
||||||
it.key,
|
else -> true
|
||||||
friend.displayName,
|
}
|
||||||
friend.mutableUsername ?: friend.usernameForSorting!!,
|
}
|
||||||
friend.bitmojiAvatarId,
|
.mapNotNull { friend ->
|
||||||
friend.bitmojiSelfieId,
|
val userId = friend.userId ?: return@mapNotNull null
|
||||||
streaks = 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)
|
appContext.bridgeClient.passGroupsAndFriends(groups, friends)
|
||||||
}
|
}
|
||||||
@@ -485,9 +494,15 @@ class PurrfectSnap {
|
|||||||
appContext.bridgeClient.sync(object : SyncCallback.Stub() {
|
appContext.bridgeClient.sync(object : SyncCallback.Stub() {
|
||||||
override fun syncFriend(uuid: String): String? {
|
override fun syncFriend(uuid: String): String? {
|
||||||
return appContext.database.getFriendInfo(uuid)?.let {
|
return appContext.database.getFriendInfo(uuid)?.let {
|
||||||
|
if (FriendLinkType.fromValue(it.friendLinkType) in setOf(
|
||||||
|
FriendLinkType.DELETED,
|
||||||
|
FriendLinkType.BLOCKED,
|
||||||
|
FriendLinkType.SUGGESTED
|
||||||
|
)
|
||||||
|
) return@let null
|
||||||
MessagingFriendInfo(
|
MessagingFriendInfo(
|
||||||
userId = it.userId!!,
|
userId = it.userId!!,
|
||||||
dmConversationId = null,
|
dmConversationId = appContext.database.getDMConversationId(it.userId!!),
|
||||||
displayName = it.displayName,
|
displayName = it.displayName,
|
||||||
mutableUsername = it.mutableUsername!!,
|
mutableUsername = it.mutableUsername!!,
|
||||||
bitmojiId = it.bitmojiAvatarId,
|
bitmojiId = it.bitmojiAvatarId,
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleTyp
|
|||||||
).contains(ruleType) && getRuleState() != null
|
).contains(ruleType) && getRuleState() != null
|
||||||
|
|
||||||
fun canUseRule(conversationId: String): Boolean {
|
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)
|
val state = getState(conversationId)
|
||||||
if (context.config.rules.getRuleState(ruleType) == RuleState.BLACKLIST) {
|
if (context.config.rules.getRuleState(ruleType) == RuleState.BLACKLIST) {
|
||||||
return !state
|
return !state
|
||||||
|
|||||||
@@ -4,13 +4,31 @@ import me.eternal.purrfectsnap.common.data.MessagingRuleType
|
|||||||
import me.eternal.purrfectsnap.core.features.MessagingRuleFeature
|
import me.eternal.purrfectsnap.core.features.MessagingRuleFeature
|
||||||
import me.eternal.purrfectsnap.core.util.hook.HookStage
|
import me.eternal.purrfectsnap.core.util.hook.HookStage
|
||||||
import me.eternal.purrfectsnap.core.util.hook.hook
|
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) {
|
class HideTypingIndicator : MessagingRuleFeature("Hide Typing Indicator", MessagingRuleType.HIDE_TYPING_INDICATOR) {
|
||||||
private val messaging: Messaging by lazy { context.feature(Messaging::class) }
|
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() {
|
override fun init() {
|
||||||
context.classCache.presenceSession.hook("processTypingActivity", HookStage.BEFORE, {
|
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)
|
it.setResult(null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import me.eternal.purrfectsnap.core.util.ktx.getObjectFieldOrNull
|
|||||||
import me.eternal.purrfectsnap.core.wrapper.impl.*
|
import me.eternal.purrfectsnap.core.wrapper.impl.*
|
||||||
import me.eternal.purrfectsnap.mapper.impl.CallbackMapper
|
import me.eternal.purrfectsnap.mapper.impl.CallbackMapper
|
||||||
import me.eternal.purrfectsnap.mapper.impl.FriendsFeedEventDispatcherMapper
|
import me.eternal.purrfectsnap.mapper.impl.FriendsFeedEventDispatcherMapper
|
||||||
|
import me.eternal.purrfectsnap.mapper.impl.PlatformPresenceActionWrapperMapper
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.Future
|
import java.util.concurrent.Future
|
||||||
|
|
||||||
@@ -52,8 +53,37 @@ class Messaging : Feature("Messaging") {
|
|||||||
lastFocusedConversationType = -1
|
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() {
|
override fun init() {
|
||||||
val stealthMode = context.feature(StealthMode::class)
|
val stealthMode = context.feature(StealthMode::class)
|
||||||
|
val hideTypingIndicator = context.feature(HideTypingIndicator::class)
|
||||||
|
|
||||||
context.classCache.conversationManager.hookConstructor(HookStage.BEFORE) { param ->
|
context.classCache.conversationManager.hookConstructor(HookStage.BEFORE) { param ->
|
||||||
synchronized(conversationManagerReadyListeners) {
|
synchronized(conversationManagerReadyListeners) {
|
||||||
conversationManager = ConversationManager(context, param.thisObject())
|
conversationManager = ConversationManager(context, param.thisObject())
|
||||||
@@ -98,20 +128,100 @@ class Messaging : Feature("Messaging") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer {
|
defer {
|
||||||
arrayOf("activate", "deactivate").forEach { hook ->
|
arrayOf("activate", "deactivate", "processTypingActivity").forEach { hook ->
|
||||||
context.classCache.presenceSession.hook(hook, HookStage.BEFORE, {
|
context.classCache.presenceSession.hook(hook, HookStage.BEFORE, {
|
||||||
val conversationId = openedConversationUUID?.toString() ?: return@hook false
|
shouldHideBitmojiPresence(stealthMode)
|
||||||
context.config.messaging.hideBitmojiPresence.get() || stealthMode.canUseRule(conversationId)
|
|
||||||
}) {
|
}) {
|
||||||
it.setResult(null)
|
it.setResult(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context.classCache.presenceSession.hook("startPeeking", HookStage.BEFORE, {
|
context.classCache.presenceSession.hook("startPeeking", HookStage.BEFORE, {
|
||||||
val conversationId = openedConversationUUID?.toString() ?: return@hook false
|
shouldHidePeek(stealthMode)
|
||||||
context.config.messaging.hidePeekAPeek.get() || stealthMode.canUseRule(conversationId)
|
|
||||||
}) { it.setResult(null) }
|
}) { 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
|
//get last opened snap for media downloader
|
||||||
context.event.subscribe(OnSnapInteractionEvent::class) { event ->
|
context.event.subscribe(OnSnapInteractionEvent::class) { event ->
|
||||||
openedConversationUUID = event.conversationId
|
openedConversationUUID = event.conversationId
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ class StealthMode : MessagingRuleFeature("StealthMode", MessagingRuleType.STEALT
|
|||||||
|
|
||||||
|
|
||||||
override fun init() {
|
override fun init() {
|
||||||
if (getRuleState() == null) return
|
|
||||||
val isConversationInStealthMode: (SnapUUID) -> Boolean = { canUseRule(it.toString()) }
|
val isConversationInStealthMode: (SnapUUID) -> Boolean = { canUseRule(it.toString()) }
|
||||||
|
|
||||||
arrayOf("mediaMessagesDisplayed", "displayedMessages").forEach { methodName: String ->
|
arrayOf("mediaMessagesDisplayed", "displayedMessages").forEach { methodName: String ->
|
||||||
|
|||||||
@@ -27,16 +27,18 @@ class CameraTweaks : Feature("Camera Tweaks") {
|
|||||||
override fun init() {
|
override fun init() {
|
||||||
val config = context.config.camera
|
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 {
|
val frontCameraId by lazy {
|
||||||
runCatching { context.androidContext.getSystemService(CameraManager::class.java).run {
|
runCatching { context.androidContext.getSystemService(CameraManager::class.java).run {
|
||||||
cameraIdList.firstOrNull { getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT }
|
cameraIdList.firstOrNull { getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT }
|
||||||
} }.getOrNull()
|
} }.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) {
|
if (config.disableCameras.get().isNotEmpty() && frontCameraId != null) {
|
||||||
ContextWrapper::class.java.hook("checkPermission", HookStage.BEFORE) { param ->
|
ContextWrapper::class.java.hook("checkPermission", HookStage.BEFORE) { param ->
|
||||||
val permission = param.arg<String>(0)
|
val permission = param.arg<String>(0)
|
||||||
@@ -47,11 +49,24 @@ class CameraTweaks : Feature("Camera Tweaks") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var isLastCameraFront = false
|
var isLastCameraFront = false
|
||||||
|
var startupCameraApplied = false
|
||||||
|
|
||||||
CameraManager::class.java.hook("openCamera", HookStage.BEFORE) { param ->
|
CameraManager::class.java.hook("openCamera", HookStage.BEFORE) { param ->
|
||||||
val cameraManager = param.thisObject() as? CameraManager ?: return@hook
|
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 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) {
|
if (disabledCameras.size >= 2) {
|
||||||
param.setResult(null)
|
param.setResult(null)
|
||||||
|
|||||||
@@ -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() {
|
override fun init() {
|
||||||
if (!context.config.userInterface.hideFriendFeedEntry.get()) return
|
if (!context.config.userInterface.hideFriendFeedEntry.get()) return
|
||||||
|
|
||||||
context.mappings.useMapper(CallbackMapper::class) {
|
context.mappings.useMapper(CallbackMapper::class) {
|
||||||
arrayOf(
|
classLoader = context.androidContext.classLoader
|
||||||
"FetchAndSyncFeedWithConversationIdsCallback" to "onFetchAndSyncFeedComplete",
|
val hasFetchAndSyncCallback = callbacks.getAsMap()?.entries?.any {
|
||||||
"FetchFeedCallback" to "onFetchFeedComplete",
|
it.key.startsWith("FetchAndSyncFeed") && it.key.endsWith("Callback")
|
||||||
"FetchFeedEntriesCallback" to "onFetchFeedEntriesComplete",
|
} == true
|
||||||
"QueryFeedCallback" to "onQueryFeedComplete",
|
if (callbacks.getClass("SyncFeedCallback") == null || !hasFetchAndSyncCallback) {
|
||||||
"FeedManagerDelegate" to "onFeedEntriesUpdated",
|
runCatching { context.mappings.refresh() }.onFailure {
|
||||||
"FeedManagerDelegate" to "onInternalSyncFeed",
|
context.log.error("Failed to refresh mappings for HideFriendFeedEntry callbacks", it)
|
||||||
).forEach { (callbackName, methodName) ->
|
}
|
||||||
findClass(callbacks.get()!![callbackName] ?: return@forEach).hook(methodName, HookStage.BEFORE) { param ->
|
classLoader = context.androidContext.classLoader
|
||||||
filterFriendFeed(param.arg(0))
|
}
|
||||||
|
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") }
|
if (callbackMap.entries.none { it.key.startsWith("FetchAndSyncFeed") && it.key.endsWith("Callback") }) {
|
||||||
?.value
|
context.log.warn("Failed to hook FetchAndSyncFeedCallback")
|
||||||
?.let { findClass(it) }
|
}
|
||||||
?.hook("onFetchAndSyncFeedComplete", HookStage.BEFORE) { param ->
|
if (callbackMap.entries.none { it.key.contains("SyncFeed") && it.key.endsWith("Callback") }) {
|
||||||
val deletedConversations: ArrayList<Any> = param.arg(2)
|
context.log.warn("Failed to hook SyncFeedCallback")
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class ClassMapper(
|
|||||||
FoldingLayoutMapper(),
|
FoldingLayoutMapper(),
|
||||||
PlatformClientAttestationMapper(),
|
PlatformClientAttestationMapper(),
|
||||||
ChatMediaDrawerMapper(),
|
ChatMediaDrawerMapper(),
|
||||||
|
PlatformPresenceActionWrapperMapper(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user