// // MBIMSendMessageOperation.m // kordophoned // // Created by James Magahern on 11/13/18. // Copyright © 2018 James Magahern. All rights reserved. // #import "MBIMSendMessageOperation.h" #import "IMCore_ClassDump.h" #import "IMMessageItem+Encoded.h" #import "MBIMErrorResponse.h" @implementation MBIMSendMessageOperation + (void)load { [super load]; } + (NSString *)endpointName { return @"sendMessage"; } - (nullable IMChat *)_existingSingleChatForHandle:(IMHandle *)handle registry:(IMChatRegistry *)registry { if ([registry respondsToSelector:@selector(existingChatWithHandle:allowAlternativeService:)]) { MBIMLogInfo(@"Using IMChatRegistry existingChatWithHandle:allowAlternativeService:"); return [registry existingChatWithHandle:handle allowAlternativeService:NO]; } if ([registry respondsToSelector:@selector(existingChatWithHandle:)]) { MBIMLogInfo(@"Using IMChatRegistry existingChatWithHandle:"); return [registry existingChatWithHandle:handle]; } if ([registry respondsToSelector:@selector(existingChatForIMHandle:allowRetargeting:)]) { MBIMLogInfo(@"Using IMChatRegistry existingChatForIMHandle:allowRetargeting:"); return [registry existingChatForIMHandle:handle allowRetargeting:NO]; } if ([registry respondsToSelector:@selector(existingChatForIMHandle:)]) { MBIMLogInfo(@"Using IMChatRegistry existingChatForIMHandle:"); return [registry existingChatForIMHandle:handle]; } MBIMLogError(@"IMChatRegistry does not support any known single-handle existing chat lookup selector."); return nil; } - (nullable IMChat *)_createSingleChatForHandle:(IMHandle *)handle registry:(IMChatRegistry *)registry { if ([registry respondsToSelector:@selector(chatWithHandle:)]) { MBIMLogInfo(@"Using IMChatRegistry chatWithHandle:"); return [registry chatWithHandle:handle]; } if ([registry respondsToSelector:@selector(chatForIMHandle:)]) { MBIMLogInfo(@"Using IMChatRegistry chatForIMHandle:"); return [registry chatForIMHandle:handle]; } MBIMLogError(@"IMChatRegistry does not support any known single-handle chat creation selector."); return nil; } - (nullable IMChat *)_existingGroupChatForHandles:(NSArray *)handles registry:(IMChatRegistry *)registry { if ([registry respondsToSelector:@selector(existingChatWithHandles:allowAlternativeService:groupID:displayName:joinedChatsOnly:)]) { MBIMLogInfo(@"Using IMChatRegistry existingChatWithHandles:allowAlternativeService:groupID:displayName:joinedChatsOnly:"); return [registry existingChatWithHandles:handles allowAlternativeService:NO groupID:nil displayName:nil joinedChatsOnly:YES]; } if ([registry respondsToSelector:@selector(existingChatWithHandles:allowAlternativeService:)]) { MBIMLogInfo(@"Using IMChatRegistry existingChatWithHandles:allowAlternativeService:"); return [registry existingChatWithHandles:handles allowAlternativeService:NO]; } if ([registry respondsToSelector:@selector(existingChatWithHandles:)]) { MBIMLogInfo(@"Using IMChatRegistry existingChatWithHandles:"); return [registry existingChatWithHandles:handles]; } if ([registry respondsToSelector:@selector(existingChatForIMHandles:allowRetargeting:groupID:displayName:joinedChatsOnly:)]) { MBIMLogInfo(@"Using IMChatRegistry existingChatForIMHandles:allowRetargeting:groupID:displayName:joinedChatsOnly:"); return [registry existingChatForIMHandles:handles allowRetargeting:NO groupID:nil displayName:nil joinedChatsOnly:YES]; } if ([registry respondsToSelector:@selector(existingChatForIMHandles:allowRetargeting:)]) { MBIMLogInfo(@"Using IMChatRegistry existingChatForIMHandles:allowRetargeting:"); return [registry existingChatForIMHandles:handles allowRetargeting:NO]; } if ([registry respondsToSelector:@selector(existingChatForIMHandles:)]) { MBIMLogInfo(@"Using IMChatRegistry existingChatForIMHandles:"); return [registry existingChatForIMHandles:handles]; } MBIMLogError(@"IMChatRegistry does not support any known multi-handle existing chat lookup selector."); return nil; } - (nullable IMChat *)_createGroupChatForHandles:(NSArray *)handles registry:(IMChatRegistry *)registry { if ([registry respondsToSelector:@selector(chatWithHandles:displayName:joinedChatsOnly:)]) { MBIMLogInfo(@"Using IMChatRegistry chatWithHandles:displayName:joinedChatsOnly:"); return [registry chatWithHandles:handles displayName:nil joinedChatsOnly:YES]; } if ([registry respondsToSelector:@selector(chatWithHandles:)]) { MBIMLogInfo(@"Using IMChatRegistry chatWithHandles:"); return [registry chatWithHandles:handles]; } if ([registry respondsToSelector:@selector(chatForIMHandles:displayName:joinedChatsOnly:)]) { MBIMLogInfo(@"Using IMChatRegistry chatForIMHandles:displayName:joinedChatsOnly:"); return [registry chatForIMHandles:handles displayName:nil joinedChatsOnly:YES]; } if ([registry respondsToSelector:@selector(chatForIMHandles:)]) { MBIMLogInfo(@"Using IMChatRegistry chatForIMHandles:"); return [registry chatForIMHandles:handles]; } MBIMLogError(@"IMChatRegistry does not support any known multi-handle chat creation selector."); return nil; } - (nullable IMChat *)_chatForHandleIDs:(NSArray *)handleIDs registry:(IMChatRegistry *)registry { MBIMLogInfo(@"Resolving send target for handles: %@", handleIDs); IMAccount *iMessageAccount = [[IMAccountController sharedInstance] bestAccountForService:[IMServiceImpl iMessageService]]; if (!iMessageAccount) { MBIMLogError(@"Unable to find an iMessage account for message send."); return nil; } NSMutableArray *handles = [NSMutableArray arrayWithCapacity:[handleIDs count]]; for (NSString *handleID in handleIDs) { IMHandle *handle = [iMessageAccount imHandleWithID:handleID]; if (!handle) { MBIMLogError(@"Couldn't resolve IMHandle for id %@", handleID); return nil; } [handles addObject:handle]; } if ([handles count] == 1) { IMHandle *handle = [handles firstObject]; IMChat *chat = [self _existingSingleChatForHandle:handle registry:registry]; if (!chat) { chat = [self _createSingleChatForHandle:handle registry:registry]; } if (chat) { MBIMLogInfo(@"Resolved send target %@ to chat %@", [handle ID], [chat guid] ?: @""); } else { MBIMLogError(@"Unable to locate or create chat for handle %@", [handle ID]); } return chat; } IMChat *chat = [self _existingGroupChatForHandles:handles registry:registry]; if (!chat) { chat = [self _createGroupChatForHandles:handles registry:registry]; } if (chat) { MBIMLogInfo(@"Resolved handles %@ to chat %@", handleIDs, [chat guid] ?: @""); } else { MBIMLogError(@"Unable to locate or create chat for handles %@", handleIDs); } return chat; } - (nullable NSDictionary *)_sendMessage:(NSString *)messageBody toChat:(IMChat *)chat attachmentGUIDs:(NSArray *)guids includeConversationGUID:(BOOL)includeConversationGUID { if (!chat) { return nil; } NSString *chatGUID = [chat guid]; if (!chatGUID) { chatGUID = [[[IMChatRegistry sharedInstance] allGUIDsForChat:chat] firstObject]; } MBIMLogInfo(@"Preparing sendMessage for chat %@ (bodyLength=%lu attachmentCount=%lu)", chatGUID ?: @"", (unsigned long)[messageBody length], (unsigned long)[guids count]); IMAccount *sendingAccount = [chat account]; if (!sendingAccount) { sendingAccount = [[IMAccountController sharedInstance] bestAccountForService:[IMServiceImpl iMessageService]]; } IMHandle *senderHandle = [sendingAccount loginIMHandle]; if (!senderHandle) { MBIMLogError(@"Unable to determine sender handle for message send."); return nil; } NSAttributedString *replyAttrString = [[NSAttributedString alloc] initWithString:messageBody]; NSAttributedString *attrStringWithFileTransfers = IMCreateSuperFormatStringWithAppendedFileTransfers(replyAttrString, guids); IMMessage *reply = [IMMessage fromMeIMHandle:senderHandle withText:attrStringWithFileTransfers fileTransferGUIDs:guids flags:(kIMMessageFinished | kIMMessageIsFromMe)]; for (NSString *guid in [reply fileTransferGUIDs]) { [[IMFileTransferCenter sharedInstance] assignTransfer:guid toMessage:reply account:sendingAccount]; } NSDictionary *replyRepresentation = [reply mbim_dictionaryRepresentation]; if (![replyRepresentation isKindOfClass:[NSDictionary class]]) { MBIMLogError(@"Unable to encode sent message for chat %@", chatGUID ?: @""); return nil; } NSMutableDictionary *result = [replyRepresentation mutableCopy]; if (includeConversationGUID) { NSString *conversationGUID = chatGUID; if (!conversationGUID) { conversationGUID = [[[IMChatRegistry sharedInstance] allGUIDsForChat:chat] firstObject]; } if (conversationGUID) { result[@"conversationGUID"] = conversationGUID; } } MBIMLogInfo(@"Dispatching IMCore send for chat %@", chatGUID ?: @""); dispatch_async(dispatch_get_main_queue(), ^{ [chat sendMessage:reply]; }); return result; } #if 0 - (NSDictionary *)adjustMessageSummaryInfoForSending:(NSDictionary *)messageSummaryInfo { NSMutableDictionary *adjustedInfo = [messageSummaryInfo mutableCopy]; if (!adjustedInfo) { adjustedInfo = [NSMutableDictionary dictionary]; } if ([fullText length] > 50) { summary = [[summary substringToIndex:[summary rangeOfComposedCharacterSequenceAtIndex:kMaxSummaryLength].location] stringByAppendingString:@"…"]; adjustedInfo[IMMessageSummaryInfoSummary] = summary; } adjustedInfo[IMMessageSummaryInfoTapbackRepresentationKey] = @"Loved"; return adjustedInfo; } #endif - (void)main { __block NSObject *response = [[HTTPErrorResponse alloc] initWithErrorCode:500]; NSError *error = nil; NSDictionary *args = [NSJSONSerialization JSONObjectWithData:self.requestBodyData options:0 error:&error]; if (error || args.count == 0) { MBIMLogError(@"Unable to parse sendMessage request body: %@", error); self.serverCompletionBlock(response); return; } NSString *guid = [args objectForKey:@"guid"]; NSString *messageBody = [args objectForKey:@"body"]; NSArray *rawHandleIDs = [args objectForKey:@"handleIDs"]; BOOL hasGUID = [guid isKindOfClass:[NSString class]] && [guid length] > 0; BOOL hasHandleIDs = [rawHandleIDs isKindOfClass:[NSArray class]] && [rawHandleIDs count] > 0; if (![messageBody isKindOfClass:[NSString class]] || (!hasGUID && !hasHandleIDs) || (hasGUID && hasHandleIDs)) { response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"sendMessage requires body and exactly one of guid or handleIDs."]; self.serverCompletionBlock(response); return; } NSMutableArray *handleIDs = [NSMutableArray array]; if (hasHandleIDs) { for (id handleID in rawHandleIDs) { if ([handleID isKindOfClass:[NSString class]] && [handleID length] > 0) { [handleIDs addObject:handleID]; } } handleIDs = [[[NSOrderedSet orderedSetWithArray:handleIDs] array] mutableCopy]; if ([handleIDs count] == 0) { response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"No valid handle IDs provided."]; self.serverCompletionBlock(response); return; } } NSArray *rawTransferGUIDs = [args objectForKey:@"fileTransferGUIDs"]; NSMutableArray *transferGUIDs = [NSMutableArray array]; if ([rawTransferGUIDs isKindOfClass:[NSArray class]]) { for (id transferGUID in rawTransferGUIDs) { if ([transferGUID isKindOfClass:[NSString class]] && [transferGUID length] > 0) { [transferGUIDs addObject:transferGUID]; } } } MBIMLogInfo(@"sendMessage request received. guid=%@ handleIDs=%@ bodyLength=%lu attachmentGUIDs=%@", hasGUID ? guid : @"", handleIDs, (unsigned long)[messageBody length], transferGUIDs); @try { dispatch_sync([[self class] sharedIMAccessQueue], ^{ IMChatRegistry *registry = [IMChatRegistry sharedInstance]; IMChat *chat = nil; BOOL includeConversationGUID = NO; if (hasGUID) { MBIMLogInfo(@"sendMessage targeting existing conversation %@", guid); chat = [registry existingChatWithGUID:guid]; if (!chat) { MBIMLogError(@"Chat does not exist for guid %@", guid); response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"Chat does not exist for the provided guid."]; return; } } else { MBIMLogInfo(@"sendMessage targeting handles %@", handleIDs); chat = [self _chatForHandleIDs:handleIDs registry:registry]; includeConversationGUID = YES; if (!chat) { response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"Unable to create or locate a chat for the provided handles."]; return; } } NSString *resolvedChatGUID = [chat guid]; if (!resolvedChatGUID) { resolvedChatGUID = [[[IMChatRegistry sharedInstance] allGUIDsForChat:chat] firstObject]; } MBIMLogInfo(@"sendMessage resolved target chat %@", resolvedChatGUID ?: @""); NSDictionary *result = [self _sendMessage:messageBody toChat:chat attachmentGUIDs:transferGUIDs includeConversationGUID:includeConversationGUID]; if (!result) { MBIMLogError(@"sendMessage failed before a response payload could be encoded for chat %@", resolvedChatGUID ?: @""); response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"Unable to construct sent message response."]; return; } NSObject *jsonResponse = [MBIMJSONDataResponse responseWithJSONObject:result]; if (jsonResponse) { response = jsonResponse; } else { MBIMLogError(@"Unable to encode sendMessage JSON response for chat %@", resolvedChatGUID ?: @""); response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"Unable to encode sendMessage response."]; } }); } @catch (NSException *exception) { MBIMLogError(@"Unhandled exception during sendMessage. name=%@ reason=%@ userInfo=%@", exception.name, exception.reason, exception.userInfo); response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"Unhandled exception while sending message. Check server logs."]; } if (response == nil) { MBIMLogError(@"sendMessage completed without producing a response. guid=%@ handleIDs=%@", hasGUID ? guid : @"", handleIDs); response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"sendMessage did not produce a response. Check server logs."]; } self.serverCompletionBlock(response); } @end