From b1736002ecb83a86680be9458644bb9a495e3b6f Mon Sep 17 00:00:00 2001 From: liraz747 Date: Sun, 13 Sep 2026 16:21:15 +0300 Subject: [PATCH] fix: restore text-message sending on recent WhatsApp versions WppCore.sendMessage targeted an ActionUser method whose shape no longer exists on WhatsApp 2.26.x, so sending silently did nothing (this also broke the Tasker "send message" integration). Instead, resolve the current text-message sending facade: - the facade class is found dynamically by its marker string, - the send method by its (Jid, String) -> void signature, - the instance is captured from the facade constructor, all via the existing Unobfuscator/DexKit convention, with a legacy ActionUser fallback for older versions. Sending now also runs on the main thread and reports failures. --- .../wmods/wppenhacer/xposed/core/WppCore.kt | 148 ++++++++++++++---- .../xposed/core/devkit/Unobfuscator.kt | 70 +++++++++ 2 files changed, 188 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/core/WppCore.kt b/app/src/main/java/com/wmods/wppenhacer/xposed/core/WppCore.kt index b2b683ffa..32471e630 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/core/WppCore.kt +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/core/WppCore.kt @@ -8,6 +8,8 @@ import android.content.SharedPreferences import android.database.sqlite.SQLiteDatabase import android.graphics.drawable.Drawable import android.os.Environment +import android.os.Handler +import android.os.Looper import android.text.TextUtils import android.util.LruCache import android.widget.Toast @@ -64,6 +66,8 @@ object WppCore { private var mWaJidMapRepository: Any? = null private var convertJidToLid: Method? = null private var actionUser: Class<*>? = null + private var messageSenderClass: Class<*>? = null + private var messageSenderInstance: Any? = null private var cachedMessageStoreKey: Method? = null private var conversationJidField: Field? = null private var meManagerPhoneJidField: Field? = null @@ -117,6 +121,20 @@ object WppCore { } }) + // Text-message sending facade (dynamic lookup by marker string). + try { + val senderClass = Unobfuscator.loadTextMessageSender(loader) + messageSenderClass = senderClass + XposedBridge.hookAllConstructors(senderClass, object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + messageSenderInstance = param.thisObject + } + }) + XposedBridge.log("TextMessageSender: ${senderClass.name}") + } catch (e: Throwable) { + XposedBridge.log(e) + } + // CachedMessageStore cachedMessageStoreKey = Unobfuscator.loadCachedMessageStoreKey(loader) XposedBridge.hookAllConstructors( @@ -265,37 +283,107 @@ object WppCore { @JvmStatic fun sendMessage(number: String, message: String) { - try { - val senderMethod = ReflectionUtils.findMethodUsingFilterIfExists(actionUser) { method -> - List::class.java.isAssignableFrom(method.returnType) && - ReflectionUtils.findIndexOfType( - method.parameterTypes, - String::class.java - ) != -1 - } - if (senderMethod != null) { - val userJid = createUserJid("$number@s.whatsapp.net") - if (userJid == null) { - Utils.showToast("UserJID not found", Toast.LENGTH_SHORT) - return - } - val newObject = arrayOfNulls(senderMethod.parameterCount) - for (i in newObject.indices) { - val param = senderMethod.parameterTypes[i] - newObject[i] = ReflectionUtils.getDefaultValue(param) - } - val index = - ReflectionUtils.findIndexOfType(senderMethod.parameterTypes, String::class.java) - newObject[index] = message - val index2 = - ReflectionUtils.findIndexOfType(senderMethod.parameterTypes, List::class.java) - newObject[index2] = Collections.singletonList(userJid) - senderMethod.invoke(getActionUser(), *newObject) - Utils.showToast("Message sent to $number", Toast.LENGTH_SHORT) + val work = Runnable { + try { + if (sendMessageViaFacade(number, message)) return@Runnable + sendMessageLegacy(number, message) + } catch (e: Throwable) { + XposedBridge.log("sendMessage failed: $e") + Utils.showToast("Error in sending message:${e.message}", Toast.LENGTH_SHORT) } - } catch (e: Exception) { - Utils.showToast("Error in sending message:${e.message}", Toast.LENGTH_SHORT) - XposedBridge.log(e) + } + if (Looper.myLooper() == Looper.getMainLooper()) { + work.run() + } else { + Handler(Looper.getMainLooper()).post(work) + } + } + + /** + * Current WhatsApp builds (roughly 2.26.3x+) send text through a DI "user actions" + * facade exposing `(Jid, String) -> void`. The class is found dynamically by marker + * string, the method by signature. Returns true if the message was handed off. + */ + private fun sendMessageViaFacade(number: String, message: String): Boolean { + return try { + val jid = createUserJid("$number@s.whatsapp.net") ?: return false + val facade = getSendFacade() ?: return false + val method = findSendMethod(facade, jid.javaClass) ?: return false + method.invoke(facade, jid, message) + XposedBridge.log("sendMessage: facade ${method.name} invoked for $number") + Utils.showToast("Message sent to $number", Toast.LENGTH_SHORT) + true + } catch (e: Throwable) { + XposedBridge.log("sendMessage facade path failed: $e") + false + } + } + + /** + * Legacy path for older WhatsApp versions where the sender lived on ActionUser as + * `List f(, String)`. Kept so the module keeps working across its full + * supported version range; only reached when the facade path is unavailable. + */ + private fun sendMessageLegacy(number: String, message: String) { + val senderMethod = ReflectionUtils.findMethodUsingFilterIfExists(actionUser) { method -> + List::class.java.isAssignableFrom(method.returnType) && + ReflectionUtils.findIndexOfType( + method.parameterTypes, + String::class.java + ) != -1 && + ReflectionUtils.findIndexOfType( + method.parameterTypes, + List::class.java + ) != -1 + } + if (senderMethod == null) { + XposedBridge.log("sendMessage: no send path available for this WhatsApp version") + Utils.showToast("sendMessage: send method not found", Toast.LENGTH_SHORT) + return + } + val userJid = createUserJid("$number@s.whatsapp.net") + if (userJid == null) { + Utils.showToast("UserJID not found", Toast.LENGTH_SHORT) + return + } + val args = arrayOfNulls(senderMethod.parameterCount) + for (i in args.indices) { + args[i] = ReflectionUtils.getDefaultValue(senderMethod.parameterTypes[i]) + } + args[ReflectionUtils.findIndexOfType(senderMethod.parameterTypes, String::class.java)] = + message + args[ReflectionUtils.findIndexOfType(senderMethod.parameterTypes, List::class.java)] = + Collections.singletonList(userJid) + senderMethod.invoke(getActionUser(), *args) + XposedBridge.log("sendMessage: legacy ${senderMethod.name} invoked for $number") + Utils.showToast("Message sent to $number", Toast.LENGTH_SHORT) + } + + private fun getSendFacade(): Any? { + messageSenderInstance?.let { return it } + val cls = messageSenderClass ?: try { + Unobfuscator.loadTextMessageSender(Utils.appClassLoader).also { messageSenderClass = it } + } catch (e: Throwable) { + XposedBridge.log("getSendFacade: $e") + null + } ?: return null + + // Not constructed yet this session: obtain it from the DI service locator + // (all obfuscation-specific knowledge lives in Unobfuscator). + val resolved = Unobfuscator.resolveTextMessageSenderFacade(Utils.appClassLoader) + if (resolved != null && cls.isInstance(resolved)) { + messageSenderInstance = resolved + return resolved + } + return null + } + + private fun findSendMethod(facade: Any, jidClass: Class<*>): Method? { + return facade.javaClass.methods.firstOrNull { m -> + m.parameterCount == 2 && + m.parameterTypes[1] == String::class.java && + m.parameterTypes[0].isAssignableFrom(jidClass) && + m.returnType == Void.TYPE } } diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/core/devkit/Unobfuscator.kt b/app/src/main/java/com/wmods/wppenhacer/xposed/core/devkit/Unobfuscator.kt index 19f03afd7..6344cff88 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/core/devkit/Unobfuscator.kt +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/core/devkit/Unobfuscator.kt @@ -1806,6 +1806,76 @@ object Unobfuscator { } } + /** + * Resolves WhatsApp's text-message sending facade: the class whose helper builds + * an outgoing text message from user input ("UserActionsTextMessageSending/...") + * and which exposes a `(Jid, String) -> void` send method. + * + * Located by marker string, not by obfuscated name, so it survives WhatsApp + * version updates the same way the rest of the module's lookups do. + */ + @Throws(Exception::class) + @JvmStatic + fun loadTextMessageSender(loader: ClassLoader): Class<*> { + return UnobfuscatorCache.getInstance().getClass(loader) { + findFirstClassUsingStrings( + loader, + StringMatchType.Contains, + "UserActionsTextMessageSending" + ) ?: throw RuntimeException("Text message sender not found") + } + } + + /** + * DI binding id for the send facade. Unlike the facade class and its locator method + * (both discovered dynamically), this id is generated per WhatsApp build by Dagger, + * so it is kept here alongside the other obfuscation-specific knowledge and updated + * when WhatsApp changes it. Only used to bootstrap a send before the facade has been + * constructed; normally the instance is captured from its constructor. + */ + private const val TEXT_MESSAGE_SENDER_BINDING_ID = 99146 + + /** + * Resolves an instance of the text-message sending facade via WhatsApp's DI service + * locator. The facade class (marker string) and the locator method (found among the + * facade constructor's `(int) -> Object` calls) are discovered dynamically. + */ + @JvmStatic + fun resolveTextMessageSenderFacade(loader: ClassLoader): Any? { + return try { + val senderClass = loadTextMessageSender(loader) + val ctorData = senderClass.declaredConstructors + .mapNotNull { bridge.getMethodData(it) } + .maxByOrNull { it.invokes.size } + ?: return null + val locators = LinkedHashSet() + for (invoke in ctorData.invokes) { + if (invoke.isMethod && (invoke.modifiers and Modifier.STATIC) != 0 && + invoke.paramCount == 1 && invoke.paramTypeNames[0] == "int" && + invoke.returnTypeName == "java.lang.Object" + ) { + try { + locators.add(invoke.getMethodInstance(loader)) + } catch (_: Throwable) { + } + } + } + for (locator in locators) { + try { + val result = locator.invoke(null, TEXT_MESSAGE_SENDER_BINDING_ID) + if (result != null && senderClass.isInstance(result)) { + return result + } + } catch (_: Throwable) { + } + } + null + } catch (e: Throwable) { + XposedBridge.log("resolveTextMessageSenderFacade: $e") + null + } + } + @Throws(Exception::class) @JvmStatic fun loadOnPlaybackFinished(classLoader: ClassLoader): Method {