Files
RyukGram/src/Features/Profile/FakeProfileStats.x
T
faroukbmiled 2977873932 [release] RyukGram v1.2.2
- Profile Analyzer (beta) — follower/following scans with mutuals, non-followbacks, new/lost trackers, and profile change history; searchable lists with batch follow/unfollow
- Theme settings — force dark mode, Full OLED, OLED chat theme, and keyboard theme picker
- Confirm story like
- Confirm story emoji reaction
- Swipe down to dismiss media viewer
- Manually add users to story/chat exclusion lists by username
- Keep stories visually seen locally
- Auto-scroll reels mode
- Quality picker: audio-only and raw photo download rows
- Clear cache button with optional auto-clear interval
- Spanish, Russian, Korean, Arabic, and Chinese (Traditional) translations
- About page with version, credits, and links
- Release notes popup on first launch of a new version
- Anonymous live viewing
- Toggle live comments
- Disappearing DM media overlay — action button, mark-as-viewed eye, and audio toggle
- Hide RyukGram UI on screenshots, screen recordings, and mirroring
- Open link from clipboard — long-press the search tab
- Messages-only mode: optional "Hide tab bar" sub-toggle
- Fake profile stats — verified badge and follower/following/post counts on your own profile
- Language switcher + import/export localization from Debug
- Reveal poll/slider vote counts and quiz answers on stories and reels before interacting
- Force legacy Quiz sticker back into the story composer tray
- Advanced experimental features menu — toggle hidden IG experiments (QuickSnap, Homecoming, Prism, Direct Notes reply types) with apply-on-restart batching and a crash-loop auto-reset
- Shortcut to Advanced experimental features from the General experimental features section

- Push notifications render with rich previews on sideload again
- IG 426 compatibility across story audio toggle, like confirmation, seen-on-like, live comments, notes audio download
- Call confirm split into separate voice-call and video-call toggles
- Messages-only mode: tab swiping disabled
- Settings quick-access broken in non-English languages
- Story seen-receipt block restored on IG v426
- Block selected mode no longer marks listed stories as seen
- Hide explore posts grid works again on recent IG versions
- Hide suggested stories no longer breaks profile highlights
- Hide trending searches now also hides the category chip bar
- Story eye long-press menu opens next to the button
- Disable video autoplay: tap-to-play now works on videos inside carousels
- Disable vanish mode swipe fixed on IG 426

- "Confirm shh mode" renamed to "Confirm vanish mode" across all languages
- Confirm sticker interaction split into separate story and highlight toggles
- Shared link embed presets: added eeinstagram.com and vxinstagram.com
- Downloaded media filenames follow `@username_context_timestamp`
- Reels pause mode: optional tap-to-mute on photo reels
- Backup & Restore — scope picker with live preview for Settings / Excluded lists / Analyzer data
- Profile Analyzer: filter by Not verified
- Settings header: tap to open a sheet with GitHub and Telegram channel links

- Thanks to Furamako for the Spanish translation
- Thanks to [ZomkaDEV](https://github.com/ZomkaDEV) for the Russian translation
- Thanks to [@ch1tmdgus](https://github.com/ch1tmdgus) (N4C) for the Korean translation
- Thanks to [@bruuhim](https://github.com/bruuhim) for the Arabic translation
- Thanks to [@jaydenjcpy](https://github.com/jaydenjcpy) for the Chinese (Traditional) translation
- Thanks to [@darthplagueiswise](https://github.com/darthplagueiswise) (Radan) for the experimental flag feature set
- Thanks to [@asdfzxcvbn](https://github.com/asdfzxcvbn) for [zxPluginsInject](https://github.com/asdfzxcvbn/zxPluginsInject) and [ipapatch](https://github.com/asdfzxcvbn/ipapatch)

- Preserved unsent messages can't be removed via "Delete for you"; pull-to-refresh clears them (warning available in settings)
- "Delete for you" detection uses a ~2s window after the local action — a real unsend landing in that window may be missed (rare)
- With Liquid Glass buttons + Hide UI on capture both on, the DM eye leaves an empty glass bubble in captures
2026-04-24 02:50:30 +01:00

198 lines
8.3 KiB
Plaintext

// Fake profile stats for own profile — follower/following/post counts
// and verified badge. Counts rewrite IGStatButton labels; verified flips
// is_verified at the JSON parse layer + swizzles IGUsernameModel to catch
// cached-model renders.
#import "../../Utils.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import <substrate.h>
static BOOL sciFakeOn(NSString *key) { return [SCIUtils getBoolPref:key]; }
// IG format — 1,192 / 12.3K / 1.2M / 1.2B. Raw digits only; passthrough otherwise.
static NSString *sciFormatCount(NSString *raw) {
raw = [raw stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (!raw.length) return nil;
NSCharacterSet *digits = [NSCharacterSet decimalDigitCharacterSet];
for (NSUInteger i = 0; i < raw.length; i++) {
if (![digits characterIsMember:[raw characterAtIndex:i]]) return raw;
}
long long n = raw.longLongValue;
if (n < 10000) {
NSNumberFormatter *f = [NSNumberFormatter new];
f.numberStyle = NSNumberFormatterDecimalStyle;
return [f stringFromNumber:@(n)];
}
double d; NSString *suf;
if (n >= 1000000000LL) { d = n / 1000000000.0; suf = @"B"; }
else if (n >= 1000000LL) { d = n / 1000000.0; suf = @"M"; }
else { d = n / 1000.0; suf = @"K"; }
NSString *s = [NSString stringWithFormat:@"%.1f", d];
if ([s hasSuffix:@".0"]) s = [s substringToIndex:s.length - 2];
return [s stringByAppendingString:suf];
}
static NSString *sciFakeValue(NSString *valueKey) {
return sciFormatCount([[NSUserDefaults standardUserDefaults] stringForKey:valueKey]);
}
// ============ Fake counts — IGStatButton label rewrite ============
static BOOL sciButtonIsOnOwnProfile(UIView *btn) {
Class selfCellCls = NSClassFromString(@"IGProfileSimpleAvatarStatsCell");
if (!selfCellCls) return NO;
UIView *cur = btn;
while (cur && ![cur isKindOfClass:selfCellCls]) cur = cur.superview;
if (!cur) return NO;
Ivar iv = class_getInstanceVariable([cur class], "_isCurrentUser");
if (!iv) return NO;
return *(BOOL *)((char *)(__bridge void *)cur + ivar_getOffset(iv));
}
static NSString *sciFakeTextForName(NSString *name) {
if (!name) return nil;
NSString *low = name.lowercaseString;
if ([low containsString:@"follower"]) {
if (sciFakeOn(@"fake_follower_count")) return sciFakeValue(@"fake_follower_count_value");
} else if ([low containsString:@"following"]) {
if (sciFakeOn(@"fake_following_count")) return sciFakeValue(@"fake_following_count_value");
} else if ([low containsString:@"post"]) {
if (sciFakeOn(@"fake_post_count")) return sciFakeValue(@"fake_post_count_value");
}
return nil;
}
static void sciApplyFakeToButton(id btn) {
if (!sciFakeOn(@"fake_follower_count")
&& !sciFakeOn(@"fake_following_count")
&& !sciFakeOn(@"fake_post_count")) return;
Ivar nmIv = class_getInstanceVariable([btn class], "_name");
NSString *name = nmIv ? object_getIvar(btn, nmIv) : nil;
NSString *fake = sciFakeTextForName(name);
if (!fake) return;
if (!sciButtonIsOnOwnProfile(btn)) return;
Ivar lblIv = class_getInstanceVariable([btn class], "_countLabel");
UILabel *lbl = lblIv ? object_getIvar(btn, lblIv) : nil;
if ([lbl isKindOfClass:[UILabel class]]) lbl.text = fake;
}
static void (*orig_setName)(id, SEL, id);
static void new_setName(id self, SEL _cmd, id name) {
orig_setName(self, _cmd, name);
sciApplyFakeToButton(self);
}
static void (*orig_setCount)(id, SEL, id);
static void new_setCount(id self, SEL _cmd, id cfg) {
orig_setCount(self, _cmd, cfg);
sciApplyFakeToButton(self);
}
static void (*orig_layout)(id, SEL);
static void new_layout(id self, SEL _cmd) {
orig_layout(self, _cmd);
sciApplyFakeToButton(self);
}
// ============ Fake verified — JSON response rewrite ============
// PK + pref cached — read on every JSON parse.
static NSString *gSelfPK = nil;
static BOOL gFakeVerifiedOn = NO;
static BOOL sciPKMatchesSelf(id pk) {
if (!gSelfPK.length) return NO;
if ([pk isKindOfClass:[NSString class]]) return [pk isEqualToString:gSelfPK];
if ([pk isKindOfClass:[NSNumber class]]) return [[(NSNumber *)pk stringValue] isEqualToString:gSelfPK];
return NO;
}
static void sciFlipVerifiedInJSON(id obj, int depth) {
if (depth > 16) return;
if ([obj isKindOfClass:[NSMutableDictionary class]]) {
NSMutableDictionary *d = obj;
id pk = d[@"pk"] ?: d[@"strong_id__"] ?: d[@"user_id"] ?: d[@"id"];
if (sciPKMatchesSelf(pk)) d[@"is_verified"] = @YES;
for (id v in d.allValues) sciFlipVerifiedInJSON(v, depth + 1);
} else if ([obj isKindOfClass:[NSMutableArray class]]) {
for (id v in (NSMutableArray *)obj) sciFlipVerifiedInJSON(v, depth + 1);
}
}
// Belt-and-suspenders — profile header reads isVerified from a cached
// IGUsernameModel without re-parsing JSON on every refresh.
typedef BOOL (*SciIsVerifiedFn)(id, SEL);
static SciIsVerifiedFn orig_UsernameModel_isVerified = NULL;
static NSString *gSelfUsername = nil;
static BOOL new_UsernameModel_isVerified(id self, SEL _cmd) {
BOOL o = orig_UsernameModel_isVerified ? orig_UsernameModel_isVerified(self, _cmd) : NO;
if (o) return YES;
if (!gSelfUsername.length) return NO;
NSString *u = nil;
@try { u = [self valueForKey:@"username"]; } @catch (__unused id e) {}
if ([u isKindOfClass:[NSString class]] && [u isEqualToString:gSelfUsername]) return YES;
return NO;
}
static id (*orig_JSONObjectWithData)(Class, SEL, NSData *, NSJSONReadingOptions, NSError **);
static id new_JSONObjectWithData(Class self, SEL _cmd, NSData *data, NSJSONReadingOptions opts, NSError **err) {
if (!gFakeVerifiedOn) return orig_JSONObjectWithData(self, _cmd, data, opts, err);
opts |= NSJSONReadingMutableContainers;
id r = orig_JSONObjectWithData(self, _cmd, data, opts, err);
if (r) sciFlipVerifiedInJSON(r, 0);
return r;
}
__attribute__((constructor)) static void _sciFakeStatsInit(void) {
// Both feature sets gate install on launch pref + require restart —
// off means no hook at all.
BOOL anyCountOn = sciFakeOn(@"fake_follower_count")
|| sciFakeOn(@"fake_following_count")
|| sciFakeOn(@"fake_post_count");
if (anyCountOn) {
Class sb = NSClassFromString(@"IGStatButton");
if (sb) {
MSHookMessageEx(sb, @selector(setName:), (IMP)new_setName, (IMP *)&orig_setName);
MSHookMessageEx(sb, @selector(setCount:), (IMP)new_setCount, (IMP *)&orig_setCount);
MSHookMessageEx(sb, @selector(layoutSubviews), (IMP)new_layout, (IMP *)&orig_layout);
}
}
if (!sciFakeOn(@"fake_verified")) return;
gFakeVerifiedOn = YES;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
gSelfPK = [[SCIUtils currentUserPK] copy];
id session = [SCIUtils activeUserSession];
id user = nil;
@try { user = [session valueForKey:@"user"]; } @catch (__unused id e) {}
@try { gSelfUsername = [[user valueForKey:@"username"] copy]; } @catch (__unused id e) {}
});
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidBecomeActiveNotification
object:nil queue:nil
usingBlock:^(__unused NSNotification *n) {
if (!gSelfPK.length) gSelfPK = [[SCIUtils currentUserPK] copy];
if (!gSelfUsername.length) {
id session = [SCIUtils activeUserSession];
id user = nil;
@try { user = [session valueForKey:@"user"]; } @catch (__unused id e) {}
@try { gSelfUsername = [[user valueForKey:@"username"] copy]; } @catch (__unused id e) {}
}
}];
Class jc = object_getClass([NSJSONSerialization class]);
MSHookMessageEx(jc, @selector(JSONObjectWithData:options:error:),
(IMP)new_JSONObjectWithData, (IMP *)&orig_JSONObjectWithData);
Class um = NSClassFromString(@"IGUsernameModel");
if (um) {
Method m = class_getInstanceMethod(um, @selector(isVerified));
if (m) {
orig_UsernameModel_isVerified = (SciIsVerifiedFn)method_getImplementation(m);
method_setImplementation(m, (IMP)new_UsernameModel_isVerified);
}
}
}