#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