diff --git a/.gitignore b/.gitignore
index b777fcf..6a93e4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -160,4 +160,6 @@ cython_debug/
 #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
 #  and can be added to the global gitignore or merged into this file.  For a more nuclear
 #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
\ No newline at end of file
+.idea/
+
+attachments/
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..423611c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,33 @@
+# pypush
+`pypush` is a POC demo of my recent iMessage reverse-engineering.
+It can currently register as a new device on an Apple ID, set up encryption keys, and ***send and receive iMessages***!
+
+`pypush` is completely platform-independent, and does not require a Mac or other Apple device to use!
+
+## Installation
+It's pretty self explanatory:
+1. `git clone https://github.com/JJTech0130/pypush`
+2. `pip3 install -r requirements.txt`
+3. `python3 ./demo.py`
+
+## Troubleshooting
+If you have any issues, please join [the Discord](https://discord.gg/BVvNukmfTC) and ask for help.
+
+## Operation
+`pypush` will generate a `config.json` in the repository when you run demo.py. DO NOT SHARE THIS FILE.
+It contains all the encryption keys necessary to log into you Apple ID and send iMessages as you.
+
+Once it loads, it should prompt you with `>>`. Type `help` and press enter for a list of supported commands.
+
+## Special Notes
+### Unicorn dependency
+`pypush` currently uses the Unicorn CPU emulator and a custom MachO loader to load a framework from an old version of macOS,
+in order to call some obfuscated functions.
+
+This is only necessary during initial registration, so theoretically you can register on one device, and then copy the `config.json`
+to another device that doesn't support the Unicorn emulator. Or you could switch out the emulator for another x86 emulator if you really wanted to.
+
+### Public key caching
+iMessage will cache public keys. If you get decryption errors in pypush or can only send and not receive messages from another device,
+try logging out and back into iMessage on that device, forcing it to refresh it's key cache. Alternatively, you can wait and the cache should
+expire eventually.
diff --git a/demo.py b/demo.py
index d650ba6..b9793d0 100644
--- a/demo.py
+++ b/demo.py
@@ -127,11 +127,33 @@ threading.Thread(target=input_thread, daemon=True).start()
 
 print("Type 'help' for help")  
 
+def fixup_handle(handle):
+    if handle.startswith('tel:+'):
+        return handle
+    elif handle.startswith('mailto:'):
+        return handle
+    elif handle.startswith('tel:'):
+        return 'tel:+' + handle[4:]
+    elif handle.startswith('+'):
+        return 'tel:' + handle
+    # If the handle starts with a number
+    elif handle[0].isdigit():
+        # If the handle is 10 digits, assume it's a US number
+        if len(handle) == 10:
+            return 'tel:+1' + handle
+        # If the handle is 11 digits, assume it's a US number with country code
+        elif len(handle) == 11:
+            return 'tel:+' + handle
+    else: # Assume it's an email
+        return 'mailto:' + handle
+
 current_participants = []
+current_effect = None
 while True:
     msg = im.receive()
     if msg is not None:
-        print(f'[{msg.sender}] {msg.text}')
+        # print(f'[{msg.sender}] {msg.text}')
+        print(msg.to_string())
 
         attachments = msg.attachments()
         if len(attachments) > 0:
@@ -142,7 +164,7 @@ while True:
                 with open(attachments_path + attachment.name, "wb") as attachment_file:
                     attachment_file.write(attachment.versions[0].data())
 
-            print(f"({len(attachments)} attachment{'s' if len(attachments) == 1 else ''} have been downloaded and put "
+            print(f"({len(attachments)} attachment{'s have' if len(attachments) != 1 else ' has'} been downloaded and put "
                   f"in {attachments_path})")
     
     if len(INPUT_QUEUE) > 0:
@@ -153,28 +175,60 @@ while True:
             print('quit (q): quit')
             #print('send (s) [recipient] [message]: send a message')
             print('filter (f) [recipient]: set the current chat')
+            print('effect (e): adds an iMessage effect to the next sent message')
             print('note: recipient must start with tel: or mailto: and include the country code')
+            print('handle <handle>: set the current handle (for sending messages)')
             print('\\: escape commands (will be removed from message)')
         elif msg == 'quit' or msg == 'q':
             break
-        elif msg.startswith('filter ') or msg.startswith('f '):
+        elif msg == 'effect' or msg == 'e' or msg.startswith("effect ") or msg.startswith("e "):
+            msg = msg.split(" ")
+            if len(msg) < 2 or msg[1] == "":
+                print("effect [effect namespace]")
+            else:
+                print(f"next message will be sent with [{msg[1]}]")
+                current_effect = msg[1]
+        elif msg == 'filter' or msg == 'f' or msg.startswith('filter ') or msg.startswith('f '):
             # Set the curernt chat
             msg = msg.split(' ')
             if len(msg) < 2 or msg[1] == '':
                 print('filter [recipients]')
             else:
-                print(f'Filtering to {msg[1:]}')
-                current_participants = msg[1:]
+                print(f'Filtering to {[fixup_handle(h) for h in msg[1:]]}')
+                current_participants = [fixup_handle(h) for h in msg[1:]]
+        elif msg == 'handle' or msg.startswith('handle '):
+            msg = msg.split(' ')
+            if len(msg) < 2 or msg[1] == '':
+                print('handle [handle]')
+                print('Available handles:')
+                for h in user.handles:
+                    if h == user.current_handle:
+                        print(f'\t{h} (current)')
+                    else:
+                        print(f'\t{h}')
+            else:
+                h = msg[1]
+                h = fixup_handle(h)
+                if h in user.handles:
+                    print(f'Using {h} as handle')
+                    user.current_handle = h
+                else:
+                    print(f'Handle {h} not found')
+
         elif current_participants != []:
             if msg.startswith('\\'):
                 msg = msg[1:]
             im.send(imessage.iMessage(
                 text=msg,
                 participants=current_participants,
-                sender=user.handles[0]
+                sender=user.current_handle,
+                effect=current_effect
             ))
+            current_effect = None
         else:
             print('No chat selected, use help for help')
+
+    time.sleep(0.1)
         
         # elif msg.startswith('send') or msg.startswith('s'):
         #     msg = msg.split(' ')
diff --git a/ids/__init__.py b/ids/__init__.py
index 5d7226b..7e561c4 100644
--- a/ids/__init__.py
+++ b/ids/__init__.py
@@ -45,6 +45,8 @@ class IDSUser:
             self._auth_keypair,
             self._push_keypair,
         )
+        self.current_handle = self.handles[0]
+
 
     # Uses an existing authentication keypair
     def restore_authentication(
@@ -53,6 +55,7 @@ class IDSUser:
         self._auth_keypair = auth_keypair
         self.user_id = user_id
         self.handles = handles 
+        self.current_handle = self.handles[0]
 
     # This is a separate call so that the user can make sure the first part succeeds before asking for validation data
     def register(self, validation_data: str):
@@ -79,5 +82,5 @@ class IDSUser:
         self._id_keypair = id_keypair
 
     def lookup(self, uris: list[str], topic: str = "com.apple.madrid") -> any:
-        return query.lookup(self.push_connection, self.handles[0], self._id_keypair, uris, topic)
+        return query.lookup(self.push_connection, self.current_handle, self._id_keypair, uris, topic)
         
diff --git a/imessage.py b/imessage.py
index 1922693..6722f44 100644
--- a/imessage.py
+++ b/imessage.py
@@ -57,9 +57,6 @@ class MMCSFile(AttachmentFile):
             url=self.url,
             headers={
                 "User-Agent": f"IMTransferAgent/900 CFNetwork/596.2.3 Darwin/12.2.0 (x86_64) (Macmini5,1)",
-                "x-apple-mmcs-proto-version": f"basic {base64.encodebytes(f'benjiebabioles@outlook.com:!Qwerty4!'.encode('utf-8'))}",
-                "x-apple-mmcs-dataclass": f"basic {base64.encodebytes(f'benjiebabioles@outlook.com:!Qwerty4!'.encode('utf-8'))}",
-                # "Authorization": f"basic {base64.encodebytes(f'benjiebabioles@outlook.com:!Qwerty4!'.encode('utf-8'))}",
                 # "MMCS-Url": self.url,
                 # "MMCS-Signature": str(base64.encodebytes(self.signature)),
                 # "MMCS-Owner": self.owner
@@ -146,6 +143,8 @@ class iMessage:
     """Group ID of the message, will be randomly generated if not provided"""
     body: BalloonBody | None = None
     """BalloonBody, may be None"""
+    effect: str | None = None
+    """iMessage effect sent with this message, may be None"""
 
     _compressed: bool = True
     """Internal property representing whether the message should be compressed"""
@@ -184,7 +183,7 @@ class iMessage:
 
         return True
 
-    def from_raw(message: bytes) -> "iMessage":
+    def from_raw(message: bytes, sender: str | None = None) -> "iMessage":
         """Create an `iMessage` from raw message bytes"""
         compressed = False
         try:
@@ -199,12 +198,11 @@ class iMessage:
             text=message.get("t", ""),
             xml=message.get("x"),
             participants=message.get("p", []),
-            sender=message.get("p", [])[-1] if message.get("p", []) != [] else None,
+            sender=sender if sender is not None else message.get("p", [])[-1] if "p" in message else None,
             id=uuid.UUID(message.get("r")) if "r" in message else None,
             group_id=uuid.UUID(message.get("gid")) if "gid" in message else None,
-            body=BalloonBody(message["bid"], message["b"])
-            if "bid" in message and "b" in message
-            else None,
+            body=BalloonBody(message["bid"], message["b"]) if "bid" in message and "b" in message else None,
+            effect=message["iid"] if "iid" in message else None,
             _compressed=compressed,
             _raw=message,
         )
@@ -223,6 +221,7 @@ class iMessage:
             "pv": 0,
             "gv": "8",
             "v": "1",
+            "iid": self.effect
         }
 
         # Remove keys that are None
@@ -237,6 +236,12 @@ class iMessage:
 
         return d
 
+    def to_string(self) -> str:
+        message_str = f"[{self.sender}] '{self.text}'"
+        if self.effect is not None:
+            message_str += f" with effect [{self.effect}]"
+        return message_str
+
 
 class iMessageUser:
     """Represents a logged in and connected iMessage user.
@@ -432,14 +437,21 @@ class iMessageUser:
         
         decrypted = self._decrypt_payload(payload)
         
-        return iMessage.from_raw(decrypted)
+        return iMessage.from_raw(decrypted, body['sP'])
 
+    KEY_CACHE_HANDLE: str = ""
     KEY_CACHE: dict[bytes, tuple[bytes, bytes]] = {}
     """Mapping of push token : (public key, session token)"""
     USER_CACHE: dict[str, list[bytes]] = {}
     """Mapping of handle : [push tokens]"""
 
     def _cache_keys(self, participants: list[str]):
+        # Clear the cache if the handle has changed
+        if self.KEY_CACHE_HANDLE != self.user.current_handle:
+            self.KEY_CACHE_HANDLE = self.user.current_handle
+            self.KEY_CACHE = {}
+            self.USER_CACHE = {}
+        
         # Check to see if we have cached the keys for all of the participants
         if all([p in self.USER_CACHE for p in participants]):
             return