/** * @copyright Copyright (c) 2020 Ivan Sein * * @author Ivan Sein * * @license GNU GPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #import "NCDatabaseManager.h" #import "ABContact.h" #import "NCAppBranding.h" #import "NCChatBlock.h" #import "NCChatMessage.h" #import "NCContact.h" #import "NCRoom.h" NSString *const kTalkDatabaseFolder = @"Library/Application Support/Talk"; NSString *const kTalkDatabaseFileName = @"talk.realm"; uint64_t const kTalkDatabaseSchemaVersion = 31; NSString * const kCapabilitySystemMessages = @"system-messages"; NSString * const kCapabilityNotificationLevels = @"notification-levels"; NSString * const kCapabilityInviteGroupsAndMails = @"invite-groups-and-mails"; NSString * const kCapabilityLockedOneToOneRooms = @"locked-one-to-one-rooms"; NSString * const kCapabilityWebinaryLobby = @"webinary-lobby"; NSString * const kCapabilityChatReadMarker = @"chat-read-marker"; NSString * const kCapabilityStartCallFlag = @"start-call-flag"; NSString * const kCapabilityCirclesSupport = @"circles-support"; NSString * const kCapabilityChatReferenceId = @"chat-reference-id"; NSString * const kCapabilityPhonebookSearch = @"phonebook-search"; NSString * const kCapabilityChatReadStatus = @"chat-read-status"; NSString * const kCapabilityListableRooms = @"listable-rooms"; NSString * const kCapabilityDeleteMessages = @"delete-messages"; NSString * const kCapabilityCallFlags = @"conversation-call-flags"; NSString * const kCapabilityRoomDescription = @"room-description"; NSString * const kCapabilityTempUserAvatarAPI = @"temp-user-avatar-api"; NSString * const kCapabilityLocationSharing = @"geo-location-sharing"; NSString * const kCapabilityConversationV4 = @"conversation-v4"; NSString * const kCapabilitySIPSupport = @"sip-support"; NSString * const kCapabilityVoiceMessage = @"voice-message-sharing"; NSString * const kCapabilitySignalingV3 = @"signaling-v3"; NSString * const kCapabilityClearHistory = @"clear-history"; NSString * const kCapabilityDirectMentionFlag = @"direct-mention-flag"; NSString * const kCapabilityNotificationCalls = @"notification-calls"; NSString * const kCapabilityConversationPermissions = @"conversation-permissions"; NSString * const kCapabilityChatUnread = @"chat-unread"; NSString * const kCapabilityReactions = @"reactions"; NSString * const kCapabilityRichObjectDelete = @"rich-object-delete"; NSString * const kMinimumRequiredTalkCapability = kCapabilitySystemMessages; // Talk 4.0 is the minimum required version @implementation NCDatabaseManager + (NCDatabaseManager *)sharedInstance { static dispatch_once_t once; static NCDatabaseManager *sharedInstance; dispatch_once(&once, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } - (id)init { self = [super init]; if (self) { // Create Talk database directory NSString *path = [[[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:groupIdentifier] URLByAppendingPathComponent:kTalkDatabaseFolder] path]; if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil]; } [[NSFileManager defaultManager] setAttributes:@{NSFileProtectionKey:NSFileProtectionNone} ofItemAtPath:path error:nil]; // Set Realm configuration RLMRealmConfiguration *configuration = [RLMRealmConfiguration defaultConfiguration]; NSURL *databaseURL = [[NSURL fileURLWithPath:path] URLByAppendingPathComponent:kTalkDatabaseFileName]; configuration.fileURL = databaseURL; configuration.schemaVersion = kTalkDatabaseSchemaVersion; configuration.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) { // At the very minimum we need to update the version with an empty block to indicate that the schema has been upgraded (automatically) by Realm }; // Tell Realm to use this new configuration object for the default Realm [RLMRealmConfiguration setDefaultConfiguration:configuration]; // Now that we've told Realm how to handle the schema change, opening the file // will automatically perform the migration [RLMRealm defaultRealm]; #ifdef DEBUG // Copy Talk DB to Documents directory NSString *dbCopyPath = [NSString stringWithFormat:@"%@/%@", NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0], kTalkDatabaseFileName]; NSURL *dbCopyURL = [NSURL fileURLWithPath:dbCopyPath]; [[NSFileManager defaultManager] removeItemAtURL:dbCopyURL error:nil]; [[NSFileManager defaultManager] copyItemAtURL:databaseURL toURL:dbCopyURL error:nil]; #endif } return self; } #pragma mark - Talk accounts - (NSInteger)numberOfAccounts { return [TalkAccount allObjects].count; } - (TalkAccount *)activeAccount { TalkAccount *managedActiveAccount = [TalkAccount objectsWhere:(@"active = true")].firstObject; if (managedActiveAccount) { return [[TalkAccount alloc] initWithValue:managedActiveAccount]; } return nil; } - (NSArray *)allAccounts { NSMutableArray *allAccounts = [NSMutableArray new]; for (TalkAccount *managedAccount in [TalkAccount allObjects]) { TalkAccount *account = [[TalkAccount alloc] initWithValue:managedAccount]; [allAccounts addObject:account]; } return allAccounts; } - (NSArray *)inactiveAccounts { NSMutableArray *inactiveAccounts = [NSMutableArray new]; for (TalkAccount *managedInactiveAccount in [TalkAccount objectsWhere:(@"active = false")]) { TalkAccount *inactiveAccount = [[TalkAccount alloc] initWithValue:managedInactiveAccount]; [inactiveAccounts addObject:inactiveAccount]; } return inactiveAccounts; } - (TalkAccount *)talkAccountForAccountId:(NSString *)accountId { NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", accountId]; TalkAccount *managedAccount = [TalkAccount objectsWithPredicate:query].firstObject; if (managedAccount) { return [[TalkAccount alloc] initWithValue:managedAccount]; } return nil; } - (TalkAccount *)talkAccountForUserId:(NSString *)userId inServer:(NSString *)server { NSPredicate *query = [NSPredicate predicateWithFormat:@"userId = %@ AND server = %@", userId, server]; TalkAccount *managedAccount = [TalkAccount objectsWithPredicate:query].firstObject; if (managedAccount) { return [[TalkAccount alloc] initWithValue:managedAccount]; } return nil; } - (void)setActiveAccountWithAccountId:(NSString *)accountId { RLMRealm *realm = [RLMRealm defaultRealm]; [realm beginWriteTransaction]; for (TalkAccount *account in [TalkAccount allObjects]) { account.active = NO; } NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", accountId]; TalkAccount *activeAccount = [TalkAccount objectsWithPredicate:query].firstObject; activeAccount.active = YES; [realm commitWriteTransaction]; } - (NSString *)accountIdForUser:(NSString *)user inServer:(NSString *)server { return [NSString stringWithFormat:@"%@@%@", user, server]; } - (void)createAccountForUser:(NSString *)user inServer:(NSString *)server { TalkAccount *account = [[TalkAccount alloc] init]; NSString *accountId = [self accountIdForUser:user inServer:server]; account.accountId = accountId; account.server = server; account.user = user; RLMRealm *realm = [RLMRealm defaultRealm]; [realm transactionWithBlock:^{ [realm addObject:account]; }]; } - (void)removeAccountWithAccountId:(NSString *)accountId { RLMRealm *realm = [RLMRealm defaultRealm]; [realm beginWriteTransaction]; BOOL isLastAccount = [self numberOfAccounts] == 1; NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", accountId]; TalkAccount *removeAccount = [TalkAccount objectsWithPredicate:query].firstObject; if (removeAccount) { [realm deleteObject:removeAccount]; } ServerCapabilities *serverCapabilities = [ServerCapabilities objectsWithPredicate:query].firstObject; if (serverCapabilities) { [realm deleteObject:serverCapabilities]; } [realm deleteObjects:[NCRoom objectsWithPredicate:query]]; [realm deleteObjects:[NCChatMessage objectsWithPredicate:query]]; [realm deleteObjects:[NCChatBlock objectsWithPredicate:query]]; [realm deleteObjects:[NCContact objectsWithPredicate:query]]; if (isLastAccount) { [realm deleteObjects:[ABContact allObjects]]; } [realm commitWriteTransaction]; } - (void)increaseUnreadBadgeNumberForAccountId:(NSString *)accountId { RLMRealm *realm = [RLMRealm defaultRealm]; [realm beginWriteTransaction]; NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", accountId]; TalkAccount *account = [TalkAccount objectsWithPredicate:query].firstObject; account.unreadBadgeNumber += 1; account.unreadNotification = YES; [realm commitWriteTransaction]; } - (void)decreaseUnreadBadgeNumberForAccountId:(NSString *)accountId { RLMRealm *realm = [RLMRealm defaultRealm]; [realm beginWriteTransaction]; NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", accountId]; TalkAccount *account = [TalkAccount objectsWithPredicate:query].firstObject; account.unreadBadgeNumber = (account.unreadBadgeNumber > 0) ? account.unreadBadgeNumber - 1 : 0; account.unreadNotification = (account.unreadBadgeNumber > 0) ? account.unreadNotification : NO; [realm commitWriteTransaction]; } - (void)resetUnreadBadgeNumberForAccountId:(NSString *)accountId { RLMRealm *realm = [RLMRealm defaultRealm]; [realm beginWriteTransaction]; NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", accountId]; TalkAccount *account = [TalkAccount objectsWithPredicate:query].firstObject; account.unreadBadgeNumber = 0; account.unreadNotification = NO; [realm commitWriteTransaction]; } - (NSInteger)numberOfInactiveAccountsWithUnreadNotifications { return [TalkAccount objectsWhere:(@"active = false AND unreadNotification = true")].count; } - (NSInteger)numberOfUnreadNotifications { NSInteger unreadNotifications = 0; for (TalkAccount *account in [TalkAccount allObjects]) { unreadNotifications += account.unreadBadgeNumber; } return unreadNotifications; } - (void)removeUnreadNotificationForInactiveAccounts { RLMRealm *realm = [RLMRealm defaultRealm]; [realm beginWriteTransaction]; for (TalkAccount *account in [TalkAccount allObjects]) { account.unreadNotification = NO; } [realm commitWriteTransaction]; } #pragma mark - Server capabilities - (ServerCapabilities *)serverCapabilitiesForAccountId:(NSString *)accountId { NSPredicate *query = [NSPredicate predicateWithFormat:@"accountId = %@", accountId]; ServerCapabilities *managedServerCapabilities = [ServerCapabilities objectsWithPredicate:query].firstObject; if (managedServerCapabilities) { return [[ServerCapabilities alloc] initWithValue:managedServerCapabilities]; } return nil; } - (void)setServerCapabilities:(NSDictionary *)serverCapabilities forAccountId:(NSString *)accountId { NSDictionary *serverCaps = [serverCapabilities objectForKey:@"capabilities"]; NSDictionary *version = [serverCapabilities objectForKey:@"version"]; NSDictionary *themingCaps = [serverCaps objectForKey:@"theming"]; NSDictionary *talkCaps = [serverCaps objectForKey:@"spreed"]; NSDictionary *userStatusCaps = [serverCaps objectForKey:@"user_status"]; NSDictionary *provisioningAPICaps = [serverCaps objectForKey:@"provisioning_api"]; ServerCapabilities *capabilities = [[ServerCapabilities alloc] init]; capabilities.accountId = accountId; capabilities.name = [themingCaps objectForKey:@"name"]; capabilities.slogan = [themingCaps objectForKey:@"slogan"]; capabilities.url = [themingCaps objectForKey:@"url"]; capabilities.logo = [themingCaps objectForKey:@"logo"]; capabilities.color = [themingCaps objectForKey:@"color"]; capabilities.colorElement = [themingCaps objectForKey:@"color-element"]; capabilities.colorElementBright = [themingCaps objectForKey:@"color-element-bright"]; capabilities.colorElementDark = [themingCaps objectForKey:@"color-element-dark"]; capabilities.colorText = [themingCaps objectForKey:@"color-text"]; capabilities.background = [themingCaps objectForKey:@"background"]; capabilities.backgroundDefault = [[themingCaps objectForKey:@"background-default"] boolValue]; capabilities.backgroundPlain = [[themingCaps objectForKey:@"background-plain"] boolValue]; capabilities.version = [version objectForKey:@"string"]; capabilities.versionMajor = [[version objectForKey:@"major"] integerValue]; capabilities.versionMinor = [[version objectForKey:@"minor"] integerValue]; capabilities.versionMicro = [[version objectForKey:@"micro"] integerValue]; capabilities.edition = [version objectForKey:@"edition"]; capabilities.userStatus = [[userStatusCaps objectForKey:@"enabled"] boolValue]; capabilities.webDAVRoot = [[serverCaps objectForKey:@"core"] objectForKey:@"webdav-root"]; capabilities.extendedSupport = [[version objectForKey:@"extendedSupport"] boolValue]; capabilities.talkCapabilities = [talkCaps objectForKey:@"features"]; capabilities.chatMaxLength = [[[[talkCaps objectForKey:@"config"] objectForKey:@"chat"] objectForKey:@"max-length"] integerValue]; if ([[[[talkCaps objectForKey:@"config"] objectForKey:@"conversations"] allKeys] containsObject:@"can-create"]) { capabilities.canCreate = [[[[talkCaps objectForKey:@"config"] objectForKey:@"conversations"] objectForKey:@"can-create"] boolValue]; } else { capabilities.canCreate = YES; } capabilities.attachmentsAllowed = [[[[talkCaps objectForKey:@"config"] objectForKey:@"attachments"] objectForKey:@"allowed"] boolValue]; capabilities.attachmentsFolder = [[[talkCaps objectForKey:@"config"] objectForKey:@"attachments"] objectForKey:@"folder"]; capabilities.readStatusPrivacy = [[[[talkCaps objectForKey:@"config"] objectForKey:@"chat"] objectForKey:@"read-privacy"] boolValue]; capabilities.accountPropertyScopesVersion2 = [[provisioningAPICaps objectForKey:@"AccountPropertyScopesVersion"] integerValue] == 2; capabilities.accountPropertyScopesFederationEnabled = [[provisioningAPICaps objectForKey:@"AccountPropertyScopesFederationEnabled"] boolValue]; RLMRealm *realm = [RLMRealm defaultRealm]; [realm transactionWithBlock:^{ [realm addOrUpdateObject:capabilities]; }]; } - (BOOL)serverHasTalkCapability:(NSString *)capability { TalkAccount *activeAccount = [self activeAccount]; return [self serverHasTalkCapability:capability forAccountId:activeAccount.accountId]; } - (BOOL)serverHasTalkCapability:(NSString *)capability forAccountId:(NSString *)accountId { ServerCapabilities *serverCapabilities = [self serverCapabilitiesForAccountId:accountId]; if (serverCapabilities) { NSArray *talkFeatures = [serverCapabilities.talkCapabilities valueForKey:@"self"]; if ([talkFeatures containsObject:capability]) { return YES; } } return NO; } @end