// _____ _
// |_ _| |_ _ _ ___ ___ _ __ __ _
// | | | ' \| '_/ -_) -_) ' \/ _` |_
// |_| |_||_|_| \___\___|_|_|_\__,_(_)
//
// Threema iOS Client
// Copyright (c) 2015-2020 Threema GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License, version 3,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
#import "ScreenshotJsonParser.h"
#import "MyIdentityStore.h"
#import "EntityManager.h"
#import "Contact.h"
#import "Conversation.h"
#import "GroupProxy.h"
#import "NSString+Hex.h"
#import "MediaConverter.h"
#import "ProtocolDefines.h"
#import "ServerConnector.h"
#import "UserSettings.h"
#import "LicenseStore.h"
#import
@interface ScreenshotJsonParser ()
@property NSString *myIdentity;
@property NSBundle *bundle;
@property EntityManager *entityManager;
@end
@implementation ScreenshotJsonParser
- (instancetype)init
{
self = [super init];
if (self) {
_entityManager = [[EntityManager alloc] initForBackgroundProcess:NO];
}
return self;
}
- (void)clearAll {
[[ServerConnector sharedServerConnector] disconnect];
MyIdentityStore *myIdentityStore = [MyIdentityStore sharedMyIdentityStore];
[myIdentityStore destroy];
[_entityManager performSyncBlockAndSafe:^{
NSArray *conversations = [_entityManager.entityFetcher allConversations];
for (Conversation* conversation in conversations) {
[[_entityManager entityDestroyer] deleteObjectWithObject:conversation];
}
NSArray *contacts = [_entityManager.entityFetcher allContacts];
for (Contact* contact in contacts) {
[[_entityManager entityDestroyer] deleteObjectWithObject:contact];
}
}];
}
- (void)loadDataFromDirectory:(NSString*)directory {
_bundle = [NSBundle bundleWithPath:directory];
NSString *path = [_bundle pathForResource:@"data" ofType:@"json"];
NSData *jsonData = [NSData dataWithContentsOfFile:path];
NSError *error;
NSDictionary *json = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (json == nil) {
NSLog(@"Error parsing ballot json data %@, %@", error, [error userInfo]);
return;
}
NSString *idBackup = [json objectForKey:@"identity"];
[self handleIdBackup:idBackup];
_myIdentity = [MyIdentityStore sharedMyIdentityStore].identity;
NSMutableDictionary *profile = [NSMutableDictionary new];
[profile setValue:[self imageDataFromFileNamed:@"me.jpg"] forKey:@"ProfilePicture"];
[profile removeObjectForKey:@"LastUpload"];
[[MyIdentityStore sharedMyIdentityStore] setProfilePicture:profile];
if ([LicenseStore requiresLicenseKey]) {
[[MyIdentityStore sharedMyIdentityStore] setPushFromName:@"Julia S."];
[[MyIdentityStore sharedMyIdentityStore] setLinkedEmail:@"***@***"];
[[MyIdentityStore sharedMyIdentityStore] setLinkedMobileNo:@"1234567890"];
[[MyIdentityStore sharedMyIdentityStore] setLinkEmailPending:false];
[[MyIdentityStore sharedMyIdentityStore] setLinkMobileNoPending:false];
} else {
[[MyIdentityStore sharedMyIdentityStore] setPushFromName:@"Eva Anonymous"];
}
[_entityManager performSyncBlockAndSafe:^{
NSDictionary *contactData = [json objectForKey:@"contacts"];
[self handleContacts:contactData];
}];
[_entityManager performSyncBlockAndSafe:^{
NSDictionary *groupData = [json objectForKey:@"groups"];
[self handleGroups:groupData];
}];
}
- (void)handleIdBackup:(NSString *)idBackup {
MyIdentityStore *myIdentityStore = [MyIdentityStore sharedMyIdentityStore];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
[myIdentityStore restoreFromBackup:idBackup withPassword:@"12345678" onCompletion:^{
myIdentityStore.serverGroup = @"ae";
[myIdentityStore storeInKeychain];
dispatch_semaphore_signal(sema);
} onError:^(NSError *error) {
[self fail:error.localizedDescription];
dispatch_semaphore_signal(sema);
}];
});
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
- (void)fail:(NSString *)reason {
NSLog(@"failed: %@", reason);
NSAssert(FALSE, reason);
}
- (void)handleContacts:(NSDictionary *)contactData {
NSEnumerator *enumerator = [contactData keyEnumerator];
id identity;
while ((identity = [enumerator nextObject])) {
NSDictionary *data = [contactData objectForKey:identity];
[self handleContact:identity data:data];
}
}
- (void)handleContact:(NSString *)identity data:(NSDictionary *)data {
Contact *contact = [_entityManager.entityCreator contact];
contact.identity = identity;
contact.imageData = [self localizedImageForKey:@"avatar" in:data];
contact.verifiedEmail = [self localizedStringForKey:@"mail" in:data];
NSNumber *verificationLevel = [data objectForKey:@"verification"];
contact.verificationLevel = [NSNumber numberWithInt: verificationLevel.intValue - 1];
BOOL isWork = [[data objectForKey:@"isWork"] boolValue];
if (isWork) {
contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified];
if ([LicenseStore requiresLicenseKey]) {
contact.workContact = [NSNumber numberWithBool:YES];
}
NSMutableOrderedSet *workIdentities = [[NSMutableOrderedSet alloc] initWithOrderedSet:[UserSettings sharedUserSettings].workIdentities];
[workIdentities addObject:contact.identity];
[UserSettings sharedUserSettings].workIdentities = workIdentities;
}
NSString *publicKey = [data objectForKey:@"pk"];
contact.publicKey = [publicKey decodeHex];
NSArray *names = [self localizedArrayForKey:@"name" in:data];
if (names.count == 2) {
contact.firstName = names[0];
contact.lastName = names[1];
}
NSArray *conversationData = [data objectForKey:@"conversation"];
if (conversationData) {
Conversation *conversation = [_entityManager.entityCreator conversation];
conversation.contact = contact;
[self handleConversation:conversation data:conversationData];
}
}
- (void)handleConversation:(Conversation *)conversation data:(NSArray *)data {
for (NSDictionary *messageData in data) {
NSString *type = [messageData objectForKey:@"type"];
BaseMessage *message;
if ([type isEqualToString:@"TEXT"]) {
message = [self handleTextMessage:messageData inConversation:conversation];
} else if ([type isEqualToString:@"IMAGE"]) {
message = [self handleImageMessage:messageData inConversation:conversation];
} else if ([type isEqualToString:@"AUDIO"]) {
message = [self handleAudioMessage:messageData inConversation:conversation];
} else if ([type isEqualToString:@"BALLOT"]) {
message = [self handleBallotMessage:messageData inConversation:conversation];
} else if ([type isEqualToString:@"FILE"]) {
message = [self handleFileMessage:messageData inConversation:conversation];
} else if ([type isEqualToString:@"VOIP_STATUS"]) {
message = [self handleCallMessage:messageData inConversation:conversation];
} else if ([type isEqualToString:@"LOCATION"]) {
message = [self handleLocationMessage:messageData inConversation:conversation];
}
NSNumber *dateOffsetMin = [messageData objectForKey:@"date"];
NSInteger dateOffsetS = dateOffsetMin.integerValue * 60;
message.date = [NSDate dateWithTimeInterval:dateOffsetS sinceDate:_referenceDate];
BOOL outgoing = ((NSNumber *)[messageData objectForKey:@"out"]).boolValue;
message.isOwn = [NSNumber numberWithBool:outgoing];
if (outgoing == NO) {
message.sender = conversation.contact;
}
message.sent = [NSNumber numberWithBool:YES];
message.delivered = [NSNumber numberWithBool:YES];
NSNumber *read = ((NSNumber *)[messageData objectForKey:@"read"]);
if (!read) {
message.read = [NSNumber numberWithBool:YES];
} else {
message.read = read;
int unreadMessageCount = conversation.unreadMessageCount.intValue;
unreadMessageCount++;
conversation.unreadMessageCount = [NSNumber numberWithInt:unreadMessageCount];
}
NSString *state = [messageData objectForKey:@"state"];
if ([state isEqualToString:@"USERACK"]) {
message.userackDate = [NSDate date];
message.userack = [NSNumber numberWithBool:YES];
}
// groups only
NSString *identity = [messageData objectForKey:@"identity"];
if ([identity isEqualToString:@"$"] == NO) {
message.sender = [_entityManager.entityFetcher contactForId:identity];
}
}
}
- (Conversation *)createGroupConversationFromData:(NSDictionary *)groupData {
Conversation *conversation = [_entityManager.entityCreator conversation];
NSString *groupId = [groupData objectForKey:@"id"];
conversation.groupId = [groupId decodeHex];
NSString *creator = [groupData objectForKey:@"creator"];
if ([creator isEqualToString:_myIdentity] == NO) {
Contact *creatorContact = [_entityManager.entityFetcher contactForId:creator];
conversation.contact = creatorContact;
}
NSData *avatar = [self localizedImageForKey:@"avatar" in:groupData];
if (avatar) {
ImageData *dbImage = [_entityManager.entityCreator imageData];
dbImage.data = avatar;
conversation.groupImage = dbImage;
}
conversation.groupName = [self localizedStringForKey:@"name" in:groupData];
conversation.groupMyIdentity = [MyIdentityStore sharedMyIdentityStore].identity;
NSArray *memberNames = [groupData objectForKey:@"members"];
NSSet *members = [self groupMembersFrom:memberNames];
[conversation addMembers:members];
return conversation;
}
- (NSSet *)groupMembersFrom:(NSArray *)memberNames {
NSMutableSet *members = [NSMutableSet setWithCapacity:memberNames.count];
for (NSString *name in memberNames) {
Contact *contact = [_entityManager.entityFetcher contactForId:name];
[members addObject:contact];
}
return members;
}
- (void)handleGroups:(NSDictionary *)groupsData {
for (NSDictionary *groupData in groupsData) {
Conversation *conversation = [self createGroupConversationFromData:groupData];
NSArray *conversationData = [groupData objectForKey:@"conversation"];
if (conversationData) {
[self handleConversation:conversation data:conversationData];
}
}
}
- (BaseMessage *)handleTextMessage:(NSDictionary *)messageData inConversation:(Conversation *)conversation {
TextMessage *message = [_entityManager.entityCreator textMessageForConversation:conversation];
message.text = [self localizedStringForKey:@"content" in:messageData];
return message;
}
- (BaseMessage *)handleImageMessage:(NSDictionary *)messageData inConversation:(Conversation *)conversation {
ImageMessage *message = [_entityManager.entityCreator imageMessageForConversation:conversation];
ImageData *imageData = [_entityManager.entityCreator imageData];
NSDictionary *captions = messageData[@"caption"];
NSString *caption = nil;
if (captions) {
caption = [captions objectForKey:_languageCode];
if (caption == nil) {
caption = [captions objectForKey:@"default"];
}
}
[imageData setCaption:caption];
NSData *data = [self localizedImageForKey:@"content" in:messageData];
if (data) {
imageData.data = data;
[imageData setCaption:caption];
UIImage *image = [UIImage imageWithData:data];
UIImage *thumbnail = [MediaConverter getThumbnailForImage:image];
NSData *thumbnailData = UIImageJPEGRepresentation(thumbnail, kJPEGCompressionQuality);
ImageData *dbThumbnail = [_entityManager.entityCreator imageData];
dbThumbnail.data = thumbnailData;
dbThumbnail.width = [NSNumber numberWithInt:thumbnail.size.width];
dbThumbnail.height = [NSNumber numberWithInt:thumbnail.size.height];
[imageData setCaption:caption];
message.thumbnail = dbThumbnail;
} else {
imageData.data = [NSData data];
}
message.image = imageData;
return message;
}
- (BaseMessage *)handleAudioMessage:(NSDictionary *)messageData inConversation:(Conversation *)conversation {
AudioMessage *message = [_entityManager.entityCreator audioMessageForConversation:conversation];
message.duration = [NSNumber numberWithInt:42];
return message;
}
- (BaseMessage *)handleLocationMessage:(NSDictionary *)messageData inConversation:(Conversation *)conversation {
LocationMessage *message = [_entityManager.entityCreator locationMessageForConversation:conversation];
NSArray *location = [self localizedArrayForKey:@"content" in:messageData];
if (location.count == 4) {
message.latitude = location[0];
message.longitude = location[1];
message.accuracy = location[2];
message.poiName = location[3];
}
return message;
}
- (BaseMessage *)handleFileMessage:(NSDictionary *)messageData inConversation:(Conversation *)conversation {
FileMessage *message = [_entityManager.entityCreator fileMessageForConversation:conversation];
message.fileName = [self localizedStringForKey:@"content" in:messageData];
message.fileSize = [NSNumber numberWithInt:2308565];
return message;
}
- (BaseMessage *)handleCallMessage:(NSDictionary *)messageData inConversation:(Conversation *)conversation {
SystemMessage *message = [_entityManager.entityCreator systemMessageForConversation:conversation];
BOOL outgoing = ((NSNumber *)[messageData objectForKey:@"out"]).boolValue;
NSDictionary *callData = [messageData objectForKey:@"content"];
NSString *type = [callData objectForKey:@"type"];
if ([type isEqualToString:@"answered"]) {
int duration = ((NSNumber *)[callData objectForKey:@"duration"]).intValue;
NSString *durationString = [self timeFormatted:duration];
NSDictionary *argDict = @{@"DateString": [DateFormatter shortStyleTimeNoDate:[NSDate date]], @"CallTime": durationString, @"CallInitiator": [NSNumber numberWithBool:outgoing]};
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:argDict options:NSJSONWritingPrettyPrinted error:&error];
message.arg = data;
message.type = [NSNumber numberWithInteger:kSystemMessageCallEnded];
conversation.lastMessage = message;
}
return message;
}
- (BaseMessage *)handleBallotMessage:(NSDictionary *)messageData inConversation:(Conversation *)conversation {
BallotMessage *message = [_entityManager.entityCreator ballotMessageForConversation:conversation];
NSDictionary *ballotData = [messageData objectForKey:@"content"];
Ballot *ballot = [_entityManager.entityCreator ballot];
NSData *idData = [NSData data];
ballot.id = idData;
ballot.title = [self localizedStringForKey:@"question" in:ballotData];
ballot.conversation = conversation;
if ([[ballotData objectForKey:@"state"] isEqualToString:@"closed"]) {
[ballot setClosed];
}
NSArray *choices = [ballotData objectForKey:@"choices"];
[self handleBallot:ballot choices:choices];
message.ballot = ballot;
return message;
}
- (void)handleBallot:(Ballot *)ballot choices:(NSArray *)choices {
NSInteger count = 0;
for (NSDictionary *choiceData in choices) {
BallotChoice *ballotChoice = [_entityManager.entityCreator ballotChoice];
ballotChoice.name = [self localizedStringForKey:@"choice" in:choiceData];
ballotChoice.id = [NSNumber numberWithInteger:count];
ballotChoice.orderPosition = [NSNumber numberWithInteger:count];
NSDictionary *votes = [choiceData objectForKey:@"votes"];
[self handleBallotChoice:ballotChoice voteData:votes];
[ballot addChoicesObject:ballotChoice];
++count;
}
}
- (void)handleBallotChoice:(BallotChoice *)choice voteData:(NSDictionary *)voteData {
NSEnumerator *enumerator = [voteData keyEnumerator];
NSString *identity;
while ((identity = [enumerator nextObject])) {
BallotResult *ballotResult = [_entityManager.entityCreator ballotResult];
ballotResult.value = [voteData objectForKey:identity];
if ([identity isEqualToString:@"$"]) {
ballotResult.participantId = _myIdentity;
} else {
ballotResult.participantId = identity;
}
[choice addResultObject:ballotResult];
}
}
- (id)localizedObjectForKey:(NSString *)key in:(NSDictionary *)dictionary {
NSDictionary *localizations = [dictionary objectForKey:key];
id result = [localizations objectForKey:_languageCode];
if (result == nil) {
result = [localizations objectForKey:@"default"];
}
return result;
}
- (NSString *)localizedStringForKey:(NSString *)key in:(NSDictionary *)dictionary {
return (NSString *)[self localizedObjectForKey:key in:dictionary];
}
- (NSData *)localizedImageForKey:(NSString *)key in:(NSDictionary *)dictionary {
NSString *imageName = [self localizedObjectForKey:key in:dictionary];
return [self imageDataFromFileNamed:imageName];
}
- (NSArray *)localizedArrayForKey:(NSString *)key in:(NSDictionary *)dictionary {
return (NSArray *)[self localizedObjectForKey:key in:dictionary];
}
- (NSData *)imageDataFromFileNamed:(NSString *)imageName {
NSString *path = [_bundle pathForResource:imageName ofType:nil];
NSData *data = [NSData dataWithContentsOfFile:path];
return data;
}
- (NSString *)timeFormatted:(int)totalSeconds {
int seconds = totalSeconds % 60;
int minutes = (totalSeconds / 60);
return [NSString stringWithFormat:@"%02d:%02d", minutes, seconds];
}
@end