This commit is contained in:
particle-box
2026-02-24 01:58:11 +05:30
19 changed files with 201 additions and 214 deletions

257
README.md
View File

@@ -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) [![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) [![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)
</div> </div>
@@ -40,44 +40,9 @@ No aggressive donation requests. No minimal changes disguised as “major update
<br> <br>
---
<br>
## 🔄 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
```
<br>
---
<br>
## Features ## Features
PurrfectSnap offers deep control across media, privacy, automation, and UIdesigned 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.
<table> <table>
<tr> <tr>
@@ -246,152 +211,6 @@ Extensibility through custom scripts.
<br> <br>
---
<br>
## What's New
Here's what makes PurrfectSnap different from upstream SnapEnhance.
<br>
### 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.
<br>
### 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.
<br>
### Auto Patcher
Installation was complicated. Now it isnt.
We've eliminated 90% of the installation friction. No Shizuku. No LSPatch. Just a straightforward process that respects your time.
<br>
### 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.
<br>
### 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.
<br>
### Data Persistence
**Friend Notes Backup**
Your notes are valuable. Now they're protected. Export and restore with ease. Never lose context again.
<br>
### 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.
<br>
### 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.
<br>
### 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.
<br>
### 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.
<br>
### 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.
<br>
### 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.
<br>
### 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 Snapchats terms of service. > Some features are intended for educational, recovery, or accessibility purposes. Misuse may violate Snapchats 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!** > **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.
<br> <br>
## 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 :) 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 :)
<br> ### 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.
<br>
## Credits ## Credits

View File

@@ -48,6 +48,7 @@ import me.eternal.purrfectsnap.core.ui.ViewAppearanceHelper
import me.eternal.purrfectsnap.core.util.dataBuilder import me.eternal.purrfectsnap.core.util.dataBuilder
import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.HookStage
import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hook
import me.eternal.purrfectsnap.mapper.impl.ChatMediaDrawerMapper
import java.io.InputStream import java.io.InputStream
import java.lang.reflect.Method import java.lang.reflect.Method
import kotlin.random.Random import kotlin.random.Random
@@ -62,14 +63,27 @@ class MediaFilePicker : Feature("Media File Picker") {
onNextActivityCreate(defer = true) { onNextActivityCreate(defer = true) {
lateinit var chatMediaDrawerActionHandler: Any 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 { context.mappings.useMapper(ChatMediaDrawerMapper::class) {
methods.first { val drawerCls = chatMediaDrawerClass.getAsClass() ?: return@useMapper
it.parameterTypes.size == 1 && it.parameterTypes[0].name.endsWith("ChatMediaDrawerActionHandler") val actionHandlerCls = actionHandlerClass.getAsClass() ?: return@useMapper
}.also { method -> val sendItemsName = sendItemsMethodName.getAsString() ?: "sendItems"
sendItemsMethod = method.parameterTypes[0].methods.first { it.name == "sendItems" } drawerViewClass = drawerCls
}.hook(HookStage.AFTER) { 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) chatMediaDrawerActionHandler = it.arg(0)
} }
} }
@@ -102,7 +116,7 @@ class MediaFilePicker : Feature("Media File Picker") {
} }
context.event.subscribe(ActivityResultEvent::class) { event -> 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 requestCode = null
firstVideoId = context.androidContext.contentResolver.query( firstVideoId = context.androidContext.contentResolver.query(
@@ -128,25 +142,35 @@ class MediaFilePicker : Feature("Media File Picker") {
} }
fun sendMedia() { fun sendMedia() {
sendItemsMethod.invoke(chatMediaDrawerActionHandler, listOf<Any>(), listOf( val method = sendItemsMethod ?: return
sendItemsMethod.genericParameterTypes[1].getTypeArguments().first().dataBuilder { val itemClass = method.genericParameterTypes.getOrNull(1)?.getTypeArguments()?.firstOrNull()
from("_item") { ?: sendItemsListItemClassFallback
set("_cameraRollSource", "Snapchat") if (itemClass == null) {
set("_contentUri", "") context.log.warn("MediaFilePicker: sendItems second parameter type has no generic info (type erasure). genericParameterTypes[1]=${method.genericParameterTypes.getOrNull(1)}")
set("_durationMs", 0.0) context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to send media (incompatible version).")
set("_disabled", false) return
set("_imageRotation", 0.0) }
set("_width", 1080.0) val item = itemClass.dataBuilder {
set("_height", 1920.0) from("_item") {
set("_timestampMs", System.currentTimeMillis().toDouble()) set("_cameraRollSource", "Snapchat")
from("_itemId") { set("_contentUri", "")
set("_itemId", firstVideoId.toString()) set("_durationMs", 0.0)
set("_type", "VIDEO") 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<Any>(), listOf(item))
} }
fun startConversion(audioOnly: Boolean) { fun startConversion(audioOnly: Boolean) {
@@ -204,7 +228,7 @@ class MediaFilePicker : Feature("Media File Picker") {
val buttonTag = Random.nextInt(0, 65535) val buttonTag = Random.nextInt(0, 65535)
context.event.subscribe(AddViewEvent::class) { event -> 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 { event.view.addOnAttachStateChangeListener(object: View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) { override fun onViewAttachedToWindow(v: View) {

View File

@@ -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)

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

View File

@@ -0,0 +1 @@
Advanced Snapchat customization, privacy, and automation tools for rooted and non-rooted devices.

View File

@@ -0,0 +1 @@
PurrfectSnap

View File

@@ -1,6 +1,7 @@
org.gradle.jvmargs=-Xms2G -Xmx4G -Dfile.encoding=UTF-8 --enable-native-access=ALL-UNNAMED org.gradle.jvmargs=-Xms2G -Xmx4G -Dfile.encoding=UTF-8 --enable-native-access=ALL-UNNAMED
org.gradle.parallel=true 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 android.useAndroidX=true
kotlin.code.style=official kotlin.code.style=official
android.nonTransitiveRClass=true android.nonTransitiveRClass=true

View File

@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-bin.zip
distributionSha256Sum=371cb9fbebbe9880d147f59bab36d61eee122854ef8c9ee1ecf12b82368bcf10
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@@ -40,6 +40,7 @@ class ClassMapper(
COFObservableMapper(), COFObservableMapper(),
FoldingLayoutMapper(), FoldingLayoutMapper(),
PlatformClientAttestationMapper(), PlatformClientAttestationMapper(),
ChatMediaDrawerMapper(),
) )
} }

View File

@@ -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("/", ".")) }
}
}
}