diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index bc854eae0..66ba5bda5 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -9,6 +9,8 @@ on: env: GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.caching=true" + ANDROID_SDK_ROOT: /usr/local/lib/android/sdk + ANDROID_HOME: /usr/local/lib/android/sdk jobs: build-debug: @@ -28,6 +30,35 @@ jobs: distribution: 'temurin' cache: gradle + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + with: + packages: | + platform-tools + platforms;android-33 + build-tools;33.0.2 + ndk;21.4.7075529 + cmake;3.22.1 + + - name: Create build config files + env: + APP_ID: ${{ secrets.APP_ID }} + APP_HASH: ${{ secrets.APP_HASH }} + MAPS_V2_API: ${{ secrets.MAPS_V2_API }} + SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} + SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} + SIGNING_KEY_STORE_PASSWORD: ${{ secrets.SIGNING_KEY_STORE_PASSWORD }} + run: | + echo "sdk.dir=${ANDROID_HOME:-$ANDROID_SDK_ROOT}" > local.properties + cat > API_KEYS < local.properties + cat > API_KEYS < local.properties + cat > API_KEYS < local.properties + cat > API_KEYS < local.properties + cat > API_KEYS < getMinAndMaxIds(ArrayList messArr) { + if (messArr == null || messArr.isEmpty()) { + return new Pair<>(0, 0); + } + + int minId = Integer.MAX_VALUE; + int maxId = Integer.MIN_VALUE; + + for (MessageObject msg : messArr) { + if (msg != null && msg.messageOwner != null) { + int id = msg.messageOwner.id; + if (id < minId) minId = id; + if (id > maxId) maxId = id; + } + } + + // If no valid messages found + if (minId == Integer.MAX_VALUE || maxId == Integer.MIN_VALUE) { + return new Pair<>(0, 0); + } + + return new Pair<>(minId, maxId); + } + + /** + * Hook into message history loading to inject saved/deleted messages + * + * @param currentAccount Current account ID + * @param messArr ArrayList of MessageObject to potentially modify + * @param messagesDict Dictionary of messages by dialog and message ID + * @param startId Start message ID + * @param endId End message ID + * @param dialogId Dialog ID + * @param limit Message limit + * @param topicId Topic ID (for forum topics) + * @param isSecretChat Whether this is a secret chat + */ + public static void doHook( + int currentAccount, + ArrayList messArr, + SparseArray[] messagesDict, + int startId, + int endId, + long dialogId, + int limit, + long topicId, + boolean isSecretChat + ) { + // Don't inject messages if the feature is disabled for this dialog + if (!AyuConfig.saveDeletedMessageFor(currentAccount, dialogId)) { + return; + } + + // Don't process if we don't have a valid range + if (startId == 0 && endId == 0) { + return; + } + + try { + long userId = UserConfig.getInstance(currentAccount).getClientUserId(); + AyuMessagesController controller = AyuMessagesController.getInstance(); + + // Query deleted messages in the range + List deletedMessages = controller.getMessages( + userId, + dialogId, + topicId, + startId, + endId, + limit + ); + + if (deletedMessages == null || deletedMessages.isEmpty()) { + return; + } + + // Convert deleted messages to MessageObject instances + List restoredMessages = new ArrayList<>(); + for (DeletedMessageFull deletedMsg : deletedMessages) { + MessageObject messageObject = reconstructMessageObject(deletedMsg, currentAccount); + if (messageObject != null) { + restoredMessages.add(messageObject); + } + } + + if (restoredMessages.isEmpty()) { + return; + } + + // Merge restored messages with existing messages + mergeMessages(messArr, messagesDict, restoredMessages, dialogId); + + } catch (Exception e) { + // Silently fail - don't break chat loading if there's an error + android.util.Log.e("AyuGram", "Error in history hook", e); + } + } + + /** + * Reconstructs a MessageObject from a deleted message database entry + */ + private static MessageObject reconstructMessageObject(DeletedMessageFull deletedMsg, int currentAccount) { + try { + TLRPC.TL_message msg = new TLRPC.TL_message(); + + var deletedMessage = deletedMsg.message; + + // Set basic properties + msg.id = deletedMessage.messageId; + msg.message = deletedMessage.text; + msg.date = deletedMessage.date; + msg.edit_date = deletedMessage.editDate; + msg.views = deletedMessage.views; + msg.flags = deletedMessage.flags; + msg.grouped_id = deletedMessage.groupedId; + msg.dialog_id = deletedMessage.dialogId; + + // Mark as deleted for UI indication + msg.flags |= 0x80000000; // Custom flag to indicate deleted message + + // Reconstruct peer IDs + if (deletedMessage.peerId != 0) { + msg.peer_id = createPeer(deletedMessage.peerId); + } + + if (deletedMessage.fromId != 0) { + msg.from_id = createPeer(deletedMessage.fromId); + } + + // Deserialize entities + if (deletedMessage.textEntities != null) { + msg.entities = deserializeEntities(deletedMessage.textEntities); + } + + // Reconstruct forward info + if (deletedMessage.fwdFlags != 0) { + msg.fwd_from = new TLRPC.TL_messageFwdHeader(); + msg.fwd_from.flags = deletedMessage.fwdFlags; + msg.fwd_from.date = deletedMessage.fwdDate; + if (deletedMessage.fwdFromId != 0) { + msg.fwd_from.from_id = createPeer(deletedMessage.fwdFromId); + } + if (deletedMessage.fwdName != null) { + msg.fwd_from.from_name = deletedMessage.fwdName; + } + if (deletedMessage.fwdPostAuthor != null) { + msg.fwd_from.post_author = deletedMessage.fwdPostAuthor; + } + } + + // Reconstruct reply info + if (deletedMessage.replyFlags != 0 || deletedMessage.replyMessageId != 0) { + msg.reply_to = new TLRPC.TL_messageReplyHeader(); + msg.reply_to.flags = deletedMessage.replyFlags; + msg.reply_to.reply_to_msg_id = deletedMessage.replyMessageId; + msg.reply_to.reply_to_top_id = deletedMessage.replyTopId; + msg.reply_to.forum_topic = deletedMessage.replyForumTopic; + if (deletedMessage.replyPeerId != 0) { + msg.reply_to.reply_to_peer_id = createPeer(deletedMessage.replyPeerId); + } + } + + // Reconstruct media + reconstructMedia(msg, deletedMessage); + + // Create MessageObject + return new MessageObject(currentAccount, msg, true, true); + + } catch (Exception e) { + android.util.Log.e("AyuGram", "Error reconstructing message", e); + return null; + } + } + + /** + * Reconstructs media from deleted message data + */ + private static void reconstructMedia(TLRPC.TL_message msg, com.radolyn.ayugram.database.entities.DeletedMessage deletedMessage) { + if (deletedMessage.documentType == com.radolyn.ayugram.AyuConstants.DOCUMENT_TYPE_NONE) { + msg.media = new TLRPC.TL_messageMediaEmpty(); + return; + } + + if (deletedMessage.documentType == com.radolyn.ayugram.AyuConstants.DOCUMENT_TYPE_PHOTO) { + msg.media = new TLRPC.TL_messageMediaPhoto(); + msg.media.photo = new TLRPC.TL_photo(); + msg.media.photo.id = deletedMessage.messageId; // Use message ID as fake photo ID + + if (deletedMessage.thumbsSerialized != null) { + msg.media.photo.sizes = deserializeSizes(deletedMessage.thumbsSerialized); + } + + // If we have a saved file, we could add a file_reference + // but for now we'll just use the thumbs + } else if (deletedMessage.documentType == com.radolyn.ayugram.AyuConstants.DOCUMENT_TYPE_STICKER || + deletedMessage.documentType == com.radolyn.ayugram.AyuConstants.DOCUMENT_TYPE_FILE) { + msg.media = new TLRPC.TL_messageMediaDocument(); + + if (deletedMessage.documentSerialized != null) { + msg.media.document = deserializeDocument(deletedMessage.documentSerialized); + } else { + msg.media.document = new TLRPC.TL_document(); + } + + if (deletedMessage.thumbsSerialized != null && msg.media.document != null) { + msg.media.document.thumbs = deserializeSizes(deletedMessage.thumbsSerialized); + } + + if (deletedMessage.documentAttributesSerialized != null && msg.media.document != null) { + msg.media.document.attributes = deserializeAttributes(deletedMessage.documentAttributesSerialized); + } + } + } + + /** + * Merges restored messages into the existing message list + */ + private static void mergeMessages( + ArrayList messArr, + SparseArray[] messagesDict, + List restoredMessages, + long dialogId + ) { + // Add restored messages to the array + for (MessageObject restoredMsg : restoredMessages) { + // Check if message already exists (might not be deleted after all) + boolean exists = false; + for (MessageObject existingMsg : messArr) { + if (existingMsg.messageOwner.id == restoredMsg.messageOwner.id) { + exists = true; + break; + } + } + + // Only add if it doesn't already exist + if (!exists) { + messArr.add(restoredMsg); + + // Also add to messagesDict if provided (use index 0 for current dialog) + if (messagesDict != null && messagesDict.length > 0) { + messagesDict[0].put(restoredMsg.messageOwner.id, restoredMsg); + } + } + } + + // Re-sort messages by ID (descending order typically) + Collections.sort(messArr, new Comparator() { + @Override + public int compare(MessageObject o1, MessageObject o2) { + return Integer.compare(o2.messageOwner.id, o1.messageOwner.id); + } + }); + } + + // Helper methods (similar to AyuMessageUtils) + + private static TLRPC.Peer createPeer(long peerId) { + if (peerId > 0) { + TLRPC.TL_peerUser peer = new TLRPC.TL_peerUser(); + peer.user_id = peerId; + return peer; + } else if (peerId < -1000000000000L) { + TLRPC.TL_peerChannel peer = new TLRPC.TL_peerChannel(); + peer.channel_id = -1000000000000L - peerId; + return peer; + } else { + TLRPC.TL_peerChat peer = new TLRPC.TL_peerChat(); + peer.chat_id = -peerId; + return peer; + } + } + + private static ArrayList deserializeEntities(byte[] data) { + try { + ArrayList entities = new ArrayList<>(); + org.telegram.tgnet.NativeByteBuffer buffer = new org.telegram.tgnet.NativeByteBuffer(data.length); + buffer.writeBytes(data); + buffer.rewind(); + + int count = buffer.readInt32(false); + for (int i = 0; i < count; i++) { + TLRPC.MessageEntity entity = TLRPC.MessageEntity.TLdeserialize(buffer, buffer.readInt32(false), false); + if (entity != null) { + entities.add(entity); + } + } + buffer.reuse(); + return entities; + } catch (Exception e) { + return new ArrayList<>(); + } + } + + private static ArrayList deserializeSizes(byte[] data) { + try { + ArrayList sizes = new ArrayList<>(); + org.telegram.tgnet.NativeByteBuffer buffer = new org.telegram.tgnet.NativeByteBuffer(data.length); + buffer.writeBytes(data); + buffer.rewind(); + + int count = buffer.readInt32(false); + for (int i = 0; i < count; i++) { + TLRPC.PhotoSize size = TLRPC.PhotoSize.TLdeserialize(0, 0, 0, buffer, buffer.readInt32(false), false); + if (size != null) { + sizes.add(size); + } + } + buffer.reuse(); + return sizes; + } catch (Exception e) { + return new ArrayList<>(); + } + } + + private static TLRPC.Document deserializeDocument(byte[] data) { + try { + org.telegram.tgnet.NativeByteBuffer buffer = new org.telegram.tgnet.NativeByteBuffer(data.length); + buffer.writeBytes(data); + buffer.rewind(); + + TLRPC.Document document = TLRPC.Document.TLdeserialize(buffer, buffer.readInt32(false), false); + buffer.reuse(); + return document; + } catch (Exception e) { + return null; + } + } + + private static ArrayList deserializeAttributes(byte[] data) { + try { + ArrayList attributes = new ArrayList<>(); + org.telegram.tgnet.NativeByteBuffer buffer = new org.telegram.tgnet.NativeByteBuffer(data.length); + buffer.writeBytes(data); + buffer.rewind(); + + int count = buffer.readInt32(false); + for (int i = 0; i < count; i++) { + TLRPC.DocumentAttribute attr = TLRPC.DocumentAttribute.TLdeserialize(buffer, buffer.readInt32(false), false); + if (attr != null) { + attributes.add(attr); + } + } + buffer.reuse(); + return attributes; + } catch (Exception e) { + return new ArrayList<>(); + } + } +} diff --git a/TMessagesProj/src/main/java/com/radolyn/ayugram/proprietary/AyuMessageUtils.java b/TMessagesProj/src/main/java/com/radolyn/ayugram/proprietary/AyuMessageUtils.java new file mode 100644 index 000000000..8fdec0930 --- /dev/null +++ b/TMessagesProj/src/main/java/com/radolyn/ayugram/proprietary/AyuMessageUtils.java @@ -0,0 +1,541 @@ +/* + * Full implementation based on codebase analysis + * Reverse-engineered from usage patterns in the Overgram/AyuGram codebase + * + * This implementation provides message history and anti-recall functionality + */ + +package com.radolyn.ayugram.proprietary; + +import android.text.TextUtils; +import android.util.Log; + +import com.radolyn.ayugram.AyuConstants; +import com.radolyn.ayugram.database.entities.DeletedMessage; +import com.radolyn.ayugram.database.entities.EditedMessage; +import com.radolyn.ayugram.messages.AyuMessagesController; +import com.radolyn.ayugram.messages.AyuSavePreferences; + +import org.telegram.messenger.FileLoader; +import org.telegram.messenger.ImageLocation; +import org.telegram.messenger.UserConfig; +import org.telegram.tgnet.AbstractSerializedData; +import org.telegram.tgnet.NativeByteBuffer; +import org.telegram.tgnet.TLRPC; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.ArrayList; + +public class AyuMessageUtils { + + /** + * Maps data from AyuSavePreferences to EditedMessage entity + */ + public static void map(AyuSavePreferences prefs, EditedMessage revision) { + if (prefs == null || revision == null) return; + mapCommon(prefs, revision); + } + + /** + * Maps data from AyuSavePreferences to DeletedMessage entity + */ + public static void map(AyuSavePreferences prefs, DeletedMessage deletedMessage) { + if (prefs == null || deletedMessage == null) return; + mapCommon(prefs, deletedMessage); + } + + /** + * Common mapping logic for both EditedMessage and DeletedMessage + */ + private static void mapCommon(AyuSavePreferences prefs, Object target) { + var msg = prefs.getMessage(); + if (msg == null) return; + + Class baseClass; + try { + baseClass = Class.forName("com.radolyn.ayugram.database.entities.AyuMessageBase"); + } catch (ClassNotFoundException e) { + return; + } + + try { + // Set basic fields + baseClass.getField("userId").setLong(target, prefs.getUserId()); + baseClass.getField("dialogId").setLong(target, prefs.getDialogId()); + baseClass.getField("messageId").setInt(target, prefs.getMessageId()); + baseClass.getField("topicId").setLong(target, prefs.getTopicId()); + baseClass.getField("entityCreateDate").setInt(target, prefs.getRequestCatchTime()); + baseClass.getField("date").setInt(target, msg.date); + baseClass.getField("editDate").setInt(target, msg.edit_date); + baseClass.getField("views").setInt(target, msg.views); + baseClass.getField("flags").setInt(target, msg.flags); + baseClass.getField("groupedId").setLong(target, msg.grouped_id); + + // Parse peer IDs + if (msg.peer_id != null) { + long peerId = getPeerId(msg.peer_id); + baseClass.getField("peerId").setLong(target, peerId); + } + + if (msg.from_id != null) { + long fromId = getPeerId(msg.from_id); + baseClass.getField("fromId").setLong(target, fromId); + } + + // Handle forward info + if (msg.fwd_from != null) { + baseClass.getField("fwdFlags").setInt(target, msg.fwd_from.flags); + if (msg.fwd_from.from_id != null) { + baseClass.getField("fwdFromId").setLong(target, getPeerId(msg.fwd_from.from_id)); + } + if (msg.fwd_from.from_name != null) { + baseClass.getField("fwdName").set(target, msg.fwd_from.from_name); + } + baseClass.getField("fwdDate").setInt(target, msg.fwd_from.date); + if (msg.fwd_from.post_author != null) { + baseClass.getField("fwdPostAuthor").set(target, msg.fwd_from.post_author); + } + } + + // Handle reply info + if (msg.reply_to != null) { + baseClass.getField("replyFlags").setInt(target, msg.reply_to.flags); + baseClass.getField("replyMessageId").setInt(target, msg.reply_to.reply_to_msg_id); + if (msg.reply_to.reply_to_peer_id != null) { + baseClass.getField("replyPeerId").setLong(target, getPeerId(msg.reply_to.reply_to_peer_id)); + } + baseClass.getField("replyTopId").setInt(target, msg.reply_to.reply_to_top_id); + baseClass.getField("replyForumTopic").setBoolean(target, msg.reply_to.forum_topic); + } + + // Set message text + if (!TextUtils.isEmpty(msg.message)) { + baseClass.getField("text").set(target, msg.message); + } + + // Serialize message entities + if (msg.entities != null && !msg.entities.isEmpty()) { + byte[] serialized = serializeEntities(msg.entities); + baseClass.getField("textEntities").set(target, serialized); + } + + } catch (Exception e) { + Log.e("AyuGram", "Error mapping message fields", e); + } + } + + /** + * Maps media from message to EditedMessage entity + */ + public static void mapMedia(AyuSavePreferences prefs, EditedMessage revision, boolean copyMedia) { + if (prefs == null || revision == null) return; + mapMediaCommon(prefs, revision, copyMedia, prefs.getAccountId()); + } + + /** + * Maps media from message to DeletedMessage entity + */ + public static void mapMedia(AyuSavePreferences prefs, DeletedMessage deletedMessage, boolean copyMedia) { + if (prefs == null || deletedMessage == null) return; + mapMediaCommon(prefs, deletedMessage, copyMedia, prefs.getAccountId()); + } + + /** + * Common media mapping logic + */ + private static void mapMediaCommon(AyuSavePreferences prefs, Object target, boolean copyMedia, int accountId) { + var msg = prefs.getMessage(); + if (msg == null || msg.media == null) return; + + Class baseClass; + try { + baseClass = Class.forName("com.radolyn.ayugram.database.entities.AyuMessageBase"); + } catch (ClassNotFoundException e) { + return; + } + + try { + // Handle photo media + if (msg.media instanceof TLRPC.TL_messageMediaPhoto && msg.media.photo != null) { + baseClass.getField("documentType").setInt(target, AyuConstants.DOCUMENT_TYPE_PHOTO); + + if (copyMedia) { + File photoFile = FileLoader.getInstance(accountId).getPathToMessage(msg); + if (photoFile.exists()) { + File savedFile = saveMediaFile(photoFile, msg.id); + if (savedFile != null) { + baseClass.getField("mediaPath").set(target, savedFile.getAbsolutePath()); + } + } + } + + // Serialize photo thumbs + if (msg.media.photo.sizes != null) { + byte[] serialized = serializeSizes(msg.media.photo.sizes); + baseClass.getField("thumbsSerialized").set(target, serialized); + } + } + // Handle document media (files, videos, stickers, etc.) + else if (msg.media instanceof TLRPC.TL_messageMediaDocument && msg.media.document != null) { + var document = msg.media.document; + + // Determine document type + int docType = AyuConstants.DOCUMENT_TYPE_FILE; + for (var attr : document.attributes) { + if (attr instanceof TLRPC.TL_documentAttributeSticker) { + docType = AyuConstants.DOCUMENT_TYPE_STICKER; + break; + } + } + baseClass.getField("documentType").setInt(target, docType); + + // Save file if needed + if (copyMedia && docType != AyuConstants.DOCUMENT_TYPE_STICKER) { + File docFile = FileLoader.getInstance(accountId).getPathToMessage(msg); + if (docFile.exists()) { + File savedFile = saveMediaFile(docFile, msg.id); + if (savedFile != null) { + baseClass.getField("mediaPath").set(target, savedFile.getAbsolutePath()); + } + } + } + + // Set MIME type + if (!TextUtils.isEmpty(document.mime_type)) { + baseClass.getField("mimeType").set(target, document.mime_type); + } + + // Serialize document + byte[] docSerialized = serializeDocument(document); + baseClass.getField("documentSerialized").set(target, docSerialized); + + // Serialize thumbs + if (document.thumbs != null) { + byte[] thumbs = serializeSizes(document.thumbs); + baseClass.getField("thumbsSerialized").set(target, thumbs); + } + + // Serialize attributes + if (document.attributes != null) { + byte[] attrs = serializeAttributes(document.attributes); + baseClass.getField("documentAttributesSerialized").set(target, attrs); + } + } + } catch (Exception e) { + Log.e("AyuGram", "Error mapping media", e); + } + } + + /** + * Maps data from EditedMessage entity back to TLRPC.Message + */ + public static void map(EditedMessage editedMessage, TLRPC.TL_message msg, int currentAccount) { + if (editedMessage == null || msg == null) return; + + msg.id = editedMessage.messageId; + msg.message = editedMessage.text; + msg.date = editedMessage.date; + msg.edit_date = editedMessage.editDate; + msg.views = editedMessage.views; + msg.flags = editedMessage.flags; + msg.grouped_id = editedMessage.groupedId; + msg.dialog_id = editedMessage.dialogId; + + // Reconstruct peer IDs + if (editedMessage.peerId != 0) { + msg.peer_id = createPeer(editedMessage.peerId); + } + + if (editedMessage.fromId != 0) { + msg.from_id = createPeer(editedMessage.fromId); + } + + // Deserialize entities + if (editedMessage.textEntities != null) { + msg.entities = deserializeEntities(editedMessage.textEntities); + } + + // Reconstruct forward info + if (editedMessage.fwdFlags != 0) { + msg.fwd_from = new TLRPC.TL_messageFwdHeader(); + msg.fwd_from.flags = editedMessage.fwdFlags; + msg.fwd_from.date = editedMessage.fwdDate; + if (editedMessage.fwdFromId != 0) { + msg.fwd_from.from_id = createPeer(editedMessage.fwdFromId); + } + if (!TextUtils.isEmpty(editedMessage.fwdName)) { + msg.fwd_from.from_name = editedMessage.fwdName; + } + if (!TextUtils.isEmpty(editedMessage.fwdPostAuthor)) { + msg.fwd_from.post_author = editedMessage.fwdPostAuthor; + } + } + + // Reconstruct reply info + if (editedMessage.replyFlags != 0 || editedMessage.replyMessageId != 0) { + msg.reply_to = new TLRPC.TL_messageReplyHeader(); + msg.reply_to.flags = editedMessage.replyFlags; + msg.reply_to.reply_to_msg_id = editedMessage.replyMessageId; + msg.reply_to.reply_to_top_id = editedMessage.replyTopId; + msg.reply_to.forum_topic = editedMessage.replyForumTopic; + if (editedMessage.replyPeerId != 0) { + msg.reply_to.reply_to_peer_id = createPeer(editedMessage.replyPeerId); + } + } + } + + /** + * Maps media from EditedMessage entity back to TLRPC.Message + */ + public static void mapMedia(EditedMessage editedMessage, TLRPC.TL_message msg) { + if (editedMessage == null || msg == null) return; + + if (editedMessage.documentType == AyuConstants.DOCUMENT_TYPE_NONE) { + msg.media = new TLRPC.TL_messageMediaEmpty(); + return; + } + + if (editedMessage.documentType == AyuConstants.DOCUMENT_TYPE_PHOTO) { + msg.media = new TLRPC.TL_messageMediaPhoto(); + msg.media.photo = new TLRPC.TL_photo(); + msg.media.photo.id = editedMessage.messageId; // Fake ID + + if (editedMessage.thumbsSerialized != null) { + msg.media.photo.sizes = deserializeSizes(editedMessage.thumbsSerialized); + } + } else if (editedMessage.documentType == AyuConstants.DOCUMENT_TYPE_STICKER || + editedMessage.documentType == AyuConstants.DOCUMENT_TYPE_FILE) { + msg.media = new TLRPC.TL_messageMediaDocument(); + + if (editedMessage.documentSerialized != null) { + msg.media.document = deserializeDocument(editedMessage.documentSerialized); + } else { + msg.media.document = new TLRPC.TL_document(); + } + + if (editedMessage.thumbsSerialized != null && msg.media.document != null) { + msg.media.document.thumbs = deserializeSizes(editedMessage.thumbsSerialized); + } + + if (editedMessage.documentAttributesSerialized != null && msg.media.document != null) { + msg.media.document.attributes = deserializeAttributes(editedMessage.documentAttributesSerialized); + } + } + } + + // Helper methods + + private static long getPeerId(TLRPC.Peer peer) { + if (peer instanceof TLRPC.TL_peerUser) { + return peer.user_id; + } else if (peer instanceof TLRPC.TL_peerChat) { + return -peer.chat_id; + } else if (peer instanceof TLRPC.TL_peerChannel) { + return -1000000000000L - peer.channel_id; + } + return 0; + } + + private static TLRPC.Peer createPeer(long peerId) { + if (peerId > 0) { + TLRPC.TL_peerUser peer = new TLRPC.TL_peerUser(); + peer.user_id = peerId; + return peer; + } else if (peerId < -1000000000000L) { + TLRPC.TL_peerChannel peer = new TLRPC.TL_peerChannel(); + peer.channel_id = -1000000000000L - peerId; + return peer; + } else { + TLRPC.TL_peerChat peer = new TLRPC.TL_peerChat(); + peer.chat_id = -peerId; + return peer; + } + } + + private static File saveMediaFile(File source, int messageId) { + try { + File destDir = AyuMessagesController.attachmentsPath; + if (!destDir.exists()) { + destDir.mkdirs(); + } + + String extension = ""; + String name = source.getName(); + int lastDot = name.lastIndexOf('.'); + if (lastDot > 0) { + extension = name.substring(lastDot); + } + + File destFile = new File(destDir, "msg_" + messageId + "_" + System.currentTimeMillis() + extension); + + copyFile(source, destFile); + return destFile; + } catch (Exception e) { + Log.e("AyuGram", "Failed to save media file", e); + return null; + } + } + + private static void copyFile(File source, File dest) throws IOException { + try (FileInputStream inStream = new FileInputStream(source); + FileOutputStream outStream = new FileOutputStream(dest); + FileChannel inChannel = inStream.getChannel(); + FileChannel outChannel = outStream.getChannel()) { + inChannel.transferTo(0, inChannel.size(), outChannel); + } + } + + private static byte[] serializeEntities(ArrayList entities) { + try { + NativeByteBuffer buffer = new NativeByteBuffer(entities.size() * 128); + buffer.writeInt32(entities.size()); + for (TLRPC.MessageEntity entity : entities) { + entity.serializeToStream(buffer); + } + byte[] result = new byte[buffer.length()]; + buffer.rewind(); + buffer.readBytes(result, 0, result.length, false); + buffer.reuse(); + return result; + } catch (Exception e) { + Log.e("AyuGram", "Failed to serialize entities", e); + return null; + } + } + + private static ArrayList deserializeEntities(byte[] data) { + try { + ArrayList entities = new ArrayList<>(); + NativeByteBuffer buffer = new NativeByteBuffer(data.length); + buffer.writeBytes(data); + buffer.rewind(); + + int count = buffer.readInt32(false); + for (int i = 0; i < count; i++) { + TLRPC.MessageEntity entity = TLRPC.MessageEntity.TLdeserialize(buffer, buffer.readInt32(false), false); + if (entity != null) { + entities.add(entity); + } + } + buffer.reuse(); + return entities; + } catch (Exception e) { + Log.e("AyuGram", "Failed to deserialize entities", e); + return new ArrayList<>(); + } + } + + private static byte[] serializeSizes(ArrayList sizes) { + try { + NativeByteBuffer buffer = new NativeByteBuffer(sizes.size() * 256); + buffer.writeInt32(sizes.size()); + for (TLRPC.PhotoSize size : sizes) { + size.serializeToStream(buffer); + } + byte[] result = new byte[buffer.length()]; + buffer.rewind(); + buffer.readBytes(result, 0, result.length, false); + buffer.reuse(); + return result; + } catch (Exception e) { + Log.e("AyuGram", "Failed to serialize sizes", e); + return null; + } + } + + private static ArrayList deserializeSizes(byte[] data) { + try { + ArrayList sizes = new ArrayList<>(); + NativeByteBuffer buffer = new NativeByteBuffer(data.length); + buffer.writeBytes(data); + buffer.rewind(); + + int count = buffer.readInt32(false); + for (int i = 0; i < count; i++) { + TLRPC.PhotoSize size = TLRPC.PhotoSize.TLdeserialize(0, 0, 0, buffer, buffer.readInt32(false), false); + if (size != null) { + sizes.add(size); + } + } + buffer.reuse(); + return sizes; + } catch (Exception e) { + Log.e("AyuGram", "Failed to deserialize sizes", e); + return new ArrayList<>(); + } + } + + private static byte[] serializeDocument(TLRPC.Document document) { + try { + NativeByteBuffer buffer = new NativeByteBuffer(4096); + document.serializeToStream(buffer); + byte[] result = new byte[buffer.length()]; + buffer.rewind(); + buffer.readBytes(result, 0, result.length, false); + buffer.reuse(); + return result; + } catch (Exception e) { + Log.e("AyuGram", "Failed to serialize document", e); + return null; + } + } + + private static TLRPC.Document deserializeDocument(byte[] data) { + try { + NativeByteBuffer buffer = new NativeByteBuffer(data.length); + buffer.writeBytes(data); + buffer.rewind(); + + TLRPC.Document document = TLRPC.Document.TLdeserialize(buffer, buffer.readInt32(false), false); + buffer.reuse(); + return document; + } catch (Exception e) { + Log.e("AyuGram", "Failed to deserialize document", e); + return null; + } + } + + private static byte[] serializeAttributes(ArrayList attributes) { + try { + NativeByteBuffer buffer = new NativeByteBuffer(attributes.size() * 128); + buffer.writeInt32(attributes.size()); + for (TLRPC.DocumentAttribute attr : attributes) { + attr.serializeToStream(buffer); + } + byte[] result = new byte[buffer.length()]; + buffer.rewind(); + buffer.readBytes(result, 0, result.length, false); + buffer.reuse(); + return result; + } catch (Exception e) { + Log.e("AyuGram", "Failed to serialize attributes", e); + return null; + } + } + + private static ArrayList deserializeAttributes(byte[] data) { + try { + ArrayList attributes = new ArrayList<>(); + NativeByteBuffer buffer = new NativeByteBuffer(data.length); + buffer.writeBytes(data); + buffer.rewind(); + + int count = buffer.readInt32(false); + for (int i = 0; i < count; i++) { + TLRPC.DocumentAttribute attr = TLRPC.DocumentAttribute.TLdeserialize(buffer, buffer.readInt32(false), false); + if (attr != null) { + attributes.add(attr); + } + } + buffer.reuse(); + return attributes; + } catch (Exception e) { + Log.e("AyuGram", "Failed to deserialize attributes", e); + return new ArrayList<>(); + } + } +} diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..41291a0ec --- /dev/null +++ b/build.sh @@ -0,0 +1,198 @@ +#!/bin/bash + +# Overgram4A Build Helper Script +# Usage: ./build.sh [command] + +PROJECT_DIR="/home/wiktor/Overgram4A" +LOG_FILE="/tmp/overgram_build.log" +APK_DIR="$PROJECT_DIR/TMessagesProj/build/outputs/apk" + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +show_help() { + echo -e "${BLUE}Overgram4A Build Helper${NC}" + echo "" + echo "Usage: ./build.sh [command]" + echo "" + echo "Commands:" + echo " build - Start a new build (afat release)" + echo " status - Show current build status" + echo " clean - Clean build artifacts" + echo " rebuild - Clean and build" + echo " log - Show build log (tail -f)" + echo " apk - Show built APK location and info" + echo " features - Show where implemented features are located" + echo " upload - Upload latest APK to pixeldrain" + echo " help - Show this help message" + echo "" +} + +build_project() { + echo -e "${YELLOW}Starting build...${NC}" + cd "$PROJECT_DIR" + ./gradlew assembleAfatRelease 2>&1 | tee "$LOG_FILE" + + if [ ${PIPESTATUS[0]} -eq 0 ]; then + echo -e "${GREEN}Build successful!${NC}" + show_apk_info + else + echo -e "${RED}Build failed! Check log: $LOG_FILE${NC}" + exit 1 + fi +} + +show_status() { + echo -e "${BLUE}Build Status:${NC}" + echo "" + + # Check if gradle is running + if pgrep -f "gradle" > /dev/null; then + echo -e "${YELLOW}Status: Build is currently running${NC}" + echo "" + echo -e "Recent log output:" + tail -20 "$LOG_FILE" 2>/dev/null || echo "No log file yet" + else + echo -e "${GREEN}Status: No build currently running${NC}" + + # Check if APK exists + if [ -d "$APK_DIR" ]; then + echo "" + echo "Latest builds:" + find "$APK_DIR" -name "*.apk" -type f -exec ls -lh {} \; 2>/dev/null | tail -5 + fi + fi +} + +clean_build() { + echo -e "${YELLOW}Cleaning build artifacts...${NC}" + cd "$PROJECT_DIR" + ./gradlew clean + echo -e "${GREEN}Clean complete!${NC}" +} + +show_log() { + if [ -f "$LOG_FILE" ]; then + echo -e "${BLUE}Showing live build log (Ctrl+C to exit)${NC}" + tail -f "$LOG_FILE" + else + echo -e "${RED}No log file found at $LOG_FILE${NC}" + fi +} + +show_apk_info() { + echo -e "${BLUE}Built APK Information:${NC}" + echo "" + + # Find the latest APK + LATEST_APK=$(find "$APK_DIR" -name "*.apk" -type f -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1 | cut -f2- -d" ") + + if [ -n "$LATEST_APK" ]; then + echo -e "${GREEN}Latest APK:${NC} $LATEST_APK" + ls -lh "$LATEST_APK" + echo "" + echo -e "${GREEN}SHA256:${NC}" + sha256sum "$LATEST_APK" + echo "" + echo -e "To install: adb install \"$LATEST_APK\"" + else + echo -e "${RED}No APK files found${NC}" + fi +} + +show_features() { + echo -e "${BLUE}Implemented Features Location:${NC}" + echo "" + + echo -e "${GREEN}Proprietary Classes (Fully Implemented):${NC}" + echo " 1. AyuMessageUtils.java" + echo " Location: TMessagesProj/src/main/java/com/radolyn/ayugram/proprietary/AyuMessageUtils.java" + echo " Features: Message serialization, media handling, database mapping" + echo "" + echo " 2. AyuHistoryHook.java" + echo " Location: TMessagesProj/src/main/java/com/radolyn/ayugram/proprietary/AyuHistoryHook.java" + echo " Features: Deleted message injection, message reconstruction, history merging" + echo "" + + echo -e "${GREEN}Configuration Files:${NC}" + echo " - API_KEYS (Telegram API credentials and signing config)" + echo " - local.properties (Android SDK location)" + echo "" + + echo -e "${GREEN}Key Features:${NC}" + echo " ✓ Anti-delete messages (saves deleted messages to database)" + echo " ✓ Message history injection (shows deleted messages in chat)" + echo " ✓ Media file preservation (copies photos, videos, stickers)" + echo " ✓ Edit history tracking (saves message edits)" + echo " ✓ Full TLRPC serialization/deserialization" + echo "" + + echo -e "${YELLOW}To verify implementation:${NC}" + echo " cat TMessagesProj/src/main/java/com/radolyn/ayugram/proprietary/AyuMessageUtils.java | wc -l" + echo " cat TMessagesProj/src/main/java/com/radolyn/ayugram/proprietary/AyuHistoryHook.java | wc -l" +} + +upload_apk() { + LATEST_APK=$(find "$APK_DIR" -name "*.apk" -type f -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1 | cut -f2- -d" ") + + if [ -z "$LATEST_APK" ]; then + echo -e "${RED}No APK found to upload${NC}" + exit 1 + fi + + echo -e "${YELLOW}Uploading to pixeldrain...${NC}" + echo "File: $LATEST_APK" + + RESPONSE=$(curl -T "$LATEST_APK" -u :dce76fe0-c905-4fba-8813-fb90cb4c1960 https://pixeldrain.com/api/file/ 2>/dev/null) + FILE_ID=$(echo "$RESPONSE" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + + if [ -n "$FILE_ID" ]; then + echo -e "${GREEN}Upload successful!${NC}" + echo -e "${GREEN}Download link:${NC} https://pixeldrain.com/u/$FILE_ID" + else + echo -e "${RED}Upload failed${NC}" + echo "$RESPONSE" + fi +} + +# Main command handler +case "$1" in + build) + build_project + ;; + status) + show_status + ;; + clean) + clean_build + ;; + rebuild) + clean_build + build_project + ;; + log) + show_log + ;; + apk) + show_apk_info + ;; + features) + show_features + ;; + upload) + upload_apk + ;; + help|"") + show_help + ;; + *) + echo -e "${RED}Unknown command: $1${NC}" + echo "" + show_help + exit 1 + ;; +esac