diff --git a/src/Contacts/ContactsUpdater.m b/src/Contacts/ContactsUpdater.m index 61168eafc..88e68e0d4 100644 --- a/src/Contacts/ContactsUpdater.m +++ b/src/Contacts/ContactsUpdater.m @@ -62,7 +62,8 @@ failure:(void (^)(NSError *error))failure { if(!identifier) { - NSError *error = OWSErrorWithCodeDescription(1, @"Cannot lookup nil identifier"); + NSError *error + = OWSErrorWithCodeDescription(OWSErrorCodeInvalidMethodParameters, @"Cannot lookup nil identifier"); BLOCK_SAFE_RUN(failure, error); return; } diff --git a/src/Devices/OWSDevice.h b/src/Devices/OWSDevice.h new file mode 100644 index 000000000..6c3566bbf --- /dev/null +++ b/src/Devices/OWSDevice.h @@ -0,0 +1,21 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "TSYapDatabaseObject.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSDevice : TSYapDatabaseObject + +@property (nonatomic, readonly) NSInteger deviceId; +@property (nullable, nonatomic, readonly) NSString *name; +@property (nonatomic, readonly) NSDate *createdAt; +@property (nonatomic, readonly) NSDate *lastSeenAt; + ++ (instancetype)deviceFromJSONDictionary:(NSDictionary *)deviceAttributes error:(NSError **)error; ++ (NSArray *)secondaryDevices; ++ (void)replaceAll:(NSArray *)devices; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/Devices/OWSDevice.m b/src/Devices/OWSDevice.m new file mode 100644 index 000000000..ca7741750 --- /dev/null +++ b/src/Devices/OWSDevice.m @@ -0,0 +1,120 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "OWSDevice.h" +#import "NSDate+millisecondTimeStamp.h" +#import "OWSError.h" +#import "YapDatabaseConnection.h" +#import "YapDatabaseTransaction.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +static MTLValueTransformer *_millisecondTimestampToDateTransformer; +static int const OWSDevicePrimaryDeviceId = 1; + +@implementation OWSDevice + +@synthesize name = _name; + ++ (instancetype)deviceFromJSONDictionary:(NSDictionary *)deviceAttributes error:(NSError **)error +{ + return [MTLJSONAdapter modelOfClass:[self class] fromJSONDictionary:deviceAttributes error:error]; +} + ++ (NSDictionary *)JSONKeyPathsByPropertyKey +{ + return @{ + @"createdAt": @"created", + @"lastSeenAt": @"lastSeen", + @"deviceId": @"id", + @"name": @"name" + }; +} + ++ (MTLValueTransformer *)createdAtJSONTransformer +{ + return self.millisecondTimestampToDateTransformer; +} + ++ (MTLValueTransformer *)lastSeenAtJSONTransformer +{ + return self.millisecondTimestampToDateTransformer; +} + ++ (void)replaceAll:(NSArray *)devices +{ + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [transaction removeAllObjectsInCollection:[self collection]]; + for (OWSDevice *device in devices) { + [device saveWithTransaction:transaction]; + } + }]; +} + ++ (MTLValueTransformer *)millisecondTimestampToDateTransformer +{ + if (!_millisecondTimestampToDateTransformer) { + _millisecondTimestampToDateTransformer = + [MTLValueTransformer transformerUsingForwardBlock:^id(id value, BOOL *success, NSError **error) { + if ([value isKindOfClass:[NSNumber class]]) { + NSNumber *number = (NSNumber *)value; + NSDate *result = [NSDate ows_dateWithMillisecondsSince1970:[number longLongValue]]; + if (result) { + *success = YES; + return result; + } + } + *success = NO; + *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecodeJson, + [NSString stringWithFormat:@"unable to decode date from %@", value]); + return nil; + } + reverseBlock:^id(id value, BOOL *success, NSError **error) { + if ([value isKindOfClass:[NSDate class]]) { + NSDate *date = (NSDate *)value; + NSNumber *result = [NSNumber numberWithLongLong:[NSDate ows_millisecondsSince1970ForDate:date]]; + if (result) { + *success = YES; + return result; + } + } + *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToEncodeJson, + [NSString stringWithFormat:@"unable to encode date from %@", value]); + *success = NO; + return nil; + }]; + } + return _millisecondTimestampToDateTransformer; +} + ++ (NSArray *)secondaryDevices +{ + NSMutableArray *devices = [NSMutableArray new]; + + [self enumerateCollectionObjectsUsingBlock:^(id obj, BOOL *stop) { + if ([obj isKindOfClass:[OWSDevice class]]) { + OWSDevice *device = (OWSDevice *)obj; + if (device.deviceId != OWSDevicePrimaryDeviceId) { + [devices addObject:device]; + } + } + }]; + + return [devices copy]; +} + +- (nullable NSString *)name +{ + if (_name) { + return _name; + } + + if (self.deviceId == OWSDevicePrimaryDeviceId) { + return @"This Device"; + } + return NSLocalizedString(@"UNNAMED_DEVICE", @"Label text in device manager for a device with no name"); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/Network/API/OWSDevicesService.h b/src/Network/API/OWSDevicesService.h new file mode 100644 index 000000000..90acff721 --- /dev/null +++ b/src/Network/API/OWSDevicesService.h @@ -0,0 +1,18 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +NS_ASSUME_NONNULL_BEGIN + +@class OWSDevice; + +@interface OWSDevicesService : NSObject + +- (void)getDevicesWithSuccess:(void (^)(NSArray *))successCallback + failure:(void (^)(NSError *))failureCallback; + +- (void)unlinkDevice:(OWSDevice *)device + success:(void (^)())successCallback + failure:(void (^)(NSError *))failureCallback; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/Network/API/OWSDevicesService.m b/src/Network/API/OWSDevicesService.m new file mode 100644 index 000000000..3c75d8c0a --- /dev/null +++ b/src/Network/API/OWSDevicesService.m @@ -0,0 +1,84 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "OWSDevicesService.h" +#import "OWSDeleteDeviceRequest.h" +#import "OWSDevice.h" +#import "OWSError.h" +#import "OWSGetDevicesRequest.h" +#import "TSNetworkManager.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSDevicesService + +- (void)getDevicesWithSuccess:(void (^)(NSArray *))successCallback + failure:(void (^)(NSError *))failureCallback +{ + OWSGetDevicesRequest *request = [OWSGetDevicesRequest new]; + [[TSNetworkManager sharedManager] makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + DDLogVerbose(@"Get devices request succeeded"); + NSArray *devices = [self parseResponse:responseObject]; + + if (devices) { + successCallback(devices); + } else { + failureCallback(OWSErrorWithCodeDescription( + OWSErrorCodeUnableToProcessServerResponse, @"Unable to parse server response")); + } + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + DDLogVerbose(@"Get devices request failed with error: %@", error); + failureCallback(error); + }]; +} + +- (void)unlinkDevice:(OWSDevice *)device + success:(void (^)())successCallback + failure:(void (^)(NSError *))failureCallback +{ + OWSDeleteDeviceRequest *request = [[OWSDeleteDeviceRequest alloc] initWithDevice:device]; + + [[TSNetworkManager sharedManager] makeRequest:request + success:^(NSURLSessionDataTask *task, id responseObject) { + DDLogVerbose(@"Delete device request succeeded"); + successCallback(); + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + DDLogVerbose(@"Get devices request failed with error: %@", error); + failureCallback(error); + }]; +} + +- (NSArray *)parseResponse:(id)responseObject +{ + if (![responseObject isKindOfClass:[NSDictionary class]]) { + DDLogError(@"Device response was not a dictionary."); + return nil; + } + NSDictionary *response = (NSDictionary *)responseObject; + + NSArray *devicesAttributes = response[@"devices"]; + if (!devicesAttributes) { + DDLogError(@"Device response had no devices."); + return nil; + } + + NSMutableArray *devices = [NSMutableArray new]; + for (NSDictionary *deviceAttributes in devicesAttributes) { + NSError *error; + OWSDevice *device = [OWSDevice deviceFromJSONDictionary:deviceAttributes error:&error]; + if (error) { + DDLogError(@"Failed to build device from dictionary with error: %@", error); + } else { + [devices addObject:device]; + } + } + + return [devices copy]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/Network/API/Requests/OWSDeleteDeviceRequest.h b/src/Network/API/Requests/OWSDeleteDeviceRequest.h new file mode 100644 index 000000000..044cd057e --- /dev/null +++ b/src/Network/API/Requests/OWSDeleteDeviceRequest.h @@ -0,0 +1,15 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "TSRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +@class OWSDevice; + +@interface OWSDeleteDeviceRequest : TSRequest + +- (instancetype)initWithDevice:(OWSDevice *)device; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/src/Network/API/Requests/OWSDeleteDeviceRequest.m b/src/Network/API/Requests/OWSDeleteDeviceRequest.m new file mode 100644 index 000000000..891f651b6 --- /dev/null +++ b/src/Network/API/Requests/OWSDeleteDeviceRequest.m @@ -0,0 +1,27 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "OWSDeleteDeviceRequest.h" +#import "OWSDevice.h" +#import "TSConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSDeleteDeviceRequest + +- (instancetype)initWithDevice:(OWSDevice *)device +{ + NSString *deleteDevicePath = [NSString + stringWithFormat:textSecureDevicesAPIFormat, [NSString stringWithFormat:@"%ld", (long)device.deviceId]]; + self = [super initWithURL:[NSURL URLWithString:deleteDevicePath]]; + if (!self) { + return self; + } + + [self setHTTPMethod:@"DELETE"]; + + return self; +} + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/src/Network/API/Requests/OWSGetDevicesRequest.h b/src/Network/API/Requests/OWSGetDevicesRequest.h new file mode 100644 index 000000000..438dd760e --- /dev/null +++ b/src/Network/API/Requests/OWSGetDevicesRequest.h @@ -0,0 +1,11 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "TSRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSGetDevicesRequest : TSRequest + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/Network/API/Requests/OWSGetDevicesRequest.m b/src/Network/API/Requests/OWSGetDevicesRequest.m new file mode 100644 index 000000000..b2018315b --- /dev/null +++ b/src/Network/API/Requests/OWSGetDevicesRequest.m @@ -0,0 +1,25 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "OWSGetDevicesRequest.h" +#import "TSConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSGetDevicesRequest + +- (instancetype)init +{ + NSString *getDevicesPath = [NSString stringWithFormat:textSecureDevicesAPIFormat, @""]; + self = [super initWithURL:[NSURL URLWithString:getDevicesPath]]; + if (!self) { + return self; + } + + [self setHTTPMethod:@"GET"]; + + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/TSConstants.h b/src/TSConstants.h index 1f8a69d09..0c6631184 100644 --- a/src/TSConstants.h +++ b/src/TSConstants.h @@ -38,6 +38,7 @@ typedef enum { kSMSVerification, kPhoneNumberVerification } VerificationTranspor #define textSecureAttachmentsAPI @"v1/attachments" #define textSecureDeviceProvisioningCodeAPI @"v1/devices/provisioning/code" #define textSecureDeviceProvisioningAPIFormat @"v1/provisioning/%@" +#define textSecureDevicesAPIFormat @"v1/devices/%@" typedef void (^successCompletionBlock)(void); typedef void (^failedRegistrationRequestBlock)(void); diff --git a/src/Util/NSDate+millisecondTimeStamp.h b/src/Util/NSDate+millisecondTimeStamp.h index 9f3032162..0dac0ced9 100644 --- a/src/Util/NSDate+millisecondTimeStamp.h +++ b/src/Util/NSDate+millisecondTimeStamp.h @@ -1,13 +1,11 @@ -// -// NSDate+millisecondTimeStamp.h -// Signal -// // Created by Frederic Jacobs on 25/11/14. // Copyright (c) 2014 Open Whisper Systems. All rights reserved. -// -#import @interface NSDate (millisecondTimeStamp) + + (uint64_t)ows_millisecondTimeStamp; ++ (NSDate *)ows_dateWithMillisecondsSince1970:(uint64_t)milliseconds; ++ (uint64_t)ows_millisecondsSince1970ForDate:(NSDate *)date; + @end diff --git a/src/Util/NSDate+millisecondTimeStamp.mm b/src/Util/NSDate+millisecondTimeStamp.mm index cb2cd44fc..6814990ec 100644 --- a/src/Util/NSDate+millisecondTimeStamp.mm +++ b/src/Util/NSDate+millisecondTimeStamp.mm @@ -1,10 +1,5 @@ -// -// NSDate+millisecondTimeStamp.m -// Signal -// // Created by Frederic Jacobs on 25/11/14. // Copyright (c) 2014 Open Whisper Systems. All rights reserved. -// #import #import "NSDate+millisecondTimeStamp.h" @@ -17,4 +12,14 @@ return milliseconds; } ++ (NSDate *)ows_dateWithMillisecondsSince1970:(uint64_t)milliseconds +{ + return [NSDate dateWithTimeIntervalSince1970:(milliseconds / 1000.0)]; +} + ++ (uint64_t)ows_millisecondsSince1970ForDate:(NSDate *)date +{ + return (uint64_t)(date.timeIntervalSince1970 * 1000); +} + @end diff --git a/src/Util/OWSError.h b/src/Util/OWSError.h index 500867b4b..3cc807eeb 100644 --- a/src/Util/OWSError.h +++ b/src/Util/OWSError.h @@ -4,6 +4,13 @@ NS_ASSUME_NONNULL_BEGIN extern NSString *const OWSSignalServiceKitErrorDomain; -extern NSError *OWSErrorWithCodeDescription(NSInteger code, NSString *description); +typedef NS_ENUM(NSInteger, OWSErrorCode) { + OWSErrorCodeInvalidMethodParameters = 11, + OWSErrorCodeUnableToProcessServerResponse = 12, + OWSErrorCodeFailedToDecodeJson = 13, + OWSErrorCodeFailedToEncodeJson = 14 +}; + +extern NSError *OWSErrorWithCodeDescription(OWSErrorCode code, NSString *description); NS_ASSUME_NONNULL_END diff --git a/src/Util/OWSError.m b/src/Util/OWSError.m index b35f82b12..e37b700e3 100644 --- a/src/Util/OWSError.m +++ b/src/Util/OWSError.m @@ -6,7 +6,7 @@ NS_ASSUME_NONNULL_BEGIN NSString *const OWSSignalServiceKitErrorDomain = @"OWSSignalServiceKitErrorDomain"; -NSError *OWSErrorWithCodeDescription(NSInteger code, NSString *description) +NSError *OWSErrorWithCodeDescription(OWSErrorCode code, NSString *description) { return [NSError errorWithDomain:OWSSignalServiceKitErrorDomain code:code