# Glow Facebook Tweak — Session Context

## Problem
Clone Glow từ source (reverse engineering). Giữ nguyên tất cả hook gốc, thêm seen fix cho FB 560.x. 
Facebook v560.1.0 (build 963085760) đã thay đổi cơ chế seen state, Glow's old hook không còn hoạt động.

## IPA
- File: `~/test/glow/facebook.ipa` (184M, arm64, đã decrypt)
- Facebook version: 560.1.0, min iOS 15.1
- Frameworks: FBSharedFramework.framework, FBSharedDynamicFramework.framework

## Glow Tweak
- Source: `https://github.com/dayanch96/Glow` (only pre-built .deb, no source)
- Current version: v1.3.1
- Works alone, but story seen feature broken
- Uses `_markThreadAsSeen:`, `_canMarkStoryAsSeen`, `MarkStoryAsSeen` — all removed in FB 560.x

## facebook-no-ads approach (reference)
- Repo: `https://github.com/haoict/facebook-no-ads`
- Story seen disable: hook `FBSnacksBucketsSeenStateManager._sendSeenThreadIDsWithBucket:session:` → no-op
- Class này đã bị xoá khỏi FB 560.x, cần tìm class mới

## Glow.dylib analysis (completed 16 May 2026)
- **Giải nén & run class-dump/otool/nm/strings** trên macOS GA runner
- **File**: 16.8MB, ~95% ffmpeg static lib (libavcodec, libavformat, libx264, libx265, libdav1d) + Go runtime + thin ObjC UI layer
- **Build**: macOS Xcode 16.2 SDK, iPhoneOS 18.2 SDK, target iOS 12.1, arm64 only
- **Link**: Substrate (MSHookMessageEx), UIKit, Foundation, AVFoundation, Photos, Security

### Glow's 23 ObjC classes
- **UI**: WelcomeVC, ChangelogVC, SettingsViewController, DVNSheetController, DVNSheetPresenter, PseudoDetentController, PseudoDetentTransitioningDelegate, DVNLongPressGestureRecognizer
- **Toast**: ToastManager, ToastView, ToastWindow
- **Download**: Downloader, DownloaderHelper, MPDParser, FFMpegHelper, FFmpegKit, FFmpegKitConfig, FFmpegExecution
- **Util**: GlowUserDefaults, ArchDetect, AtomicLong, CallbackData, Statistics

### Hook mechanism
- **Runtime**: `NSClassFromString` (class names built dynamically → NOT stored as static strings)
- **Selectors**: `NSSelectorFromString` (selector names from string table)
- **Hook**: `MSHookMessageEx`
- **Dynamic methods**: `class_addMethod`, `class_addProperty`
- **Result**: `_OBJC_CLASS_$_FB*` NOT imported — all FB classes resolved at runtime

### Known hooks (from strings analysis)
| Selector | Class | Purpose |
|---|---|---|
| `initWithFBTree:` | FBMemFeedStory, FBVideoChannelPlaylistItem | Block ads |
| `initWithFBPandoTree:` | Various FBMem* | Block unwanted items |
| `advanceToNextItemWithNavigationAction:` | Story player | Disable auto-next |
| `tabbarHeightDidChange:` | Tab bar | Override |
| `_markThreadAsSeen:bucket:session:shouldMarkThreadSeenStateUpdates:` | OLD (removed FB 560.x) | Seen (silently fails) |
| `_canMarkStoryAsSeen` | OLD (removed FB 560.x) | Seen check (silently fails) |
| `markThreadAsSeen:` | OLD (removed FB 560.x) | Seen (silently fails) |

### FB 560.x seen replacement (from IPA analysis 16 May 2026)
| Selector | Class | Purpose |
|---|---|---|
| `FBSnacksUnifiedSeenStateMutator` | ❌ **KHÔNG TỒN TẠI** trong FB 560.x | — |
| `shouldDeferSeenStateUpdates` | `FBStoryInlineViewerConfiguration` | Property — defer seen updates |
| `FBShortsSeenStateComponentFragmentUpdater` | GraphQL protocol | Seen state updater |
| `FBReuseNuxMarkSeenMutation` | GraphQL mutation | Mark seen |
| `FBInspirationVODPrivacyMergeToastHasSeenMutationMutation` | GraphQL mutation | Has seen mutation |
| `seenState` / `setSeenState:` | Various | Seen state property |
| `FBShortsSeenStateComponentFragment` | GraphQL fragment | Seen state data |

## Key Finding: Linux toolchain incompatible
Theos toolchain trên Linux (`ld64.lld`) KHÔNG tạo được `LC_DYLD_CHAINED_FIXUPS` hợp lệ.
Dylib build trên Linux có `LC_DYLD_INFO_ONLY` → crash ngay khi mở app trên iOS 16.6.
Dylib build trên macOS (Xcode ld) có `LC_DYLD_CHAINED_FIXUPS` → chạy được.

## Current approach (16 May 2026)
- **Repo**: `https://github.com/Long-Trinh-Tien/facebook-no-ads` (branch master)
- **Build**: GitHub Actions (macOS runner, ARM64 Apple Silicon) — `.github/workflows/build-glow.yml`
- **Dylib**: `com.dvntm.glow_1.3.1_iphoneos-arm.deb` (single dylib, Glow clone + seen fix + Glow.bundle)
- **Token**: `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`

## Injection
- Use cyan (single `-f` for single dylib — 2 dylibs cause crash due to cyan LC_LOAD_DYLIB bug)
- `-u -w -e -s` flags: remove UISupportedDevices, remove watch, remove extensions, fakesign
- `--overwrite`

## Build steps
```bash
# Push → GA builds automatically
cd ~/test/glow/facebooknoseen
git add -A && git commit -m "msg"
git push origin master

# Or trigger manually:
curl -X POST -H "Authorization: token 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" \
  https://api.github.com/repos/Long-Trinh-Tien/facebook-no-ads/actions/workflows/build-glow.yml/dispatches \
  -d '{"ref":"master"}'

# Download artifact:
# (check Actions tab for latest run)

# Inject:
cyan -i facebook.ipa -o glow.ipa -u -w -e -f com.dvntm.glow_1.3.1_iphoneos-arm.deb -n "Facebook6" -b "com.facebook.Facebook6" -s --overwrite
```

## Tools & Auth
- **GitHub CLI (`gh`)**: Đã login với token PAT (user: Long-Trinh-Tien). Dùng `gh` thay vì curl cho thao tác repo/workflow.
- **Tailscale Funnel**: Đã setup, serve `facebook.ipa` qua `https://tmy-tuf-1.danio-map.ts.net/` (port 8888)
- **GitHub token**: `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`

## Storage
- GitHub repo: `https://github.com/Long-Trinh-Tien/facebook-no-ads` (branches: master, qwen-glow)
- Code được push lên GitHub, ko mất khi reboot
- Source local: `~/test/glow/facebooknoseen/` (có thể outdated so với GitHub)
- Artifacts (.deb, .ipa) trong `~/test/glow/`
- Để rebuild lại từ đầu: `cd /tmp && git clone https://token@github.com/Long-Trinh-Tien/facebook-no-ads.git`
- **Repo clone**: `/tmp/facebook-no-ads/` (working copy, branch qwen-glow)

## Build steps (manual)
```bash
# Push → GA builds automatically (noseen)
cd ~/test/glow/facebooknoseen
git add -A && git commit -m "msg"
git push origin master

# Or trigger manually:
curl -X POST -H "Authorization: token ghp_..." \
  https://api.github.com/repos/Long-Trinh-Tien/facebook-no-ads/actions/workflows/build.yml/dispatches \
  -d '{"ref":"master"}'

# Download artifact → local:
# (check Actions tab for latest run)

# Inject (noseen only):
cyan -i facebook.ipa -o output.ipa -u -w -e -f com.tommy.facebooknoseen_1.0.0_iphoneos-arm.deb -n "Facebook6" -b "com.facebook.Facebook6" -s --overwrite

# Inject (glow-from-source):
cyan -i facebook.ipa -o output.ipa -u -w -e -f glow-from-source/packages/*.deb -n "Facebook6" -b "com.facebook.Facebook6" -s --overwrite
```

## Key findings
- **Ko thể combine 2 dylib** vào 1 IPA: cyan/insert_dylib crash, dlopen bị Substrate suppress constructor
- **Cần 1 dylib duy nhất** để có cả Glow + noseen
- **Glow source ko public** → rebuilt from source at `glow-from-source/`
- **FB 560.x IPA analysis (16 May 2026)**: `FBSnacksUnifiedSeenStateMutator` KHÔNG tồn tại. Seen mechanism đổi sang GraphQL-based (`FBShortsSeenStateComponentFragmentUpdater`, `FBReuseNuxMarkSeenMutation`, `shouldDeferSeenStateUpdates`)
- **Video URL properties**: `videoURLString`, `playableURLString`, `hdPlayableURLString`, `dashPlayableURL`, `playableURL`, `mediaURLString`
- **Story viewer**: `FBSnacksStoryViewerWindowingEventsListener` (không phải `FBStoryViewerController`)
- **Like selector**: `didTapLike:` (không phải `performLikeAction:`)

## Repo structure (Long-Trinh-Tien/facebook-no-ads)
```
master branch:
├── Tweak.xm              ← noseen (seen fix only)
├── Makefile / control     ← noseen build
├── .github/workflows/     ← noseen + glow CI + IPA analysis
├── glow-from-source/      ← Glow rebuild (Tweak.xm + bundle)
│   ├── Tweak.xm           ← comprehensive tweak (ads, download, seen fix)
│   ├── Makefile / control  ← Glow build config
│   └── layout/            ← Glow.bundle assets
└── analysis_output/       ← Glow.dylib forensic dump (otool, nm, strings)

qwen-glow branch:
├── glow-from-source/      ← Updated Tweak.xm (AVAssetExportSession, MediaExtractor, download overlay)
└── .github/workflows/
    ├── analyze-fb-ipa.yml ← IPA analysis via Tailscale Funnel
    ├── build-glow.yml
    └── build.yml
```

## Current working features
- ✅ Remove ads (FBMemFeedStory, FBVideoChannelPlaylistItem initWithFBTree:)
- ❌ Anonymous stories (FBSnacksUnifiedSeenStateMutator — CLASS REMOVED in FB 560.x)
- ✅ Welcome popup + Settings UI (in progress)
- ❌ Download video/stories/reels (need media player reverse)
- ❌ Confirm likes (need FBLikeActionHandler replacement in 560.x)
- ❌ Remove PYMK/Reels/Recs (need new FB 560.x classes)

## Todo
1. Clone Glow's hooks exactly (same selectors, same runtime approach)
2. Add FB 560.x seen fix alongside old hooks (need GraphQL-based approach)
3. Build WelcomeVC + SettingsViewController from scratch
4. Add download overlay + NSURLSession download
5. Replace ffmpeg with AVAssetExportSession (reduce binary to <1MB)
6. Confirm like + Remove PYMK/Reels (need Frida)

---

## Frida Debug — Original Glow.dylib (16 May 2026)

### Mục tiêu
Dùng Frida spawn mode để log chính xác Glow.dylib gốc hook class nào, selector nào trên FB 560.x. Từ đó biết hook nào còn hoạt động, hook nào cần thay thế.

### Approach: `frida -U -f` (spawn, không cần FridaGadget)
- Frida spawns app ở paused state → Glow chưa load
- Interceptor hooks được set up trước
- `%resume` → Glow ctor chạy, mọi hook của Glow đều được log
- Không cần FridaGadget nếu app có `get-task-allow` (sideload = AltStore)

### Files (trong `~/test/glow/`)
| File | Size | Purpose |
|---|---|---|
| `glow_orig_debug.ipa` | ~193MB | Facebook IPA + **original** Glow.dylib + Glow.bundle (đã inject bằng cyan) |
| `glow_log.js` | 4.5KB | Frida script log MSHookMessageEx, class_addMethod, NSClassFromString, NSSelectorFromString, dlopen |
| `FridaGadget.dylib` | 38.7MB | Dự phòng nếu spawn ko được (cần inject thêm vào IPA) |

### File glow_log.js — Frida script
```js
var logPath = "/tmp/glow_hooks_" + Date.now() + ".txt";
var f = new File(logPath, "w");

function log(msg) { f.write(msg + "\n"); f.flush(); console.log(msg); }

log("=== Glow Hook Monitor Started ===");

// class_addMethod — detect dynamic methods Glow adds to FB classes
Interceptor.attach(Module.findExportByName(null, "class_addMethod"), {
    onEnter: function(args) {
        try {
            var cls = ObjC.Object(args[0]);
            var sel = ObjC.selectorAsString(args[1]);
            log("class_addMethod: " + cls.toString() + " -> " + sel);
        } catch(e) { log("class_addMethod ERROR: " + e); }
    }
});

// NSClassFromString — detect which FB classes Glow resolves at runtime
Interceptor.attach(Module.findExportByName(null, "NSClassFromString"), {
    onEnter: function(args) {
        try {
            var name = ObjC.Object(args[0]);
            log("NSClassFromString: " + name.toString());
        } catch(e) {}
    }
});

// NSSelectorFromString — detect selectors Glow builds at runtime
Interceptor.attach(Module.findExportByName(null, "NSSelectorFromString"), {
    onEnter: function(args) {
        try {
            var name = ObjC.Object(args[0]);
            log("NSSelectorFromString: " + name.toString());
        } catch(e) {}
    }
});

// dlopen — detect when Glow.dylib is loaded
Interceptor.attach(Module.findExportByName(null, "dlopen"), {
    onEnter: function(args) {
        try {
            var path = args[0].readCString();
            if (path && path.indexOf("Glow") !== -1) log("dlopen Glow: " + path);
        } catch(e) {}
    }
});

// MSHookMessageEx — **chính**: detect class/sel mà Glow hook
var msHookMsgEx = Module.findExportByName(null, "MSHookMessageEx");
if (msHookMsgEx) {
    Interceptor.attach(msHookMsgEx, {
        onEnter: function(args) {
            try {
                var cls = new ObjC.Object(args[0]);
                var sel = ObjC.selectorAsString(args[1]);
                log("MSHookMessageEx: " + cls.$className + " " + sel);
            } catch(e) { log("MSHookMessageEx ERROR: " + e); }
        }
    });
}

// class_addProperty — detect dynamic properties
var p = Module.findExportByName(null, "class_addProperty");
if (p) Interceptor.attach(p, { onEnter(args) { try { log("class_addProperty: " + new ObjC.Object(args[0]).toString()); } catch(e) {} } });

// method_setImplementation — detect swizzling
var m = Module.findExportByName(null, "method_setImplementation");
if (m) Interceptor.attach(m, { onEnter(args) { try { log("method_setImplementation: " + method_getName(args[0])); } catch(e) {} } });

// objc_getClass — detect FB class lookups
var g = Module.findExportByName(null, "objc_getClass");
if (g) Interceptor.attach(g, { onEnter(args) { try { var n = args[0].readCString(); if (n && n.indexOf("FB") !== -1) log("objc_getClass: " + n); } catch(e) {} } });

log("\n=== Ready. Waiting for Glow init... ===");
log("Log file: " + logPath);
```

### How to run (từ Windows có frida)
```cmd
:: Copy IPA + glow_log.js ra Windows (WSL2 path):
:: \\wsl$\Ubuntu\home\tommy\test\glow\

:: Sign IPA bằng AltStore → install vào iPhone

:: Frida spawn:
frida -U -f com.facebook.Facebook6 -l glow_log.js --no-pause

:: Thao tác app → tất cả Glow hooks log ra terminal
```

### Backup: FridaGadget mode (nếu spawn ko được)
```bash
# Extract IPA → add FridaGadget
unzip glow_orig_debug.ipa -d /tmp/glow_patch
cp FridaGadget.dylib /tmp/glow_patch/Payload/Facebook.app/Frameworks/
cd /tmp/glow_patch && zip -qr ~/test/glow/glow_orig_debug.ipa Payload

# User trên Windows kết nối:
frida -U -n Facebook6 -l glow_log.js
```

### Expected output
Script sẽ ghi vào `/tmp/glow_hooks_*.txt` (trên iPhone):
- `MSHookMessageEx: FBSnacksUnifiedSeenStateMutator _attemptSendSeenStateAndHandleResponse:bucket:` (nếu Glow đã được update cho FB 560.x)
- `MSHookMessageEx: FBMemFeedStory initWithFBTree:` (ads blocking)
- `NSClassFromString: FBSnacksUnifiedSeenStateMutator` (runtime class lookup)
- Tất cả class/sel Glow tương tác
