Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 118 additions & 30 deletions app/src/main/java/com/wmods/wppenhacer/xposed/core/WppCore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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<Any>(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(<List>, 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<Any>(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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Method>()
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 {
Expand Down