#import "SCIProfileAnalyzerService.h" #import "../../Networking/SCIInstagramAPI.h" #import "../../Utils.h" const NSInteger SCIProfileAnalyzerMaxFollowerCount = 13000; #define SCI_PA_PAGE_DELAY_S 0.25 // small pause between pages — lightweight rate cushion @interface SCIProfileAnalyzerService () { @public NSInteger _expectedFollowers; NSInteger _expectedFollowing; } @property (nonatomic, assign) BOOL cancelled; @property (nonatomic, assign) BOOL isRunning; @end @implementation SCIProfileAnalyzerService + (instancetype)sharedService { static SCIProfileAnalyzerService *s; static dispatch_once_t once; dispatch_once(&once, ^{ s = [self new]; }); return s; } - (void)cancel { self.cancelled = YES; } - (void)finishWithSnapshot:(SCIProfileAnalyzerSnapshot *)s error:(NSError *)e completion:(SCIPACompletion)completion { self.isRunning = NO; self.cancelled = NO; if (completion) dispatch_async(dispatch_get_main_queue(), ^{ completion(s, e); }); } - (NSError *)errorWithCode:(SCIProfileAnalyzerError)code message:(NSString *)msg { return [NSError errorWithDomain:@"SCIProfileAnalyzer" code:code userInfo:@{ NSLocalizedDescriptionKey: msg ?: @"" }]; } - (void)runForSelfWithHeaderInfo:(SCIPAHeaderInfo)headerInfo progress:(SCIPAProgress)progress completion:(SCIPACompletion)completion { if (self.isRunning) { if (completion) completion(nil, [self errorWithCode:SCIProfileAnalyzerErrorCancelled message:SCILocalized(@"Another analysis is already running")]); return; } self.isRunning = YES; self.cancelled = NO; NSString *selfPK = [SCIUtils currentUserPK]; if (!selfPK.length) { [self finishWithSnapshot:nil error:[self errorWithCode:SCIProfileAnalyzerErrorNoSession message:SCILocalized(@"No active Instagram session found")] completion:completion]; return; } __weak typeof(self) weakSelf = self; [self reportProgress:progress status:SCILocalized(@"Fetching profile info…") fraction:0.02]; [SCIInstagramAPI sendRequestWithMethod:@"GET" path:[NSString stringWithFormat:@"users/%@/info/", selfPK] body:nil completion:^(NSDictionary *resp, NSError *error) { typeof(self) strongSelf = weakSelf; if (!strongSelf) return; if (strongSelf.cancelled) { [strongSelf finishWithSnapshot:nil error:[strongSelf errorWithCode:SCIProfileAnalyzerErrorCancelled message:SCILocalized(@"Cancelled")] completion:completion]; return; } NSDictionary *user = [resp[@"user"] isKindOfClass:[NSDictionary class]] ? resp[@"user"] : nil; if (!user) { [strongSelf finishWithSnapshot:nil error:[strongSelf errorWithCode:SCIProfileAnalyzerErrorNetwork message:SCILocalized(@"Couldn't fetch profile information")] completion:completion]; return; } NSInteger followerCount = [user[@"follower_count"] integerValue]; if (followerCount > SCIProfileAnalyzerMaxFollowerCount) { [strongSelf finishWithSnapshot:nil error:[strongSelf errorWithCode:SCIProfileAnalyzerErrorTooManyFollowers message:SCILocalized(@"Too many followers to analyze")] completion:completion]; return; } SCIProfileAnalyzerSnapshot *snap = [SCIProfileAnalyzerSnapshot new]; snap.selfPK = selfPK; snap.selfUsername = user[@"username"]; snap.selfFullName = user[@"full_name"]; snap.selfProfilePicURL = user[@"profile_pic_url"]; snap.followerCount = followerCount; snap.followingCount = [user[@"following_count"] integerValue]; snap.mediaCount = [user[@"media_count"] integerValue]; snap.scanDate = [NSDate date]; strongSelf->_expectedFollowers = followerCount; strongSelf->_expectedFollowing = snap.followingCount; if (headerInfo) dispatch_async(dispatch_get_main_queue(), ^{ headerInfo(user); }); [strongSelf fetchFollowersForPK:selfPK snapshot:snap progress:progress completion:completion]; }]; } - (void)reportProgress:(SCIPAProgress)p status:(NSString *)s fraction:(double)f { if (!p) return; dispatch_async(dispatch_get_main_queue(), ^{ p(s, f); }); } #pragma mark - Paginated fetchers - (void)fetchFollowersForPK:(NSString *)pk snapshot:(SCIProfileAnalyzerSnapshot *)snap progress:(SCIPAProgress)progress completion:(SCIPACompletion)completion { NSMutableArray *acc = [NSMutableArray array]; [self pagePath:[NSString stringWithFormat:@"friendships/%@/followers/", pk] acc:acc maxId:nil total:snap.followerCount stage:@"followers" progress:progress completion:^(NSArray *users, NSError *error) { if (error || self.cancelled) { [self finishWithSnapshot:nil error:error ?: [self errorWithCode:SCIProfileAnalyzerErrorCancelled message:SCILocalized(@"Cancelled")] completion:completion]; return; } snap.followers = users; [self fetchFollowingForPK:pk snapshot:snap progress:progress completion:completion]; }]; } - (void)fetchFollowingForPK:(NSString *)pk snapshot:(SCIProfileAnalyzerSnapshot *)snap progress:(SCIPAProgress)progress completion:(SCIPACompletion)completion { NSMutableArray *acc = [NSMutableArray array]; [self pagePath:[NSString stringWithFormat:@"friendships/%@/following/", pk] acc:acc maxId:nil total:snap.followingCount stage:@"following" progress:progress completion:^(NSArray *users, NSError *error) { if (error || self.cancelled) { [self finishWithSnapshot:nil error:error ?: [self errorWithCode:SCIProfileAnalyzerErrorCancelled message:SCILocalized(@"Cancelled")] completion:completion]; return; } snap.following = users; [self finishWithSnapshot:snap error:nil completion:completion]; }]; } - (void)pagePath:(NSString *)basePath acc:(NSMutableArray *)acc maxId:(NSString *)maxId total:(NSInteger)total stage:(NSString *)stage progress:(SCIPAProgress)progress completion:(void(^)(NSArray *users, NSError *error))completion { if (self.cancelled) { completion(nil, [self errorWithCode:SCIProfileAnalyzerErrorCancelled message:SCILocalized(@"Cancelled")]); return; } NSString *path = maxId.length ? [NSString stringWithFormat:@"%@?max_id=%@", basePath, maxId] : basePath; __weak typeof(self) weakSelf = self; [SCIInstagramAPI sendRequestWithMethod:@"GET" path:path body:nil completion:^(NSDictionary *resp, NSError *error) { typeof(self) strongSelf = weakSelf; if (!strongSelf) return; if (error) { completion(nil, [strongSelf errorWithCode:SCIProfileAnalyzerErrorNetwork message:error.localizedDescription]); return; } NSArray *users = resp[@"users"]; if ([users isKindOfClass:[NSArray class]]) { for (NSDictionary *d in users) { SCIProfileAnalyzerUser *u = [SCIProfileAnalyzerUser userFromAPIDict:d]; if (u) [acc addObject:u]; } } // Weight each stage by its share of expected work so the ring moves // proportionally regardless of follower/following ratio. 3% reserved // up front for the initial user-info call. NSInteger followerTarget = strongSelf->_expectedFollowers; NSInteger followingTarget = strongSelf->_expectedFollowing; double total0 = MAX(1, followerTarget + followingTarget); double stageWeight = ([stage isEqualToString:@"followers"] ? followerTarget : followingTarget) / total0; double stageOffset = ([stage isEqualToString:@"followers"] ? 0.0 : (double)followerTarget / total0); double stageLocal = total > 0 ? MIN(1.0, (double)acc.count / (double)total) : 0; double frac = 0.03 + (stageOffset + stageLocal * stageWeight) * 0.97; NSString *fmt = [stage isEqualToString:@"followers"] ? SCILocalized(@"Fetching followers (%lu/%ld)…") : SCILocalized(@"Fetching following (%lu/%ld)…"); NSString *label = [NSString stringWithFormat:fmt, (unsigned long)acc.count, (long)total]; [strongSelf reportProgress:progress status:label fraction:frac]; id next = resp[@"next_max_id"]; NSString *nextMax = [next isKindOfClass:[NSString class]] ? next : ([next respondsToSelector:@selector(stringValue)] ? [next stringValue] : nil); if (!nextMax.length || strongSelf.cancelled) { completion(acc, strongSelf.cancelled ? [strongSelf errorWithCode:SCIProfileAnalyzerErrorCancelled message:SCILocalized(@"Cancelled")] : nil); return; } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(SCI_PA_PAGE_DELAY_S * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [strongSelf pagePath:basePath acc:acc maxId:nextMax total:total stage:stage progress:progress completion:completion]; }); }]; } @end