diff --git a/README.md b/README.md index bfec7f8f..c67963a1 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ [![Downloads](https://img.shields.io/github/downloads/particle-box/PurrfectSnap/total?style=for-the-badge&color=f5bde6&labelColor=1e1e2e)](https://github.com/particle-box/PurrfectSnap/releases) [![License](https://img.shields.io/badge/License-GPL_3.0-f5a97f?style=for-the-badge&labelColor=1e1e2e)](https://github.com/particle-box/PurrfectSnap/blob/dev/LICENSE) -[Installation](#installation) • [Features](#features) • [Changelog](#whats-new) • [Community](#community) +[Installation](#installation) • [Features](#features) • [Build](#build-from-source) • [Community](#community) @@ -40,44 +40,9 @@ No aggressive donation requests. No minimal changes disguised as “major update
---- - -
- -## 🔄 How It Works - -```mermaid -graph TD - A[Install PurrfectSnap] --> B{Device Type?} - B -->|Non-Rooted| C[Auto Patcher Runs] - B -->|Rooted| D[Xposed Framework] - C --> E[Snapchat Patched Automatically] - D --> E - E --> F[Launch Snapchat] - F --> G[Access PurrfectSnap Settings] - G --> H[Configure Features] - H --> I{Enable Features} - I --> J[Media Downloader] - I --> K[Privacy Controls] - I --> L[UI Customization] - I --> M[Camera Enhancements] - I --> N[AI Intelligence Layer] - J --> O[Enhanced Snapchat Experience] - K --> O - L --> O - M --> O - N --> O -``` - -
- ---- - -
- ## Features -PurrfectSnap offers deep control across media, privacy, automation, and UI—designed for both casual users and power users. +PurrfectSnap offers deep control across media, privacy, automation, and UI:designed for both casual users and power users. @@ -246,152 +211,6 @@ Extensibility through custom scripts.
---- - -
- -## What's New - -Here's what makes PurrfectSnap different from upstream SnapEnhance. - -
- -### Aurora Design System - -A complete visual language rebuilt from the ground up. Not just a theme, an entire design philosophy. - -The interface now flows with purpose. Subtle animations guide your interactions. Every screen has been reconsidered, every transition refined. The result is an experience that feels premium without being ostentatious. - -
- -### PurrAura - -The ban problem? Solved. - -No more sacrificing features to stay safe. Add friends, block users, attach music; everything works as it should. No workarounds, no compromises, no disadvantages. - -
- -### Auto Patcher - -Installation was complicated. Now it isn’t. - -We've eliminated 90% of the installation friction. No Shizuku. No LSPatch. Just a straightforward process that respects your time. - -
- -### Intelligence Layer - -**Auto Reply** -Let AI handle routine responses while you focus on what matters. Configure automatic replies for chats, stories, and half-swipes. Essential for creators managing volume. Requires an API key from Google AI Studio, they offer free tier access. - -**Scheduled Snaps** -Time-shifted communication. Set a snap to send at midnight for a birthday. Queue content for optimal timing:your schedule, automated. - -**Message Translator** -Language barriers, eliminated. Incoming messages automatically translate to your preferred language. Set it once, communicate globally. - -
- -### Privacy Enhancements - -**Granular Controls** -Whitelist and blacklist modes for typing indicators and message logging. Choose exactly who sees what, friend by friend. - -**Auto Delete Messages** -Set expiration times for your messages. They disappear on your schedule, not Snapchat's. - -**Message Logger Viewer** -Review exported logs directly in-app. No external tools required. - -
- -### Data Persistence - -**Friend Notes Backup** -Your notes are valuable. Now they're protected. Export and restore with ease. Never lose context again. - -
- -### Device Management - -**ID Spoofing** -Device bans happen. Sometimes unjustly. We offer a solution for legitimate cases, approval required with proof of wrongful ban. This feature is restricted to prevent abuse. - -**Model Spoofing** -Identify as a high-end device model. Potential performance improvements, enhanced camera processing, and better feature access. - -**Network Spoofing** -Spoof your network status to Wi-Fi always to prevent several restrictions on mobile data. - -
- -### Restored Functionality - -Features that disappeared? They're back. - -Bulk messaging works again. Friend list management is fully operational. These aren't new features, they're restored capabilities that should never have been lost. - -
- -### Discovery - -**Scripts Catalog** -Browse and install scripts without leaving the app. No more hunting for import links. One-tap installation. Always current. - -**Friend Tracker Catalog** -Browse and install tracking rules without leaving the app. One-tap installation. Always current. - -**Friend Tracker Import/Export** -Backup and restore your Friend Tracker configurations with ease. - -
- -### Advanced Tracking - -**I Can See You Rule** -Track the exact time and duration when a friend enters your chat. Precision visibility tracking for Friend Tracker. - -**Auto Open Snaps Enhancement** -More granular control with queue size configuration and customizable delays. - -
- -### Visual Refinements - -**AMOLED Theme** -True black. Battery-saving. The most requested feature, delivered. - -**Haptic Feedback** -Subtle tactile responses. Every interaction confirmed through touch. - -**Customizable Bottom Bar** -Your navigation, your rules. Rearrange tabs, remove what you don't use, set your default view. Complete flexibility. - -**Search History** -Recently searched features, instantly accessible. No more hunting through menus. - -
- -### Infrastructure - -**In-App Updates** -New versions install directly from within the app. No GitHub hunting. No manual downloads. Just seamless updates. - -**Update Notifications** -Stay current automatically. Get notified when new releases are available. - -**Config Export/Import Summary** -See exactly which features a config will enable before importing or exporting. Know what you're getting. - -
- -### Quality - -Hundreds of refinements you'll feel but might not see. Smoother animations. Faster response times. Eliminated edge cases. The kind of polish that separates good from exceptional. - - - > ⚠️Some features are intended for educational, recovery, or accessibility purposes. Misuse may violate Snapchat’s terms of service. > **We are not affiliated with Snap Inc. and are not responsible for any violations by the user. Our sole purpose is for education purposes only!** @@ -425,15 +244,79 @@ Questions? Ideas? Found a bug? Our community is active and responsive.
-## Contributing +## Build from Source We welcome contributions from developers who share our vision. Whether it's code, documentation, etc., contributions are always appreciated. Feel free to open a PR :) -
+### 1) Fork ---- +1. Fork this repository on GitHub. +``` + +### 2) Generate a Release Keystore (Certificate) + +Create a signing keystore (`.jks`) and keep it secure: + +```bash +keytool -genkeypair -v \ + -keystore purrfectsnap-release.keystore \ + -storetype JKS \ + -alias purrfectsnap \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 +``` + +Optional local placement for Gradle signing: + +```bash +mkdir -p ~/.android +cp purrfectsnap-release.keystore ~/.android/purrfectsnap-release.keystore +``` + +### 3) Convert Keystore to Base64 + +For GitHub Actions secret `PS_BASE_64`: + +Linux/macOS: + +```bash +base64 -w 0 purrfectsnap-release.keystore > keystore.base64 +``` + +PowerShell (Windows): + +```powershell +[Convert]::ToBase64String([IO.File]::ReadAllBytes("purrfectsnap-release.keystore")) | Set-Content -NoNewline keystore.base64 +``` + +### 4) Add Required GitHub Secrets (Your Fork) + +Go to: +`Settings -> Secrets and variables -> Actions -> New repository secret` + +Create these exact secrets (from `.github/workflows/release.yml`): + +- `PS_BASE_64` = contents of `keystore.base64` +- `PS_RELEASE_KEY_ALIAS` = `purrfectsnap` (or your chosen alias) +- `PS_RELEASE_KEY_PASSWORD` = key password from `keytool` +- `PS_RELEASE_STORE_PASSWORD` = keystore password from `keytool` + +### 5) Run Release Workflow + +1. Open `Actions` in your fork. +2. Run `PurrfectSnap Release CI` (`workflow_dispatch`). +3. Workflow builds and signs: + - `armv8` release APK + - `armv7` release APK +4. Workflow creates a GitHub Release and uploads APK assets. + +### Notes + +- CI uses `JDK 21 (Temurin)`. +- Signing requires all four secrets above. +- Keep keystore and passwords private. Rotate immediately if exposed. -
## Credits diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt index ba6fed6c..0168f483 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/experiments/MediaFilePicker.kt @@ -48,6 +48,7 @@ import me.eternal.purrfectsnap.core.ui.ViewAppearanceHelper import me.eternal.purrfectsnap.core.util.dataBuilder import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.hook +import me.eternal.purrfectsnap.mapper.impl.ChatMediaDrawerMapper import java.io.InputStream import java.lang.reflect.Method import kotlin.random.Random @@ -62,14 +63,27 @@ class MediaFilePicker : Feature("Media File Picker") { onNextActivityCreate(defer = true) { lateinit var chatMediaDrawerActionHandler: Any - lateinit var sendItemsMethod: Method + var sendItemsMethod: Method? = null + var drawerViewClass: Class<*>? = null + var sendItemsListItemClassFallback: Class<*>? = null - findClass("com.snap.composer.memories.ChatMediaDrawer").genericSuperclass?.getTypeArguments()?.getOrNull(1)?.apply { - methods.first { - it.parameterTypes.size == 1 && it.parameterTypes[0].name.endsWith("ChatMediaDrawerActionHandler") - }.also { method -> - sendItemsMethod = method.parameterTypes[0].methods.first { it.name == "sendItems" } - }.hook(HookStage.AFTER) { + context.mappings.useMapper(ChatMediaDrawerMapper::class) { + val drawerCls = chatMediaDrawerClass.getAsClass() ?: return@useMapper + val actionHandlerCls = actionHandlerClass.getAsClass() ?: return@useMapper + val sendItemsName = sendItemsMethodName.getAsString() ?: "sendItems" + drawerViewClass = drawerCls + sendItemsListItemClassFallback = sendItemsListItemClass.getAsClass() + + val contextType = drawerCls.genericSuperclass?.getTypeArguments()?.getOrNull(1) ?: return@useMapper + val handlerParamMethod = contextType.methods.firstOrNull { method -> + method.parameterTypes.size == 1 && ( + method.parameterTypes[0].name.endsWith("ChatMediaDrawerActionHandler") || + actionHandlerCls.isAssignableFrom(method.parameterTypes[0]) + ) + } ?: return@useMapper + val sendItems = handlerParamMethod.parameterTypes[0].methods.firstOrNull { it.name == sendItemsName } ?: return@useMapper + sendItemsMethod = sendItems + handlerParamMethod.hook(HookStage.AFTER) { chatMediaDrawerActionHandler = it.arg(0) } } @@ -102,7 +116,7 @@ class MediaFilePicker : Feature("Media File Picker") { } context.event.subscribe(ActivityResultEvent::class) { event -> - if (event.requestCode != requestCode || event.resultCode != Activity.RESULT_OK) return@subscribe + if (sendItemsMethod == null || event.requestCode != requestCode || event.resultCode != Activity.RESULT_OK) return@subscribe requestCode = null firstVideoId = context.androidContext.contentResolver.query( @@ -128,25 +142,35 @@ class MediaFilePicker : Feature("Media File Picker") { } fun sendMedia() { - sendItemsMethod.invoke(chatMediaDrawerActionHandler, listOf(), listOf( - sendItemsMethod.genericParameterTypes[1].getTypeArguments().first().dataBuilder { - from("_item") { - set("_cameraRollSource", "Snapchat") - set("_contentUri", "") - set("_durationMs", 0.0) - set("_disabled", false) - set("_imageRotation", 0.0) - set("_width", 1080.0) - set("_height", 1920.0) - set("_timestampMs", System.currentTimeMillis().toDouble()) - from("_itemId") { - set("_itemId", firstVideoId.toString()) - set("_type", "VIDEO") - } + val method = sendItemsMethod ?: return + val itemClass = method.genericParameterTypes.getOrNull(1)?.getTypeArguments()?.firstOrNull() + ?: sendItemsListItemClassFallback + if (itemClass == null) { + context.log.warn("MediaFilePicker: sendItems second parameter type has no generic info (type erasure). genericParameterTypes[1]=${method.genericParameterTypes.getOrNull(1)}") + context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to send media (incompatible version).") + return + } + val item = itemClass.dataBuilder { + from("_item") { + set("_cameraRollSource", "Snapchat") + set("_contentUri", "") + set("_durationMs", 0.0) + set("_disabled", false) + set("_imageRotation", 0.0) + set("_width", 1080.0) + set("_height", 1920.0) + set("_timestampMs", System.currentTimeMillis().toDouble()) + from("_itemId") { + set("_itemId", firstVideoId.toString()) + set("_type", "VIDEO") } - set("_order", 0.0) } - )) + set("_order", 0.0) + } ?: run { + context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to build media item.") + return + } + method.invoke(chatMediaDrawerActionHandler, listOf(), listOf(item)) } fun startConversion(audioOnly: Boolean) { @@ -204,7 +228,7 @@ class MediaFilePicker : Feature("Media File Picker") { val buttonTag = Random.nextInt(0, 65535) context.event.subscribe(AddViewEvent::class) { event -> - if (event.parent !is FrameLayout || !event.view::class.java.name.endsWith("ChatMediaDrawer")) return@subscribe + if (event.parent !is FrameLayout || drawerViewClass?.isInstance(event.view) != true) return@subscribe event.view.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { diff --git a/fastlane/metadata/android/en-US/changelogs/270.txt b/fastlane/metadata/android/en-US/changelogs/270.txt new file mode 100644 index 00000000..cea775f4 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/270.txt @@ -0,0 +1,18 @@ +- New: Bulk Manage Friend Requests(tq to RSR) +- Fix: Bulk Action Progress Dialogs(tq to RSR) +- Fix: Audio note send override(tq to RSR) +- Fix: Improve PurrAura Active indicator(tq to RSR) +- Fix: Prevent Story Rewatch(tq to Brandon) +- Fix: Location search(tq to Brandon) +- Fix: story download & decryption(tq to Hazelnut) +- New: Add confirmation dialogs for Reset PurrfectSnap option +- New: Google Maps Integration(tq to Brandon) +- Fix: support more translation keys +- Fix: several inconsistencies +- Fix: add proper support for scripts +- New: Swap wiki with new website +- Fix: Custom emoji is now stable! +- Fix: Poor Call recorder quality +- New: order feature for chat exports +- Fix: Gemini Auto Reply(tq to RSR) +- New: Spotlight Creator Info(tq to RSR) diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 00000000..ce0dfbb1 --- /dev/null +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,9 @@ +PurrfectSnap is an open-source module for Snapchat that adds advanced customization and utility features for rooted and non-rooted setups. + +It includes media download options, privacy controls, interface tweaks, automation tools, and quality-of-life improvements designed to give users more control over their Snapchat experience. + +Highlights: +- Media and download utilities +- Privacy and visibility controls +- UI customization and theming +- Rooted and non-rooted support \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png new file mode 100644 index 00000000..9abe590f Binary files /dev/null and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 00000000..782d331b Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756928.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756928.jpg new file mode 100644 index 00000000..dfbced8f Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756928.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756929.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756929.jpg new file mode 100644 index 00000000..b778dfb2 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756929.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756930.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756930.jpg new file mode 100644 index 00000000..f035eeec Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756930.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756931.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756931.jpg new file mode 100644 index 00000000..5d640689 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756931.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756932.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756932.jpg new file mode 100644 index 00000000..fc355fb0 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756932.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756933.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756933.jpg new file mode 100644 index 00000000..6915b8ef Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756933.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756934.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756934.jpg new file mode 100644 index 00000000..3a27fef6 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6030798506894756934.jpg differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 00000000..e68ee94f --- /dev/null +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +Advanced Snapchat customization, privacy, and automation tools for rooted and non-rooted devices. \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt new file mode 100644 index 00000000..a3cbaa38 --- /dev/null +++ b/fastlane/metadata/android/en-US/title.txt @@ -0,0 +1 @@ +PurrfectSnap \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index ec216424..41d1d151 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ org.gradle.jvmargs=-Xms2G -Xmx4G -Dfile.encoding=UTF-8 --enable-native-access=ALL-UNNAMED org.gradle.parallel=true -org.gradle.workers.max=8 +# Limit workers to avoid R8/minify file locking on Windows (classes.dex in use by another process) +org.gradle.workers.max=1 android.useAndroidX=true kotlin.code.style=official android.nonTransitiveRClass=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2dcec856..bc75e3bb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-bin.zip +distributionSha256Sum=371cb9fbebbe9880d147f59bab36d61eee122854ef8c9ee1ecf12b82368bcf10 networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/ClassMapper.kt b/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/ClassMapper.kt index 8f788d03..9f2eedc5 100644 --- a/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/ClassMapper.kt +++ b/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/ClassMapper.kt @@ -40,6 +40,7 @@ class ClassMapper( COFObservableMapper(), FoldingLayoutMapper(), PlatformClientAttestationMapper(), + ChatMediaDrawerMapper(), ) } diff --git a/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/impl/ChatMediaDrawerMapper.kt b/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/impl/ChatMediaDrawerMapper.kt new file mode 100644 index 00000000..dd1f2c5e --- /dev/null +++ b/mapper/src/main/kotlin/me/eternal/purrfectsnap/mapper/impl/ChatMediaDrawerMapper.kt @@ -0,0 +1,48 @@ +package me.eternal.purrfectsnap.mapper.impl + +import me.eternal.purrfectsnap.mapper.AbstractClassMapper +import me.eternal.purrfectsnap.mapper.ext.getClassName +import me.eternal.purrfectsnap.mapper.ext.getSuperClassName + +class ChatMediaDrawerMapper : AbstractClassMapper("ChatMediaDrawer") { + val chatMediaDrawerClass = classReference("chatMediaDrawerClass") + val actionHandlerClass = classReference("actionHandlerClass") + val sendItemsMethodName = string("sendItemsMethodName") + /** List element type for sendItems(List, List) second parameter — used when generic type is erased. */ + val sendItemsListItemClass = classReference("sendItemsListItemClass") + + init { + mapper { + val drawerClass = classes.firstOrNull { clazz -> + val name = clazz.getClassName() + val superName = clazz.getSuperClassName() ?: return@firstOrNull false + name.contains("ChatMediaDrawer") && + !name.contains("ActionHandler") && + superName.contains("ValdiGeneratedRootView") + } ?: return@mapper + + val actionHandlerClazz = classes.firstOrNull { clazz -> + clazz.getClassName().contains("ChatMediaDrawerActionHandler") + } ?: return@mapper + + val sendItemsMethod = actionHandlerClazz.methods.firstOrNull { method -> + method.name == "sendItems" && method.parameterTypes.size == 2 + } ?: actionHandlerClazz.methods.firstOrNull { method -> + method.parameterTypes.size == 2 && + method.parameterTypes[0].startsWith("Ljava/util/") && + method.parameterTypes[1].startsWith("Ljava/util/") + } ?: return@mapper + + chatMediaDrawerClass.set(drawerClass.getClassName().replace("/", ".")) + actionHandlerClass.set(actionHandlerClazz.getClassName().replace("/", ".")) + sendItemsMethodName.set(sendItemsMethod.name) + + // Find the list item type (e.g. Azc) by structure: has _order and _item (Valdi schema) + val listItemClass = classes.firstOrNull { clazz -> + val fieldNames = clazz.fields.map { it.name }.toSet() + fieldNames.contains("_order") && fieldNames.contains("_item") + } + listItemClass?.let { sendItemsListItemClass.set(it.getClassName().replace("/", ".")) } + } + } +}