// // MLPubSubProcessor.m // monalxmpp // // Created by Thilo Molitor on 31.10.20. // Copyright © 2020 Monal.im. All rights reserved. // #import #import "MLConstants.h" #import "MLPubSubProcessor.h" #import "MLPubSub.h" #import "MLHandler.h" #import "xmpp.h" #import "DataLayer.h" #import "MLImageManager.h" #import "MLNotificationQueue.h" #import "MLMucProcessor.h" #import "XMPPIQ.h" #import "HelperTools.h" @interface MLPubSubProcessor() @end @interface MLMucProcessor () -(void) sendDiscoQueryFor:(NSString*) roomJid withJoin:(BOOL) join andBookmarksUpdate:(BOOL) updateBookmarks; -(void) sendJoinPresenceFor:(NSString*) room; -(NSString*) calculateNickForMuc:(NSString*) room; @end @implementation MLPubSubProcessor $$class_handler(mdsHandler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary*), data)) DDLogDebug(@"Got new mds displayed status from '%@' (should be own jid)...", jid); if(![jid isEqualToString:account.connectionProperties.identity.jid]) { DDLogWarn(@"Ignoring mds update not coming from our own jid"); return; } if([type isEqualToString:@"publish"]) [account updateMdsData:data]; $$ $$class_handler(handleMdsFetchResult, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $_ID((NSDictionary*), data)) if(!success) { //item-not-found means: no mds items in storage --> use an empty data dict if([errorIq check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}item-not-found"]) data = @{}; else { DDLogWarn(@"Could not fetch mds from pep, doing nothing!"); return; } } //call +notify handler to process our data dictionary containing all mds items [account updateMdsData:data]; $$ $$class_handler(avatarHandler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary*), data)) DDLogDebug(@"Got new avatar metadata from '%@'", jid); if([type isEqualToString:@"publish"]) { for(NSString* entry in data) { MLXMLNode* metadata = [data[entry] findFirst:@"{urn:xmpp:avatar:metadata}metadata/info"]; NSString* avatarHash = [metadata findFirst:@"/@id"]; if(!avatarHash) //the user disabled his avatar { DDLogInfo(@"User '%@' disabled his avatar", jid); [[MLImageManager sharedInstance] setIconForContact:[MLContact createContactFromJid:jid andAccountID:account.accountID] WithData:nil]; [[DataLayer sharedInstance] setAvatarHash:@"" forContact:jid andAccount:account.accountID]; //delete cache to make sure the image will be regenerated [[MLImageManager sharedInstance] purgeCacheForContact:jid andAccount:account.accountID]; [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ @"contact": [MLContact createContactFromJid:jid andAccountID:account.accountID] }]; } else { NSString* currentHash = [[DataLayer sharedInstance] getAvatarHashForContact:jid andAccount:account.accountID]; if(currentHash && [avatarHash isEqualToString:currentHash]) { DDLogInfo(@"Avatar hash of '%@' is the same, we don't need to update our avatar image data", jid); break; } //only allow a maximum of 72KiB of image data when in appex due to appex memory limits //--> ignore metadata elements bigger than this size and only hande them once not in appex anymore NSUInteger avatarByteSize = [[metadata findFirst:@"/@bytes|int"] unsignedIntegerValue]; if(![HelperTools isAppExtension] || avatarByteSize < 128 * 1024) [account.pubsub fetchNode:@"urn:xmpp:avatar:data" from:jid withItemsList:@[avatarHash] andHandler:$newHandler(self, handleAvatarFetchResult, $ID(metadata))]; else { DDLogWarn(@"Not loading avatar image of '%@' because it is too big to be handled in appex (%lu bytes), rescheduling it to be fetched in mainapp", jid, (unsigned long)avatarByteSize); [account addReconnectionHandler:$newHandler(self, fetchAvatarAgain, $ID(jid), $ID(avatarHash), $ID(metadata))]; } } break; //we only want to process the first item (this should also be the only item) } if([data count] > 1) DDLogWarn(@"Got more than one avatar metadata item!"); } else { DDLogInfo(@"User %@ disabled his avatar", jid); [[MLImageManager sharedInstance] setIconForContact:[MLContact createContactFromJid:jid andAccountID:account.accountID] WithData:nil]; [[DataLayer sharedInstance] setAvatarHash:@"" forContact:jid andAccount:account.accountID]; //delete cache to make sure the image will be regenerated [[MLImageManager sharedInstance] purgeCacheForContact:jid andAccount:account.accountID]; [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ @"contact": [MLContact createContactFromJid:jid andAccountID:account.accountID] }]; } $$ //this handler will simply retry the fetchNode for urn:xmpp:avatar:data if in mainapp $$class_handler(fetchAvatarAgain, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, avatarHash), $$ID(MLXMLNode*, metadata)) if([HelperTools isAppExtension]) { DDLogWarn(@"Not loading avatar image of '%@' because we are still in appex, rescheduling it again!", jid); [account addReconnectionHandler:$newHandler(self, fetchAvatarAgain, $ID(jid), $ID(avatarHash), $ID(metadata))]; } else [account.pubsub fetchNode:@"urn:xmpp:avatar:data" from:jid withItemsList:@[avatarHash] andHandler:$newHandler(self, handleAvatarFetchResult, $ID(metadata))]; $$ $$class_handler(handleAvatarFetchResult, $$ID(xmpp*, account), $$ID(NSString*, jid), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(XMPPIQ*, errorReason), $_ID((NSDictionary*), data), $$ID(MLXMLNode*, metadata)) //ignore errors here (e.g. simply don't update the avatar image) //(this should never happen if other clients and servers behave properly) if(!success) { DDLogWarn(@"Got avatar image fetch error from jid %@: errorIq=%@, errorReason=%@", jid, errorIq, errorReason); return; } for(NSString* avatarHash in data) { //this should be small enough to not crash the appex when loading the image from file later on but large enough to have excellent quality NSData* avatarData = [data[avatarHash] findFirst:@"{urn:xmpp:avatar:data}data#|base64"]; UIImage* image = nil; if([[metadata findFirst:@"/@type"] hasPrefix:@"image/svg"]) image = (UIImage*)nilExtractor(PMKHang([HelperTools renderUIImageFromSVGData:avatarData])); else image = [UIImage imageWithData:avatarData]; if(image == nil) { DDLogWarn(@"Failed to load avatar of %@", jid); return; } //this upper limit is roughly 1.4MiB memory (600x600 with 4 byte per pixel) if(![HelperTools isAppExtension] || image.size.width * image.size.height < 600 * 600) { NSData* imageData = [HelperTools resizeAvatarImage:image withCircularMask:YES toMaxBase64Size:256000]; [[MLImageManager sharedInstance] setIconForContact:[MLContact createContactFromJid:jid andAccountID:account.accountID] WithData:imageData]; [[DataLayer sharedInstance] setAvatarHash:avatarHash forContact:jid andAccount:account.accountID]; //delete cache to make sure the image will be regenerated [[MLImageManager sharedInstance] purgeCacheForContact:jid andAccount:account.accountID]; [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ @"contact": [MLContact createContactFromJid:jid andAccountID:account.accountID] }]; DDLogInfo(@"Avatar of '%@' fetched and updated successfully", jid); } else { DDLogWarn(@"Not loading avatar image of '%@' because it is too big to be processed in appex (%lux%lu pixels), rescheduling it to be fetched in mainapp", jid, (unsigned long)image.size.width, (unsigned long)image.size.height); [account addReconnectionHandler:$newHandler(self, fetchAvatarAgain, $ID(jid), $ID(avatarHash), $ID(metadata))]; } } $$ $$class_handler(rosterNameHandler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary*), data)) //new/updated nickname if([type isEqualToString:@"publish"]) { for(NSString* itemId in data) { if([jid isEqualToString:account.connectionProperties.identity.jid]) //own roster name { DDLogInfo(@"Got own nickname: %@", [data[itemId] findFirst:@"{http://jabber.org/protocol/nick}nick#"]); NSMutableDictionary* accountDic = [[NSMutableDictionary alloc] initWithDictionary:[[DataLayer sharedInstance] detailsForAccount:account.accountID] copyItems:YES]; accountDic[kRosterName] = [data[itemId] findFirst:@"{http://jabber.org/protocol/nick}nick#"]; [[DataLayer sharedInstance] updateAccounWithDictionary:accountDic]; } else //roster name of contact { DDLogInfo(@"Got nickname of %@: %@", jid, [data[itemId] findFirst:@"{http://jabber.org/protocol/nick}nick#"]); [[DataLayer sharedInstance] setFullName:[data[itemId] findFirst:@"{http://jabber.org/protocol/nick}nick#"] forContact:jid andAccount:account.accountID]; MLContact* contact = [MLContact createContactFromJid:jid andAccountID:account.accountID]; if(contact) //ignore updates for jids not in our roster { //delete cache to make sure the image will be regenerated [[MLImageManager sharedInstance] purgeCacheForContact:jid andAccount:account.accountID]; [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ @"contact": contact }]; } } break; //we only need the first item (there should be only one item in the first place) } } //deleted/purged node or retracted item else { if([jid isEqualToString:account.connectionProperties.identity.jid]) //own roster name { DDLogInfo(@"Own nickname got retracted"); NSMutableDictionary* accountDic = [[NSMutableDictionary alloc] initWithDictionary:[[DataLayer sharedInstance] detailsForAccount:account.accountID] copyItems:NO]; accountDic[kRosterName] = @""; [[DataLayer sharedInstance] updateAccounWithDictionary:accountDic]; } else { DDLogInfo(@"Nickname of %@ got retracted", jid); [[DataLayer sharedInstance] setFullName:@"" forContact:jid andAccount:account.accountID]; MLContact* contact = [MLContact createContactFromJid:jid andAccountID:account.accountID]; if(contact) //ignore updates for jids not in our roster { //delete cache to make sure the image will be regenerated [[MLImageManager sharedInstance] purgeCacheForContact:jid andAccount:account.accountID]; [[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{ @"contact": contact }]; } } } $$ $$class_handler(bookmarks2Handler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary*), data)) if(!account.connectionProperties.supportsBookmarksCompat) { DDLogWarn(@"Ignoring new XEP-0402 bookmarks, server does not support syncing between XEP-0048 and XEP-0402!"); return; } //type will be "publish", "retract", "purge" or "delete". "publish" and "retract" will have the data dictionary filled with id --> data pairs //the data for "publish" is the item node with the given id, the data for "retract" is always @YES if(![jid isEqualToString:account.connectionProperties.identity.jid]) { DDLogWarn(@"Ignoring bookmarks update not coming from our own jid"); return; } NSSet* ownFavorites = [[DataLayer sharedInstance] listMucsForAccount:account.accountID]; //new/updated bookmarks if([type isEqualToString:@"publish"]) { //iterate through all conference elements provided for(NSString* itemId in data) { //we ignore the conference name (the name will be taken from the muc itself) //NSString* name = [data[itemId] findFirst:@"{urn:xmpp:bookmarks:1}conference@name"]; NSString* room = [itemId lowercaseString]; NSString* nick = [data[itemId] findFirst:@"{urn:xmpp:bookmarks:1}conference/nick#"]; //ignore password protected mucs if([data[itemId] check:@"{urn:xmpp:bookmarks:1}conference/password"]) continue; NSNumber* autojoin = [data[itemId] findFirst:@"{urn:xmpp:bookmarks:1}conference@autojoin|bool"]; if(autojoin == nil) autojoin = @NO; //default value specified in xep //check if this is a new entry with autojoin=true if(![ownFavorites containsObject:room] && [autojoin boolValue]) { DDLogInfo(@"Entering muc '%@' on account %@ because it got added to bookmarks...", room, account.accountID); //make sure we update our favorites table right away, to counter any race conditions when joining multiple mucs with one bookmarks update if(nick == nil) nick = [account.mucProcessor calculateNickForMuc:room]; //this will record the desired nickname: the mucProcessor will pick that up and use it to join the muc [[DataLayer sharedInstance] addMucFavorite:room forAccountID:account.accountID andMucNick:nick]; //try to join muc, but don't perform a bookmarks update (this muc came in through a bookmark already) [account.mucProcessor sendDiscoQueryFor:room withJoin:YES andBookmarksUpdate:NO]; } //check if it is a known entry that changed autojoin to false else if([ownFavorites containsObject:room] && ![autojoin boolValue]) { DDLogInfo(@"Leaving muc '%@' on account %@ because not listed as autojoin=true in bookmarks...", room, account.accountID); //delete local favorites entry and leave room afterwards, but keep buddylist entry because only the autojoin flag changed [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:YES]; } //check for nickname changes else if([ownFavorites containsObject:room] && nick != nil) { NSString* oldNick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID]; if(![nick isEqualToString:oldNick]) { DDLogInfo(@"Updating muc '%@' nick on account %@ in database to nick provided by bookmarks: '%@'...", room, account.accountID, nick); //update muc nickname in database [[DataLayer sharedInstance] updateOwnNickName:nick forMuc:room forAccount:account.accountID]; [[DataLayer sharedInstance] addMucFavorite:room forAccountID:account.accountID andMucNick:nick]; //this will upate the already existing favorites entry //rejoin the muc (e.g. change nick) //we don't have to do a full disco because we are sure this is a real muc and we are joined already //(only real mucs are part of our local favorites list and this list is joined automatically) [account.mucProcessor sendJoinPresenceFor:room]; } } } } else if([type isEqualToString:@"retract"]) { for(NSString* itemId in data) { NSString* room = [itemId lowercaseString]; if([ownFavorites containsObject:room]) { DDLogInfo(@"Leaving muc '%@' on account %@ because not listed in bookmarks anymore...", room, account.accountID); //delete local favorites entry and leave room afterwards [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:NO]; } else DDLogVerbose(@"Ignoring retracted bookmark because not listed in muc_favorites already..."); } } else { //deleted/purged node (e.g. all bookmarks deleted) //--> remove and leave all mucs for(NSString* room in ownFavorites) { DDLogInfo(@"Leaving muc '%@' on account %@ because all bookmarks got deleted...", room, account.accountID); //delete local favorites entry and leave room afterwards [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:NO]; } } $$ $$class_handler(handleBookmarks2FetchResult, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $_ID((NSDictionary*), data)) if(!account.connectionProperties.supportsBookmarksCompat) { DDLogWarn(@"Ignoring new XEP-0402 bookmarks, server does not support syncing between XEP-0048 and XEP-0402!"); return; } if(!success) { //item-not-found means: no bookmarks in storage --> use an empty data dict if([errorIq check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}item-not-found"]) data = @{}; else { DDLogWarn(@"Could not fetch bookmarks from pep prior to publishing!"); [self handleErrorWithDescription:NSLocalizedString(@"Failed to save groupchat bookmarks", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:YES]; return; } } NSString* max_items = @"255"; //fallback for servers not supporting "max" if(account.connectionProperties.supportsPubSubMax) max_items = @"max"; NSDictionary* infoDict = [[NSBundle mainBundle] infoDictionary]; NSSet* ownFavorites = [[DataLayer sharedInstance] listMucsForAccount:account.accountID]; DDLogVerbose(@"Own favorites: %@", ownFavorites); //filter passwort protected mucs and make sure jids (the item ids) are always lowercase NSMutableDictionary* _data = [NSMutableDictionary new]; for(NSString* itemId in data) { if([data[itemId] check:@"{urn:xmpp:bookmarks:1}conference/password"]) { DDLogVerbose(@"Not copying muc %@ to bookmark data: password protected", itemId); continue; } _data[[itemId lowercaseString]] = data[itemId]; } DDLogVerbose(@"Mucs listed in bookmarks2: %@", [_data allKeys]); //handle all changes of existing bookmarks for(NSString* room in _data) { MLXMLNode* item = _data[room]; //we ignore the conference name (the name will be taken from the muc itself) //NSString* name = [_data[room] findFirst:@"{urn:xmpp:bookmarks:1}conference@name"]; //NSString* nick = [_data[room] findFirst:@"{urn:xmpp:bookmarks:1}conference/nick#"]; NSNumber* autojoin = [item findFirst:@"{urn:xmpp:bookmarks:1}conference@autojoin|bool"]; if(autojoin == nil) autojoin = @NO; //default value specified in xep //check if the bookmark exists with autojoin==false and only update the autojoin and nick values, if true if([ownFavorites containsObject:room] && ![autojoin boolValue]) { DDLogInfo(@"Updating autojoin of bookmarked muc '%@' on account %@ to 'true'...", room, account.accountID); //add or update nickname NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID]; if(nick != nil) { if(![item check:@"{urn:xmpp:bookmarks:1}conference/nick"]) [[item findFirst:@"{urn:xmpp:bookmarks:1}conference"] addChildNode:[[MLXMLNode alloc] initWithElement:@"nick"]]; ((MLXMLNode*)[item findFirst:@"{urn:xmpp:bookmarks:1}conference/nick"]).data = nick; } //update autojoin value to true ((MLXMLNode*)[item findFirst:@"{urn:xmpp:bookmarks:1}conference"]).attributes[@"autojoin"] = @"true"; //publish this bookmark item again [account.pubsub publishItem:item onNode:@"urn:xmpp:bookmarks:1" withConfigOptions:@{ @"pubsub#persist_items": @"true", @"pubsub#access_model": @"whitelist", @"pubsub#max_items": max_items, } andHandler:$newHandler(self, bookmarks2Published, $ID(room))]; } } //add all mucs not yet listed in bookmarks NSMutableSet* toAdd = [ownFavorites mutableCopy]; [toAdd minusSet:[NSSet setWithArray:[_data allKeys]]]; for(NSString* room in toAdd) { DDLogInfo(@"Adding muc '%@' on account %@ to bookmarks...", room, account.accountID); NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID]; [account.pubsub publishItem: [[MLXMLNode alloc] initWithElement:@"item" withAttributes:@{@"id": room} andChildren:@[ [[MLXMLNode alloc] initWithElement:@"conference" andNamespace:@"urn:xmpp:bookmarks:1" withAttributes:@{ @"autojoin": @"true", } andChildren:@[ nilWrapper(nick != nil ? [[MLXMLNode alloc] initWithElement:@"nick" withAttributes:@{} andChildren:@[] andData:nick] : nil), [[MLXMLNode alloc] initWithElement:@"extensions" withAttributes:@{} andChildren:@[ [[MLXMLNode alloc] initWithElement:@"added-by" andNamespace:@"urn:monal.im:bookmarks:info" withAttributes:@{ @"name": @"Monal", @"version": infoDict[@"CFBundleShortVersionString"], @"build": infoDict[@"CFBundleVersion"], } andChildren:@[] andData:nil] ] andData:nil] ]andData:nil] ] andData:nil] onNode:@"urn:xmpp:bookmarks:1" withConfigOptions:@{ @"pubsub#persist_items": @"true", @"pubsub#access_model": @"whitelist", @"pubsub#max_items": max_items, } andHandler:$newHandler(self, bookmarks2Published, $ID(room))]; } //remove all mucs not listed in local favorites table NSMutableSet* toRemove = [NSMutableSet setWithArray:[_data allKeys]]; [toRemove minusSet:ownFavorites]; for(NSString* room in toRemove) { DDLogInfo(@"Removing muc '%@' on account %@ from bookmarks...", room, account.accountID); [account.pubsub retractItemWithId:room onNode:@"urn:xmpp:bookmarks:1" andHandler:$newHandler(self, bookmarks2Retracted, $ID(room))]; } $$ $$class_handler(bookmarks2Published, $$ID(xmpp*, account), $$ID(NSString*, room), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason)) if(!account.connectionProperties.supportsBookmarksCompat) { DDLogWarn(@"Ignoring new XEP-0402 bookmarks, server does not support syncing between XEP-0048 and XEP-0402!"); return; } if(!success) { DDLogWarn(@"Could not publish bookmark for muc '%@' to pep!", room); [self handleErrorWithDescription:[NSString stringWithFormat:NSLocalizedString(@"Failed to save bookmark for Group/Channel: %@", @""), room] andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:YES]; return; } DDLogDebug(@"Published bookmark for muc '%@' to pep", room); $$ $$class_handler(bookmarks2Retracted, $$ID(xmpp*, account), $$ID(NSString*, room), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason)) if(!account.connectionProperties.supportsBookmarksCompat) { DDLogWarn(@"Ignoring new XEP-0402 bookmarks, server does not support syncing between XEP-0048 and XEP-0402!"); return; } if(!success) { DDLogWarn(@"Could not retract bookmark for muc '%@' from pep!", room); [self handleErrorWithDescription:[NSString stringWithFormat:NSLocalizedString(@"Failed to remove bookmark for Group/Channel: %@", @""), room] andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:YES]; return; } DDLogDebug(@"Retracted bookmark for muc '%@' from pep", room); $$ $$class_handler(bookmarksHandler, $$ID(xmpp*, account), $$ID(NSString*, jid), $$ID(NSString*, type), $_ID((NSDictionary*), data)) if(account.connectionProperties.supportsBookmarksCompat) { DDLogInfo(@"Ignoring old XEP-0048 bookmarks, server supports syncing between XEP-0048 and XEP-0402..."); return; } if(![jid isEqualToString:account.connectionProperties.identity.jid]) { DDLogWarn(@"Ignoring bookmarks update not coming from our own jid"); return; } NSSet* ownFavorites = [[DataLayer sharedInstance] listMucsForAccount:account.accountID]; //new/updated bookmarks if([type isEqualToString:@"publish"]) { for(NSString* itemId in data) { //iterate through all conference elements provided NSMutableSet* bookmarkedMucs = [NSMutableSet new]; for(MLXMLNode* conference in [data[itemId] find:@"{storage:bookmarks}storage/conference"]) { //we ignore the conference name (the name will be taken from the muc itself) //NSString* name = [conference findFirst:@"/@name"]; NSString* room = [[conference findFirst:@"/@jid"] lowercaseString]; //ignore non-xep-compliant entries if(!room) { DDLogError(@"Received non-xep-compliant bookmarks entry, ignoring: %@", conference); continue; } //ignore password protected mucs if([conference check:@"password"]) continue; [bookmarkedMucs addObject:room]; NSString* nick = [conference findFirst:@"nick#"]; NSNumber* autojoin = [conference findFirst:@"/@autojoin|bool"]; if(autojoin == nil) autojoin = @NO; //default value specified in xep //check if this is a new entry with autojoin=true if(![ownFavorites containsObject:room] && [autojoin boolValue]) { DDLogInfo(@"Entering muc '%@' on account %@ because it got added to bookmarks...", room, account.accountID); //make sure we update our favorites table right away, to counter any race conditions when joining multiple mucs with one bookmarks update if(nick == nil) nick = [account.mucProcessor calculateNickForMuc:room]; //this will record the desired nickname: the mucProcessor will pick that up and use it to join the muc [[DataLayer sharedInstance] addMucFavorite:room forAccountID:account.accountID andMucNick:nick]; //try to join muc, but don't perform a bookmarks update (this muc came in through a bookmark already) [account.mucProcessor sendDiscoQueryFor:room withJoin:YES andBookmarksUpdate:NO]; } //check if it is a known entry that changed autojoin to false else if([ownFavorites containsObject:room] && ![autojoin boolValue]) { DDLogInfo(@"Leaving muc '%@' on account %@ because not listed as autojoin=true in bookmarks...", room, account.accountID); //delete local favorites entry and leave room afterwards, but keep buddylist entry because only the autojoin flag changed [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:YES]; } //check for nickname changes else if([ownFavorites containsObject:room] && nick != nil) { NSString* oldNick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID]; if(![nick isEqualToString:oldNick]) { DDLogInfo(@"Updating muc '%@' nick on account %@ in database to nick provided by bookmarks: '%@'...", room, account.accountID, nick); //update muc nickname in database [[DataLayer sharedInstance] updateOwnNickName:nick forMuc:room forAccount:account.accountID]; [[DataLayer sharedInstance] addMucFavorite:room forAccountID:account.accountID andMucNick:nick]; //this will upate the already existing favorites entry //rejoin the muc (e.g. change nick) //we don't have to do a full disco because we are sure this is a real muc and we are joined already //(only real mucs are part of our local favorites list and this list is joined automatically) [account.mucProcessor sendJoinPresenceFor:room]; } } } //remove and leave all mucs removed from bookmarks NSMutableSet* toLeave = [ownFavorites mutableCopy]; [toLeave minusSet:bookmarkedMucs]; for(NSString* room in toLeave) { DDLogInfo(@"Leaving muc '%@' on account %@ because not listed in bookmarks anymore...", room, account.accountID); //delete local favorites entry and leave room afterwards [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:NO]; } return; //we only need the first pep item (there should be only one item in the first place) } //FALLTHROUGH to "delete all" if no item was found } //deleted/purged node or retracted item (e.g. all bookmarks deleted) //--> remove and leave all mucs for(NSString* room in ownFavorites) { DDLogInfo(@"Leaving muc '%@' on account %@ because all bookmarks got deleted...", room, account.accountID); //delete local favorites entry and leave room afterwards [account.mucProcessor leave:room withBookmarksUpdate:NO keepBuddylistEntry:NO]; } $$ $$class_handler(handleBookarksFetchResult, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $_ID((NSDictionary*), data)) if(account.connectionProperties.supportsBookmarksCompat) { DDLogInfo(@"Ignoring old XEP-0048 bookmarks, server supports syncing between XEP-0048 and XEP-0402..."); return; } if(!success) { //item-not-found means: no bookmarks in storage --> use an empty data dict if([errorIq check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}item-not-found"]) data = @{}; else { DDLogWarn(@"Could not fetch bookmarks from pep prior to publishing!"); [self handleErrorWithDescription:NSLocalizedString(@"Failed to save groupchat bookmarks", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:YES]; return; } } BOOL changed = NO; NSSet* ownFavorites = [[DataLayer sharedInstance] listMucsForAccount:account.accountID]; for(NSString* itemId in data) { //ignore non-xep-compliant data and continue as if no data was received at all if(![data[itemId] check:@"{storage:bookmarks}storage"]) { DDLogError(@"Received non-xep-compliant bookmarks data: %@", data); break; } NSMutableSet* bookmarkedMucs = [NSMutableSet new]; for(MLXMLNode* conference in [data[itemId] find:@"{storage:bookmarks}storage/conference"]) { //we ignore the conference name (the name will be taken from the muc itself) //NSString* name = [conference findFirst:@"/@name"]; NSString* room = [[conference findFirst:@"/@jid"] lowercaseString]; //ignore non-xep-compliant entries if(!room) { DDLogError(@"Received non-xep-compliant bookmarks entry, ignoring: %@", conference); continue; } [bookmarkedMucs addObject:room]; NSNumber* autojoin = [conference findFirst:@"/@autojoin|bool"]; if(autojoin == nil) autojoin = @NO; //default value specified in xep //check if the bookmark exists with autojoin==false and only update the autojoin and nick values, if true if([ownFavorites containsObject:room] && ![autojoin boolValue]) { DDLogInfo(@"Updating autojoin of bookmarked muc '%@' on account %@ to 'true'...", room, account.accountID); //add or update nickname NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID]; if(nick != nil) { if(![conference check:@"nick"]) [conference addChildNode:[[MLXMLNode alloc] initWithElement:@"nick"]]; ((MLXMLNode*)[conference findFirst:@"nick"]).data = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID]; } //update autojoin value to true conference.attributes[@"autojoin"] = @"true"; changed = YES; } } //add all mucs not yet listed in bookmarks NSMutableSet* toAdd = [ownFavorites mutableCopy]; [toAdd minusSet:bookmarkedMucs]; for(NSString* room in toAdd) { DDLogInfo(@"Adding muc '%@' on account %@ to bookmarks...", room, account.accountID); NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID]; [[data[itemId] findFirst:@"{storage:bookmarks}storage"] addChildNode:[[MLXMLNode alloc] initWithElement:@"conference" withAttributes:@{ @"jid": room, @"name": [[MLContact createContactFromJid:room andAccountID:account.accountID] contactDisplayName], @"autojoin": @"true", } andChildren:(nick != nil ? @[[[MLXMLNode alloc] initWithElement:@"nick" withAttributes:@{} andChildren:@[] andData:nick]] : @[]) andData:nil]]; changed = YES; } //remove all mucs not listed in local favorites table NSMutableSet* toRemove = [bookmarkedMucs mutableCopy]; [toRemove minusSet:ownFavorites]; for(NSString* room in toRemove) { DDLogInfo(@"Removing muc '%@' on account %@ from bookmarks...", room, account.accountID); [[data[itemId] findFirst:@"{storage:bookmarks}storage"] removeChildNode:[data[itemId] findFirst:@"{storage:bookmarks}storage/conference", room]]; changed = YES; } //publish new bookmarks if something was changed if(changed) [account.pubsub publishItem:data[itemId] onNode:@"storage:bookmarks" withConfigOptions:@{ @"pubsub#persist_items": @"true", @"pubsub#access_model": @"whitelist" } andHandler:$newHandler(self, bookmarksPublished)]; //we only need the first pep item (there should be only one item in the first place) return; } //don't publish an empty bookmarks node if there is nothing to publish at all if([ownFavorites count] == 0) { DDLogInfo(@"neither a pep item was found, nor do we have any local muc favorites: don't publish anything"); return; } DDLogInfo(@"no pep item was found: publish our bookmarks the first time"); NSMutableArray* conferences = [NSMutableArray new]; for(NSString* room in ownFavorites) { DDLogInfo(@"Adding muc '%@' on account %@ to bookmarks...", room, account.accountID); NSString* nick = [[DataLayer sharedInstance] ownNickNameforMuc:room forAccount:account.accountID]; [conferences addObject:[[MLXMLNode alloc] initWithElement:@"conference" withAttributes:@{ @"jid": room, @"name": [[MLContact createContactFromJid:room andAccountID:account.accountID] contactDisplayName], @"autojoin": @"true", } andChildren:(nick != nil ? @[[[MLXMLNode alloc] initWithElement:@"nick" withAttributes:@{} andChildren:@[] andData:nick]] : @[]) andData:nil]]; } [account.pubsub publishItem: [[MLXMLNode alloc] initWithElement:@"item" withAttributes:@{@"id": @"current"} andChildren:@[ [[MLXMLNode alloc] initWithElement:@"storage" andNamespace:@"storage:bookmarks" withAttributes:@{} andChildren:conferences andData:nil] ] andData:nil] onNode:@"storage:bookmarks" withConfigOptions:@{ @"pubsub#persist_items": @"true", @"pubsub#access_model": @"whitelist" } andHandler:$newHandler(self, bookmarksPublished)]; $$ $$class_handler(bookmarksPublished, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason)) if(account.connectionProperties.supportsBookmarksCompat) { DDLogInfo(@"Ignoring old XEP-0048 bookmarks, server supports syncing between XEP-0048 and XEP-0402..."); return; } if(!success) { DDLogWarn(@"Could not publish bookmarks to pep!"); [self handleErrorWithDescription:NSLocalizedString(@"Failed to save groupchat bookmarks", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:YES]; return; } DDLogDebug(@"Published bookmarks to pep"); $$ $$class_handler(rosterNamePublished, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason)) if(!success) { DDLogWarn(@"Could not publish roster name to pep!"); [self handleErrorWithDescription:NSLocalizedString(@"Failed to publish own nickname", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:NO]; return; } DDLogDebug(@"Published roster name to pep"); $$ $$class_handler(rosterNameDeleted, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason)) if(!success) { //item-not-found means: nick already deleted --> ignore this error if([errorIq check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}item-not-found"]) { DDLogWarn(@"Roster name was already deleted from pep, ignoring error!"); return; } DDLogWarn(@"Could not remove roster name from pep!"); [self handleErrorWithDescription:NSLocalizedString(@"Failed to delete own nickname", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:NO]; return; } DDLogDebug(@"Removed roster name from pep"); $$ $$class_handler(avatarDeleted, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason)) if(!success) { //item-not-found means: avatar already deleted --> ignore this error if([errorIq check:@"error/{urn:ietf:params:xml:ns:xmpp-stanzas}item-not-found"]) { DDLogWarn(@"Avatar image was already deleted from pep, ignoring error!"); return; } DDLogWarn(@"Could not delete avatar image from pep!"); [self handleErrorWithDescription:NSLocalizedString(@"Failed to delete own avatar", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:NO]; return; } DDLogDebug(@"Removed avatar from pep"); $$ $$class_handler(avatarMetadataPublished, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason)) if(!success) { DDLogWarn(@"Could not publish avatar metadata to pep!"); [self handleErrorWithDescription:NSLocalizedString(@"Failed to publish own avatar", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:NO]; return; } DDLogDebug(@"Published avatar metadata to pep"); $$ $$class_handler(avatarDataPublished, $$ID(xmpp*, account), $$BOOL(success), $_ID(XMPPIQ*, errorIq), $_ID(NSString*, errorReason), $$ID(NSString*, imageHash), $$UINTEGER(imageBytesLen)) if(!success) { DDLogWarn(@"Could not publish avatar image data for hash %@!", imageHash); [self handleErrorWithDescription:NSLocalizedString(@"Failed to publish own avatar", @"") andAccount:account andErrorIq:errorIq andErrorReason:errorReason andIsSevere:NO]; return; } DDLogInfo(@"Avatar image data for hash %@ published successfully, now publishing metadata", imageHash); //publish metadata node (must be done *after* publishing the new data node) [account.pubsub publishItem: [[MLXMLNode alloc] initWithElement:@"item" withAttributes:@{@"id": imageHash} andChildren:@[ [[MLXMLNode alloc] initWithElement:@"metadata" andNamespace:@"urn:xmpp:avatar:metadata" withAttributes:@{} andChildren:@[ [[MLXMLNode alloc] initWithElement:@"info" withAttributes:@{ @"id": imageHash, @"type": @"image/jpeg", @"bytes": [NSString stringWithFormat:@"%lu", (unsigned long)imageBytesLen] } andChildren:@[] andData:nil] ] andData:nil] ] andData:nil] onNode:@"urn:xmpp:avatar:metadata" withConfigOptions:@{ @"pubsub#persist_items": @"true", @"pubsub#access_model": @"presence" } andHandler:$newHandler(self, avatarMetadataPublished)]; $$ +(void) handleErrorWithDescription:(NSString*) description andAccount:(xmpp*) account andErrorIq:(XMPPIQ*) errorIq andErrorReason:(NSString*) errorReason andIsSevere:(BOOL) isSevere { MLAssert(errorIq || errorReason, @"at least one of errorIq or errorReason must be set when calling error handler!"); if(errorIq) [HelperTools postError:description withNode:errorIq andAccount:account andIsSevere:isSevere]; else if(errorReason) [HelperTools postError:[NSString stringWithFormat:@"%@: %@", description, errorReason] withNode:nil andAccount:account andIsSevere:isSevere]; } @end