Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,22 @@ public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVers
}
}

@Override
public void onDowngrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
// Called when a lower-versioned fork (e.g. original Dev4Mod using version 5)
// opens
// a database previously created by this fork (version 10).
// SQLite does not support schema downgrade natively, so we drop all tables and
// recreate the base schema. This clears the deleted-message history stored by
// this fork, but WhatsApp's own data is in a separate database and is
// unaffected.
android.util.Log.w("WaEnhancer",
"delmessages.db downgrade from " + oldVersion + " to " + newVersion + ". Recreating schema.");
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + TABLE_DELETED_FOR_ME);
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS delmessages");
onCreate(sqLiteDatabase);
}

private void createDeletedForMeTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_DELETED_FOR_ME + " (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,34 +37,39 @@
public class AntiRevoke extends Feature {

private static final ConcurrentHashMap<String, Set<String>> messageRevokedMap = new ConcurrentHashMap<>();
private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() ->
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Utils.getApplication().getResources().getConfiguration().getLocales().get(0)));
private static final ThreadLocal<DateFormat> DATE_FORMAT_THREAD_LOCAL = ThreadLocal
.withInitial(() -> DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT,
Utils.getApplication().getResources().getConfiguration().getLocales().get(0)));

public AntiRevoke(ClassLoader loader, XSharedPreferences preferences) {
super(loader, preferences);
}

@Nullable
private static Object findObjectFMessage(XC_MethodHook.MethodHookParam param) throws IllegalAccessException {
if (param.args == null || param.args.length == 0) return null;
if (param.args == null || param.args.length == 0)
return null;

if (FMessageWpp.TYPE.isInstance(param.args[0]))
return param.args[0];

if (param.args.length > 1) {
if (FMessageWpp.TYPE.isInstance(param.args[1]))
return param.args[1];
var FMessageField = ReflectionUtils.findFieldUsingFilterIfExists(param.args[1].getClass(), f -> FMessageWpp.TYPE.isAssignableFrom(f.getType()));
var FMessageField = ReflectionUtils.findFieldUsingFilterIfExists(param.args[1].getClass(),
f -> FMessageWpp.TYPE.isAssignableFrom(f.getType()));
if (FMessageField != null) {
return FMessageField.get(param.args[1]);
}
}

var field = ReflectionUtils.findFieldUsingFilterIfExists(param.args[0].getClass(), f -> f.getType() == FMessageWpp.TYPE);
var field = ReflectionUtils.findFieldUsingFilterIfExists(param.args[0].getClass(),
f -> f.getType() == FMessageWpp.TYPE);
if (field != null)
return field.get(param.args[0]);

var field1 = ReflectionUtils.findFieldUsingFilter(param.args[0].getClass(), f -> f.getType() == FMessageWpp.Key.TYPE);
var field1 = ReflectionUtils.findFieldUsingFilter(param.args[0].getClass(),
f -> f.getType() == FMessageWpp.Key.TYPE);
if (field1 != null) {
var key = field1.get(param.args[0]);
return WppCore.getFMessageFromKey(key);
Expand All @@ -78,15 +83,18 @@ private static void persistRevokedMessage(FMessageWpp fMessage) {
var stripJID = fMessage.getKey().remoteJid.getPhoneNumber();
Set<String> messages = getRevokedMessagesForJid(fMessage);
messages.add(messageKey);
DelMessageStore.getInstance(Utils.getApplication()).insertMessage(stripJID, messageKey, System.currentTimeMillis());
DelMessageStore.getInstance(Utils.getApplication()).insertMessage(stripJID, messageKey,
System.currentTimeMillis());
}

private static Set<String> getRevokedMessagesForJid(FMessageWpp fMessage) {
String stripJID = fMessage.getKey().remoteJid.getPhoneNumber();
if (stripJID == null) return Collections.synchronizedSet(new java.util.HashSet<>());
if (stripJID == null)
return Collections.synchronizedSet(new java.util.HashSet<>());
return messageRevokedMap.computeIfAbsent(stripJID, k -> {
var messages = DelMessageStore.getInstance(Utils.getApplication()).getMessagesByJid(k);
if (messages == null) return Collections.synchronizedSet(new java.util.HashSet<>());
if (messages == null)
return Collections.synchronizedSet(new java.util.HashSet<>());
return Collections.synchronizedSet(messages);
});
}
Expand All @@ -100,13 +108,14 @@ public void doHook() throws Exception {
var unknownStatusPlaybackMethod = Unobfuscator.loadUnknownStatusPlaybackMethod(classLoader);
logDebug(Unobfuscator.getMethodDescriptor(unknownStatusPlaybackMethod));

var statusPlaybackClass = Unobfuscator.loadStatusPlaybackViewClass(classLoader);
Class<?> statusPlaybackClass = Unobfuscator.loadStatusPlaybackViewClass(classLoader);
logDebug(statusPlaybackClass);

XposedBridge.hookMethod(antiRevokeMessageMethod, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Exception {
if (param.args == null || param.args.length == 0 || param.args[0] == null) return;
if (param.args == null || param.args.length == 0 || param.args[0] == null)
return;

var fMessage = new FMessageWpp(param.args[0]);
var messageKey = fMessage.getKey();
Expand All @@ -116,14 +125,20 @@ protected void beforeHookedMethod(MethodHookParam param) throws Exception {
if (WppCore.getPrivBoolean(messageID + "_delpass", false)) {
WppCore.removePrivKey(messageID + "_delpass");
var activity = WppCore.getCurrentActivity();
Class<?> StatusPlaybackActivityClass = classLoader.loadClass("com.whatsapp.status.playback.StatusPlaybackActivity");
Class<?> StatusPlaybackActivityClass = classLoader
.loadClass("com.whatsapp.status.playback.StatusPlaybackActivity");
if (activity != null && StatusPlaybackActivityClass.isInstance(activity)) {
activity.finish();
}
return;
}
// For group messages: intercept any non-self revocation regardless of
// deviceJid.
// Previously the deviceJid != null guard caused all group revocations where
// deviceJid is null (the common case for other participants) to be silently
// skipped.
if (messageKey.remoteJid.isGroup()) {
if (deviceJid != null && handleRevocationAttempt(fMessage) != 0) {
if (!messageKey.isFromMe && handleRevocationAttempt(fMessage) != 0) {
param.setResult(true);
}
} else if (!messageKey.isFromMe && handleRevocationAttempt(fMessage) != 0) {
Expand All @@ -132,11 +147,11 @@ protected void beforeHookedMethod(MethodHookParam param) throws Exception {
}
});


ConversationItemListener.conversationListeners.add(new ConversationItemListener.OnConversationItemListener() {
@Override
public void onItemBind(FMessageWpp fMessage, ViewGroup viewGroup) {
if (fMessage.getKey().isFromMe) return;
if (fMessage.getKey().isFromMe)
return;
var dateTextView = (TextView) viewGroup.findViewById(Utils.getID("date", "id"));
bindRevokedMessageUI(fMessage, dateTextView, "antirevoke");
}
Expand All @@ -149,10 +164,12 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable {
var objFMessage = findObjectFMessage(param);
var field = ReflectionUtils.getFieldByType(param.method.getDeclaringClass(), statusPlaybackClass);

if (obj == null || field == null || objFMessage == null) return;
if (obj == null || field == null || objFMessage == null)
return;

Object objView = field.get(obj);
if (objView == null) return;
if (objView == null)
return;

var textViews = ReflectionUtils.getFieldsByType(statusPlaybackClass, TextView.class);
if (textViews.isEmpty()) {
Expand All @@ -173,22 +190,29 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable {
}

private void bindRevokedMessageUI(FMessageWpp fMessage, TextView dateTextView, String antirevokeType) {
if (dateTextView == null) return;
if (dateTextView == null)
return;

var key = fMessage.getKey();
var messageRevokedList = getRevokedMessagesForJid(fMessage);
var id = fMessage.getRowId();
String keyOrig = null;
if (messageRevokedList.contains(key.messageID) || ((keyOrig = MessageStore.getInstance().getOriginalMessageKey(id)) != null && messageRevokedList.contains(keyOrig))) {
var timestamp = DelMessageStore.getInstance(Utils.getApplication()).getTimestampByMessageId(keyOrig == null ? key.messageID : keyOrig);
if (messageRevokedList.contains(key.messageID)
|| ((keyOrig = MessageStore.getInstance().getOriginalMessageKey(id)) != null
&& messageRevokedList.contains(keyOrig))) {
var timestamp = DelMessageStore.getInstance(Utils.getApplication())
.getTimestampByMessageId(keyOrig == null ? key.messageID : keyOrig);
if (timestamp > 0) {
var date = Objects.requireNonNull(DATE_FORMAT_THREAD_LOCAL.get()).format(new Date(timestamp));
dateTextView.getPaint().setUnderlineText(true);
dateTextView.setOnClickListener(v -> Utils.showToast(String.format(Utils.getApplication().getString(ResId.string.message_removed_on), date), Toast.LENGTH_LONG));
dateTextView.setOnClickListener(v -> Utils.showToast(
String.format(Utils.getApplication().getString(ResId.string.message_removed_on), date),
Toast.LENGTH_LONG));
}
var antirevokeValue = Integer.parseInt(prefs.getString(antirevokeType, "0"));
if (antirevokeValue == 1) {
var newTextData = UnobfuscatorCache.getInstance().getString("messagedeleted") + " | " + dateTextView.getText();
var newTextData = UnobfuscatorCache.getInstance().getString("messagedeleted") + " | "
+ dateTextView.getText();
dateTextView.setText(newTextData);
} else if (antirevokeValue == 2) {
var drawable = Utils.getApplication().getDrawable(ResId.drawable.deleted);
Expand All @@ -207,7 +231,6 @@ private void bindRevokedMessageUI(FMessageWpp fMessage, TextView dateTextView, S
}
}


private int handleRevocationAttempt(FMessageWpp fMessage) {
try {
showRevocationToast(fMessage);
Expand All @@ -216,21 +239,25 @@ private int handleRevocationAttempt(FMessageWpp fMessage) {
}
String messageKey = (String) XposedHelpers.getObjectField(fMessage.getObject(), "A01");
String stripJID = fMessage.getKey().remoteJid.getPhoneNumber();
int revokeboolean = stripJID.equals("status") ? Integer.parseInt(prefs.getString("antirevokestatus", "0")) : Integer.parseInt(prefs.getString("antirevoke", "0"));
if (revokeboolean == 0) return revokeboolean;
int revokeboolean = stripJID.equals("status") ? Integer.parseInt(prefs.getString("antirevokestatus", "0"))
: Integer.parseInt(prefs.getString("antirevoke", "0"));
if (revokeboolean == 0)
return revokeboolean;
var messageRevokedList = getRevokedMessagesForJid(fMessage);
if (!messageRevokedList.contains(messageKey)) {
try {
CompletableFuture.runAsync(() -> {
persistRevokedMessage(fMessage);
try {
var mConversation = WppCore.getCurrentConversation();
if (mConversation != null && Objects.equals(stripJID, WppCore.getCurrentUserJid().getPhoneNumber())) {
if (mConversation != null
&& Objects.equals(stripJID, WppCore.getCurrentUserJid().getPhoneNumber())) {
mConversation.runOnUiThread(() -> {
if (mConversation.hasWindowFocus()) {
mConversation.startActivity(mConversation.getIntent());
mConversation.overridePendingTransition(0, 0);
mConversation.getWindow().getDecorView().findViewById(android.R.id.content).postInvalidate();
mConversation.getWindow().getDecorView().findViewById(android.R.id.content)
.postInvalidate();
} else {
mConversation.recreate();
}
Expand All @@ -254,13 +281,20 @@ private void showRevocationToast(FMessageWpp fMessage) {
messageSuffix = Utils.getApplication().getString(ResId.string.deleted_status);
jidAuthor = fMessage.getUserJid();
}
if (jidAuthor.userJid == null) return;
if (jidAuthor.userJid == null)
return;
String name = WppCore.getContactName(jidAuthor);
if (TextUtils.isEmpty(name)) {
name = jidAuthor.getPhoneNumber();
}
String message;
if (jidAuthor.isGroup() && fMessage.getUserJid().isNull()) {
// Show "Participant deleted a message in GroupName" for group messages where we
// know
// who the sender is (!isNull). The old condition was inverted — it showed this
// only
// when getUserJid().isNull() which is exactly when we do NOT know the
// participant.
if (jidAuthor.isGroup() && !fMessage.getUserJid().isNull()) {
var participantJid = fMessage.getUserJid();
String participantName = WppCore.getContactName(participantJid);
if (TextUtils.isEmpty(participantName)) {
Expand All @@ -273,7 +307,8 @@ private void showRevocationToast(FMessageWpp fMessage) {
if (prefs.getBoolean("toastdeleted", false)) {
Utils.showToast(message, Toast.LENGTH_LONG);
}
Tasker.sendTaskerEvent(name, jidAuthor.getPhoneNumber(), jidAuthor.isStatus() ? "deleted_status" : "deleted_message");
Tasker.sendTaskerEvent(name, jidAuthor.getPhoneNumber(),
jidAuthor.isStatus() ? "deleted_status" : "deleted_message");
}

@NonNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,22 @@ private void saveOne(Context context, Object msg) throws Exception {
jidObj = getObj(key, "chatJid");
if (jidObj == null)
jidObj = getObj(key, "A00");
if (jidObj != null)
chatJid = jidObj.toString();
if (jidObj != null) {
// Prefer getRawString() — WhatsApp JID object toString() may return display
// name,
// not the raw 'number@domain' form needed for the group filter query.
try {
Object rawStr = jidObj.getClass().getMethod("getRawString").invoke(jidObj);
if (rawStr instanceof String && !((String) rawStr).isEmpty()) {
chatJid = (String) rawStr;
}
} catch (Throwable ignored) {
}
// Fallback to toString() if getRawString() unavailable
if (chatJid == null) {
chatJid = jidObj.toString();
}
}
if (chatJid != null && (chatJid.equalsIgnoreCase("false") || chatJid.equalsIgnoreCase("true"))) {
chatJid = null;
}
Expand Down Expand Up @@ -197,18 +211,36 @@ private void saveOne(Context context, Object msg) throws Exception {
}

// 7. Sender JID
String senderJid = fromMe ? "Me" : chatJid;
// For personal chats: senderJid = "Me" (if fromMe) or the contact's chatJid.
// For group chats: senderJid = the individual participant JID (not the group
// JID).
String senderJid = fromMe ? "Me" : null;
Object participant = getObj(msg, "participant");
if (participant == null)
participant = getObj(msg, "senderJid");
if (participant == null)
participant = getObj(msg, "A0b");
if (participant != null) {
String val = participant.toString();
if (!val.equalsIgnoreCase("false") && !val.equalsIgnoreCase("true")) {
String val = null;
// Try getRawString() first to get 'number@s.whatsapp.net'
try {
Object rawStr = participant.getClass().getMethod("getRawString").invoke(participant);
if (rawStr instanceof String && !((String) rawStr).isEmpty()) {
val = (String) rawStr;
}
} catch (Throwable ignored) {
}
if (val == null)
val = participant.toString();
if (!val.equalsIgnoreCase("false") && !val.equalsIgnoreCase("true") && !val.isEmpty()) {
senderJid = val;
}
}
// Final fallback: use chatJid only for non-group personal chats
if (senderJid == null) {
boolean isGroupChat = chatJid != null && chatJid.endsWith("@g.us");
senderJid = isGroupChat ? "Unknown" : chatJid;
}

// 8. Media Details
String mediaPath = null;
Expand Down
Loading