Dev:IMCore.framework

From The Apple Wiki

IMCore is a framework that helps to manage handling SMS, iMessage, and MMS along with ChatKit.framework. IMCore exists on MacOS (X) as well as iOS, unlike ChatKit (which only exists on iOS).

Connecting to IMDaemon

For any process that tries to use classes or functions from IMCore, imagent (the iMessages Daemon on iOS) checks permissions to verify that the process is allowed to access what it's trying to access. To bypass this and allow your app or tweak to access what it needs to, just do the following:

%hook IMDaemonController

- (unsigned)_capabilities {
	return 17159;
}

%end

You can also conditionally check for the process to only allow your process access to IMCore. For example, if you'd like to only allow SpringBoard to access IMCore, then you can do the following:

%hook IMDaemonController

- (unsigned)_capabilities {
	NSString *process = [[NSProcessInfo processInfo] processName];
	if ([process isEqualToString:@"SpringBoard"])
		return 17159;
	else
		return %orig;
}

%end

However, even after you've hijacked the capabilities to always return full permissions, you must still sometimes connect to the IMDaemon to run your code. There are probably multiple methods to do this, but the following has worked perfectly for me:

/// Get the sharedController
IMDaemonController* controller = [%c(IMDaemonController) sharedController];

/// Attempt to connect directly to the daemon
if ([controller connectToDaemon]) {
	/// Send the code that you want it to run, basically
	/// e.g. send a text, send a reaction, create a new conversation, etc
} else {
	/// If it failed to connect to the daemon for whatever reason
	NSLog(@"Couldn't connect to daemon :(");
}

Within the "connectToDaemon" block above is where you'll run your IMCore-exclusive code. Unless stated otherwise, just assume that the rest of the code on this page is being run inside that block.

Sending a Text

On the ChatKit.framework page, there's information about how to send a text with attachments. However, if you'd prefer not to use ChatKit, you can send a text exclusively with IMCore. Theoretically, you can also send attachments with IMCore as well, but I have yet to figure that out. Here's the code:

__NSCFString *address = (__NSCFString *)@"+11231231234"; /// Must have the full phone number. just "1231234" wont work.
NSAtttributedString* text = [[NSAttributedString alloc] initWithString:@"Hello friend"];
IMChatRegistry* registry = [%c(IMChatRegistry) sharedInstance];
IMChat* chat = [registry existingChatWithChatIdentifier:address];

if (chat == nil) { /// If you havent yet texted them; this 'if' creates the conversation for you.
	/// Get your own account; must use it to register their conversation in your phone
	IMAccountController *sharedAccountController = [%c(IMAccountController) sharedInstance];
	IMAccount *myAccount = [sharedAccountController mostLoggedInAccount];

	/// Create their handle
	IMHandle *handle = [[%c(IMHandle) alloc] initWithAccount:myAccount ID:address alreadyCanonical:YES];

	/// Use the handle to get the IMChat
	chat = [registry chatForIMHandle:handle];
}

IMMessage *message;

/// iOS 14 requires the 'threadIdentifier' parameter, iOS13- doesn't support it.
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 14.0)
	message = [%c(IMMessage) instantMessageWithText:text flags:1048581 threadIdentifier:nil];
else
	message = [%c(IMMessage) instantMessageWithText:text flags:1048581];

/// Send the message :)
[chat sendMessage:message];

There are multiple functions in "IMMessage" that start with "instantMessageWithText" and have different parameters, so you can call whichever specific one fits best for your needs.

Sending a Tapback

Sending a tapback requires a significant bit of code, including the chat_identifier of the conversation in which you're sending the tapback, the guid of the text which you are reacting to, and the tapback type that you are sending. The tapback type is a long long between 2000 - 2005, inclusive. A 'Love' is 2000, 'Thumbs Up' is 2001, 'Thumbs Down' is 2002, 'Ha Ha' is 2003, 'Exclamation' is 2004, and 'Question' is 2005. To remove a tapback of a certain type, you'll just need to add 1000 to the tapback value. For example, if you want to remove a 'Ha ha', you'll just run the code below, but with the tapback value being 3003 instead of 2003.

Basically every function below is liable to unexpectedly return nil, in my testing, so I'd recommend adding a few retries here and there where needed. The code below is just the bare minimum, and will work only if everything goes perfectly (which it probably won't).

NSString *address = @"+11231231234";
NSString *guid = @"12345678-1234-1234-1234-123456789012";
long long int tapback = 2000;

IMChat *chat = [[%c(IMChatRegistry) sharedInstance] existingChatWithChatIdentifier:address];

// loadMessagesUpToGUID is necessary to populate the [IMChat chatItems] array.
// If you don't call it, [chat messageItemForGUID:] will always return nil.
[chat loadMessagesUpToGUID:guid date:nil limit:nil loadImmediately:YES];
IMMessageItem *msg = [chat messageForGUID:guid];
IMMessage *item = [msg _imMessageItem];

// You need an IMTextMessagePartChatItem to pass in to actually send the tapback
IMTextMessagePartChatItem *pci = [[%c(IMTextMessagePartChatItem) alloc] _initWithItem:item text:[item body] index:0 messagePartRange:NSMakeRange(0, [[item body] length]) subject:[item subject]];
NSDictionary *info = @{@"amc": @1, @"ams": [[item body] string]};

// the method to actually send the tapback changed in iOS 14, so we need to check and call the right one.
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 14.0)
	[chat sendMessageAcknowledgment:tapback forChatItem:pci withAssociatedMessageInfo:info];
else
	[chat sendMessageAcknowledgment:tapback forChatItem:pci withMessageSummaryInfo:info];

Typing Indicators

To sense when someone is typing or recently stopped typing, you'll just need to hook the following two functions:

%hook IMMessageItem

- (bool)isCancelTypingMessage {
	bool orig = %orig;
	
	if (orig) {
		// if orig is true here, someone stopped typing.
		// do whatever you'd like to do in this block
	}

	return orig;
}

- (bool)isIncomingTypingMessage {
	bool orig = %orig;

	if (orig) {
		// if orig is true here, someone started typing
		// do whatever you'd like to do in this block
	}
	
	return orig;
}

%end

To send a typing indicator is actually fairly simple (as opposed to detecting them). All you'll need is the address of the conversation for which you want to send a typing indicator (e.g. the full phone number or email address). If you wanted to send a typing indicator for the conversation with the phone number "+11231231234", this is the code you'd run:

/// Get the chat for the address
IMChat *chat = [[%c(IMChatRegistry) sharedInstance] existingChatWithChatIdentifier:(__NSCFString *)@"+11231231234"];

/// Change "YES" to "NO" if you want to set yourself as not typing
[convo setLocalUserIsTyping:YES];

Getting Pinned Chats

This is only available in iOS 14+ and MacOS 10.16 (11.0)+. However, this snippet doesn't work on MacOS. The [pinnedController pinnedConversationIdentifierSet] method returns an NSOrderedList of pinning identifiers, not addresses. We have to use ChatKit to parse those pinning identifiers to get us the actual addresses of the conversations to which those pinning identifiers correspond.

/// Pinned chats are only available for iOS 14+, so check that first
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 14.0) {
	IMPinnedConversationsController* pinnedController = [%c(IMPinnedConversationsController) sharedInstance];
	NSOrderedSet* set = [pinnedController pinnedConversationIdentifierSet];

	CKConversationList* list = [%c(CKConversationList) sharedConversationList];
	NSMutableArray* conversations = [NSMutableArray arrayWithCapacity:[set count];

	for (id obj in set) {
		CKConversation* convo = (CKConversation *)[list conversationForExistingChatWithPinningIdentifier:obj];
		if (convo == nil) continue; // just in case
		NSString* identifier = [[convo chat] identifier];
		[conversations addObject:identifier];

	}

	return convos; 
}

/// If it's iOS 13-, just return an empty array.
return [NSArray array];

Setting a conversation as read

Once again, very straightforward; you just need the address of the conversation that you want to set as read.

/// Get the conversation
IMChat* imchat = [[%c(IMChatRegistry) sharedInstance] existingChatWithChatIdentifier:(__NSCFString *)@"+11231231234"];
/// mark it as read!
[imchat markAllMessagesAsRead];