IMCore.framework: Difference between revisions

From iPhone Development Wiki
(→‎Getting Pinned Chats: Fixed code snippet that gets pinned chats)
(→‎Typing Indicators: Added better method to sense when someone has started/stopped typing.)
Line 84: Line 84:


== Typing Indicators ==
== Typing Indicators ==
There is probably a better method to detect when another party starts typing than what I've found, but I've yet to find it yet. This article will be updated once I find something that works better or is more dependable. For the following code to work, you must also have the MobileSMS app running (at least in the background). Here's what you need to hook:
To sense when someone is typing or recently stopped typing, you'll just need to hook the following two functions:


<source lang=objc>
<source lang=objc>
%hook IMTypingChatItem
%hook IMMessageItem


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


NSString *chat = [(IMMessageItem *)arg1 sender];
return orig;
/// Do whatever you want with the chat, which contains the phone number or email address of the person who's typing.
}


- (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;
return orig;
}
}

Revision as of 22:02, 1 February 2021

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.

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];