mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-06-07 16:03:54 +02:00
2977873932
- 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
198 lines
8.3 KiB
Plaintext
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);
|
|
}
|
|
}
|
|
}
|