mirror of
https://github.com/faroukbmiled/RyukGram.git
synced 2026-06-08 00:13: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
213 lines
8.2 KiB
Objective-C
213 lines
8.2 KiB
Objective-C
#import "SCIProfileAnalyzerModels.h"
|
|
|
|
#pragma mark - User
|
|
|
|
@implementation SCIProfileAnalyzerUser
|
|
|
|
+ (instancetype)userFromAPIDict:(NSDictionary *)d {
|
|
id pkRaw = d[@"pk"] ?: d[@"pk_id"] ?: d[@"id"];
|
|
NSString *pk = [pkRaw isKindOfClass:[NSString class]] ? pkRaw
|
|
: [pkRaw respondsToSelector:@selector(stringValue)] ? [pkRaw stringValue] : nil;
|
|
if (!pk.length) return nil;
|
|
|
|
SCIProfileAnalyzerUser *u = [self new];
|
|
u.pk = pk;
|
|
u.username = [d[@"username"] isKindOfClass:[NSString class]] ? d[@"username"] : @"";
|
|
u.fullName = [d[@"full_name"] isKindOfClass:[NSString class]] ? d[@"full_name"] : nil;
|
|
u.profilePicURL = [d[@"profile_pic_url"] isKindOfClass:[NSString class]] ? d[@"profile_pic_url"] : nil;
|
|
id pid = d[@"profile_pic_id"];
|
|
if ([pid isKindOfClass:[NSString class]]) u.profilePicID = pid;
|
|
else if ([pid respondsToSelector:@selector(stringValue)]) u.profilePicID = [pid stringValue];
|
|
u.isPrivate = [d[@"is_private"] boolValue];
|
|
u.isVerified = [d[@"is_verified"] boolValue];
|
|
return u;
|
|
}
|
|
|
|
+ (instancetype)userFromJSONDict:(NSDictionary *)d {
|
|
if (![d[@"pk"] isKindOfClass:[NSString class]]) return nil;
|
|
SCIProfileAnalyzerUser *u = [self new];
|
|
u.pk = d[@"pk"];
|
|
u.username = d[@"username"] ?: @"";
|
|
u.fullName = d[@"full_name"];
|
|
u.profilePicURL = d[@"profile_pic_url"];
|
|
u.profilePicID = d[@"profile_pic_id"];
|
|
u.isPrivate = [d[@"is_private"] boolValue];
|
|
u.isVerified = [d[@"is_verified"] boolValue];
|
|
return u;
|
|
}
|
|
|
|
- (NSDictionary *)toJSONDict {
|
|
NSMutableDictionary *d = [NSMutableDictionary dictionary];
|
|
d[@"pk"] = self.pk ?: @"";
|
|
d[@"username"] = self.username ?: @"";
|
|
if (self.fullName) d[@"full_name"] = self.fullName;
|
|
if (self.profilePicURL) d[@"profile_pic_url"] = self.profilePicURL;
|
|
if (self.profilePicID) d[@"profile_pic_id"] = self.profilePicID;
|
|
d[@"is_private"] = @(self.isPrivate);
|
|
d[@"is_verified"] = @(self.isVerified);
|
|
return d;
|
|
}
|
|
|
|
- (id)copyWithZone:(NSZone *)zone {
|
|
SCIProfileAnalyzerUser *u = [SCIProfileAnalyzerUser new];
|
|
u.pk = self.pk;
|
|
u.username = self.username;
|
|
u.fullName = self.fullName;
|
|
u.profilePicURL = self.profilePicURL;
|
|
u.profilePicID = self.profilePicID;
|
|
u.isPrivate = self.isPrivate;
|
|
u.isVerified = self.isVerified;
|
|
return u;
|
|
}
|
|
|
|
- (NSUInteger)hash { return self.pk.hash; }
|
|
- (BOOL)isEqual:(id)other {
|
|
if (![other isKindOfClass:[SCIProfileAnalyzerUser class]]) return NO;
|
|
return [self.pk isEqualToString:((SCIProfileAnalyzerUser *)other).pk];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark - Snapshot
|
|
|
|
@implementation SCIProfileAnalyzerSnapshot
|
|
|
|
+ (instancetype)snapshotFromJSONDict:(NSDictionary *)d {
|
|
if (!d[@"self_pk"]) return nil;
|
|
SCIProfileAnalyzerSnapshot *s = [self new];
|
|
s.scanDate = [NSDate dateWithTimeIntervalSince1970:[d[@"scan_date"] doubleValue]];
|
|
s.selfPK = d[@"self_pk"];
|
|
s.selfUsername = d[@"self_username"];
|
|
s.selfFullName = d[@"self_full_name"];
|
|
s.selfProfilePicURL = d[@"self_profile_pic_url"];
|
|
s.followerCount = [d[@"follower_count"] integerValue];
|
|
s.followingCount = [d[@"following_count"] integerValue];
|
|
s.mediaCount = [d[@"media_count"] integerValue];
|
|
|
|
NSMutableArray *f = [NSMutableArray array];
|
|
for (NSDictionary *u in d[@"followers"]) {
|
|
SCIProfileAnalyzerUser *user = [SCIProfileAnalyzerUser userFromJSONDict:u];
|
|
if (user) [f addObject:user];
|
|
}
|
|
s.followers = f;
|
|
|
|
NSMutableArray *g = [NSMutableArray array];
|
|
for (NSDictionary *u in d[@"following"]) {
|
|
SCIProfileAnalyzerUser *user = [SCIProfileAnalyzerUser userFromJSONDict:u];
|
|
if (user) [g addObject:user];
|
|
}
|
|
s.following = g;
|
|
return s;
|
|
}
|
|
|
|
- (NSDictionary *)toJSONDict {
|
|
NSMutableArray *f = [NSMutableArray arrayWithCapacity:self.followers.count];
|
|
for (SCIProfileAnalyzerUser *u in self.followers) [f addObject:[u toJSONDict]];
|
|
NSMutableArray *g = [NSMutableArray arrayWithCapacity:self.following.count];
|
|
for (SCIProfileAnalyzerUser *u in self.following) [g addObject:[u toJSONDict]];
|
|
|
|
return @{
|
|
@"scan_date": @([self.scanDate timeIntervalSince1970]),
|
|
@"self_pk": self.selfPK ?: @"",
|
|
@"self_username": self.selfUsername ?: @"",
|
|
@"self_full_name": self.selfFullName ?: @"",
|
|
@"self_profile_pic_url": self.selfProfilePicURL ?: @"",
|
|
@"follower_count": @(self.followerCount),
|
|
@"following_count": @(self.followingCount),
|
|
@"media_count": @(self.mediaCount),
|
|
@"followers": f,
|
|
@"following": g,
|
|
};
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark - Profile change
|
|
|
|
@implementation SCIProfileAnalyzerProfileChange
|
|
- (BOOL)usernameChanged { return ![self.previous.username isEqualToString:self.current.username]; }
|
|
- (BOOL)fullNameChanged { return ![(self.previous.fullName ?: @"") isEqualToString:(self.current.fullName ?: @"")]; }
|
|
// Compare profile_pic_id (stable per pic; changes only on upload). URL
|
|
// diffing was unusable — IG rotates the CDN host + path hash per request.
|
|
// Skip when either side is missing the id (old snapshots pre-feature).
|
|
- (BOOL)profilePicChanged {
|
|
NSString *a = self.previous.profilePicID;
|
|
NSString *b = self.current.profilePicID;
|
|
if (!a.length || !b.length) return NO;
|
|
return ![a isEqualToString:b];
|
|
}
|
|
@end
|
|
|
|
#pragma mark - Report
|
|
|
|
@implementation SCIProfileAnalyzerReport
|
|
|
|
static NSArray *sciSubtract(NSArray *a, NSSet *bSet) {
|
|
if (!a.count) return @[];
|
|
NSMutableArray *out = [NSMutableArray arrayWithCapacity:a.count];
|
|
for (SCIProfileAnalyzerUser *u in a) if (![bSet containsObject:u]) [out addObject:u];
|
|
return out;
|
|
}
|
|
|
|
static NSArray *sciIntersect(NSArray *a, NSSet *bSet) {
|
|
if (!a.count) return @[];
|
|
NSMutableArray *out = [NSMutableArray arrayWithCapacity:a.count];
|
|
for (SCIProfileAnalyzerUser *u in a) if ([bSet containsObject:u]) [out addObject:u];
|
|
return out;
|
|
}
|
|
|
|
+ (SCIProfileAnalyzerReport *)reportFromCurrent:(SCIProfileAnalyzerSnapshot *)current
|
|
previous:(SCIProfileAnalyzerSnapshot *)previous {
|
|
SCIProfileAnalyzerReport *r = [self new];
|
|
r.current = current;
|
|
r.previous = previous;
|
|
r.mutualFollowers = @[];
|
|
r.notFollowingYouBack = @[];
|
|
r.youDontFollowBack = @[];
|
|
r.recentFollowers = @[];
|
|
r.lostFollowers = @[];
|
|
r.youStartedFollowing = @[];
|
|
r.youUnfollowed = @[];
|
|
r.profileUpdates = @[];
|
|
if (!current) return r;
|
|
|
|
NSSet *followersSet = [NSSet setWithArray:current.followers];
|
|
NSSet *followingSet = [NSSet setWithArray:current.following];
|
|
|
|
r.mutualFollowers = sciIntersect(current.followers, followingSet);
|
|
r.notFollowingYouBack = sciSubtract(current.following, followersSet);
|
|
r.youDontFollowBack = sciSubtract(current.followers, followingSet);
|
|
|
|
if (previous) {
|
|
NSSet *prevFollowers = [NSSet setWithArray:previous.followers];
|
|
NSSet *prevFollowing = [NSSet setWithArray:previous.following];
|
|
r.recentFollowers = sciSubtract(current.followers, prevFollowers);
|
|
r.lostFollowers = sciSubtract(previous.followers, followersSet);
|
|
r.youStartedFollowing = sciSubtract(current.following, prevFollowing);
|
|
r.youUnfollowed = sciSubtract(previous.following, followingSet);
|
|
|
|
// Profile updates: same pk in both snapshots, any field differs.
|
|
NSMutableDictionary *prevByPK = [NSMutableDictionary dictionary];
|
|
for (SCIProfileAnalyzerUser *u in previous.followers) prevByPK[u.pk] = u;
|
|
for (SCIProfileAnalyzerUser *u in previous.following) prevByPK[u.pk] = u;
|
|
|
|
NSMutableArray *updates = [NSMutableArray array];
|
|
NSMutableSet *seen = [NSMutableSet set];
|
|
NSArray *currentAll = [current.followers arrayByAddingObjectsFromArray:current.following];
|
|
for (SCIProfileAnalyzerUser *u in currentAll) {
|
|
if ([seen containsObject:u.pk]) continue;
|
|
[seen addObject:u.pk];
|
|
SCIProfileAnalyzerUser *prev = prevByPK[u.pk];
|
|
if (!prev) continue;
|
|
SCIProfileAnalyzerProfileChange *ch = [SCIProfileAnalyzerProfileChange new];
|
|
ch.previous = prev;
|
|
ch.current = u;
|
|
if (ch.usernameChanged || ch.fullNameChanged || ch.profilePicChanged) [updates addObject:ch];
|
|
}
|
|
r.profileUpdates = updates;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
@end
|