// // HPRPCClient.m // HotPocket // // Created by Tomek Wójcik on 19/09/2025. // #import "HPRPCClient.h" @implementation HPRPCCallResult #pragma mark - HPRPCCallResult implementation -(id)init { if (self = [super init]) { self.error = nil; self.result = nil; } return self; } -(NSString *)description { NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithCapacity:2]; if (self.error == nil) { [attributes setValue:@"(null)" forKey:@"error"]; } else { [attributes setValue:self.error forKey:@"error"]; } if (self.result == nil) { [attributes setValue:@"(null)" forKey:@"result"]; } else { [attributes setValue:self.result forKey:@"result"]; } return [NSString stringWithFormat:@"<%@: %p; %@>", NSStringFromClass([self class]), self, attributes]; } @end @implementation HPRPCClient #pragma mark - Initialization -(id)initWithBaseURL:(nullable NSURL *)baseURL accessToken:(nullable NSString *)accessToken { if (self = [super init]) { self.baseURL = baseURL; self.accessToken = accessToken; self.session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.ephemeralSessionConfiguration]; } return self; } #pragma mark - Public interface -(BOOL)hasCredentials { return (self.baseURL != nil && self.accessToken != nil); } -(BOOL)call:(nullable NSString *)callId method:(NSString *)method params:(NSArray *)params endopoint:(nullable NSString *)endpoint completionHandler:(HPRPCClientCompletionHandler)completionHandler { if (self.baseURL == nil) { return NO; } if (callId == nil) { callId = [[NSUUID UUID] UUIDString]; } if (endpoint == nil) { endpoint = @"/rpc/"; } NSBundle *mainBundle = [NSBundle mainBundle]; NSDictionary *payload = @{ @"jsonrpc": @"2.0", @"id": callId, @"method": method, @"params": params, }; NSError *error; NSData *jsonPayload = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error]; if (!jsonPayload) { NSLog(@"-[HPRPCClient call:method:params:endpoint:] Unable to serialize payload: error=`%@`", error); HPRPCCallResult *result = [[HPRPCCallResult alloc] init]; result.error = error; if (self.delegate != nil) { [self.delegate rpcClientDidReceiveResult:result callId:callId]; } return NO; } NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:[self.baseURL URLByAppendingPathComponent:endpoint] resolvingAgainstBaseURL:NO]; urlComponents.queryItems = @[ [NSURLQueryItem queryItemWithName:@"method" value:method], ]; NSURL *callURL = [urlComponents URL]; #ifdef DEBUG NSLog(@"-[HPRPCClient call:method:params:endpoint:] callURL=`%@`", callURL.absoluteString); #endif NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:callURL]; request.HTTPMethod = @"POST"; request.HTTPBody = jsonPayload; [request setValue:@"application/json;charset=utf-8" forHTTPHeaderField:@"Content-Type"]; [request setValue:[[mainBundle infoDictionary] valueForKey:@"HPRPCClientOrigin"] forHTTPHeaderField:@"Origin"]; if (self.accessToken != nil) { NSString *authorization = [NSString stringWithFormat:@"Bearer %@", self.accessToken]; [request setValue:authorization forHTTPHeaderField:@"Authorization"]; } NSString *build = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; NSString *userAgent = [NSString stringWithFormat:@"HotPocket/%@", build]; [request setValue:userAgent forHTTPHeaderField:@"User-Agent"]; NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { HPRPCCallResult *result = [[HPRPCCallResult alloc] init]; if (error != nil) { result.error = error; } else { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; if (httpResponse.statusCode != 200) { result.error = [[NSError alloc] initWithDomain:@"pl.bthlabs.HotPocket.HPRPCClient" code:-32000 userInfo:@{ @"callId": callId, @"url": httpResponse.URL, @"statusCode": [NSNumber numberWithInteger:httpResponse.statusCode], @"response": response, }]; } else { NSError *jsonDecodeError; NSDictionary *callResult = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingTopLevelDictionaryAssumed error:&error]; if (jsonDecodeError != nil) { result.error = jsonDecodeError; } else { NSDictionary *rpcError = [callResult valueForKey:@"error"]; if (rpcError != nil) { NSNumber *rpcErrorCode = [rpcError valueForKey:@"code"]; if (rpcErrorCode == nil) { rpcErrorCode = [NSNumber numberWithInt:-32000]; } result.error = [[NSError alloc] initWithDomain:@"pl.bthlabs.HotPocket.HPRPCClient" code:[rpcErrorCode integerValue] userInfo:rpcError]; } else { result.result = [callResult valueForKey:@"result"]; } } } } if (completionHandler) { completionHandler(callId, result); } }]; [task resume]; return YES; } -(BOOL)call:(nullable NSString *)callId method:(NSString *)method params:(NSArray *)params endopoint:(nullable NSString *)endpoint { return [self call:callId method:method params:params endopoint:endpoint completionHandler:^(NSString *callId, HPRPCCallResult *result) { if (self.delegate != nil) { [self.delegate rpcClientDidReceiveResult:result callId:callId]; } }]; } -(BOOL)call:(nullable NSString *)callId method:(NSString *)method params:(NSArray *)params { return [self call:callId method:method params:params endopoint:nil]; } -(BOOL)call:(nullable NSString *)callId method:(NSString *)method params:(NSArray *)params completionHandler:(HPRPCClientCompletionHandler)completionHandler { return [self call:callId method:method params:params endopoint:nil completionHandler:completionHandler]; } @end