I Built My Own iMessage Wrapped (And So Can You)

My daughter was joking that someone should build a Spotify Wrapped, but for iMessage — your year in emoji and reactions. My wife said my brother-in-law should build it since he works at Apple. (Every Apple problem is his fault in our house.)

From the other room, my daughter yelled: “I bet Dad could write this!”

Challenge accepted.

The Result

Thirty minutes later, I had a working script. Here’s my 2025:

🎁 iMessage Wrapped 2025

📱 Messages
   Total:    35,768
   Sent:     12,191
   Received: 23,577

💬 Reactions
   Given:    1,974
   Received: 5,014

🏆 Your Reaction Style
   👍  982
   ❤️  398
   😂  275
   ‼️  126
   ❓  19
   👎  16

📈 Messages by Month
   Jan  ███████████████ 3,052
   Feb  █████████████ 2,621
   Mar  █████████████████ 3,422
   Apr  ██████████████████ 3,696
   May  ██████████████████ 3,640
   Jun  ████████████████████ 3,904
   Jul  █████████████ 2,561
   Aug  ████████████ 2,448
   Sep  ██████████████ 2,790
   Oct  █████████████████ 3,466
   Nov  ███████████ 2,206
   Dec  ██████████ 1,962

Some things I learned about myself:

  • I’m a listener. I receive almost 2x as many messages as I send.
  • People love reacting to me. 5,014 reactions received vs 1,974 given.
  • I’m a thumbs-up guy. 👍 accounts for half my reactions. Apparently I’m very agreeable.
  • June was my chattiest month. December was quietest (holiday break mode).

How It Works

Your iMessage history lives in a SQLite database at ~/Library/Messages/chat.db. It’s just sitting there, queryable.

The tricky bits:

  1. Dates are weird. Apple stores timestamps as nanoseconds since January 1, 2001 (because of course they do).
  2. Reactions are messages. When you tapback a ❤️, it’s stored as a separate message with associated_message_type set to a magic number (2000 = loved, 2001 = liked, etc.).
  3. Custom emoji reactions landed in iOS 17 and are stored in associated_message_emoji.

Once you know the schema, it’s just SQL.

The Script

Here’s the full thing — about 100 lines of Python, no dependencies beyond the standard library:

#!/usr/bin/env python3
"""
iMessage Wrapped — Your year in emoji and reactions
Usage: python3 imessage-wrapped.py [year]
Requires: Full Disk Access for Terminal
"""

import sqlite3
import os
import sys
from datetime import datetime
from pathlib import Path

YEAR = int(sys.argv[1]) if len(sys.argv) > 1 else 2025
DB_PATH = Path.home() / "Library/Messages/chat.db"
APPLE_EPOCH_OFFSET = 978307200

TAPBACKS = {
    2000: "❤️",   # Loved
    2001: "👍",   # Liked
    2002: "👎",   # Disliked
    2003: "😂",   # Laughed
    2004: "‼️",   # Emphasized
    2005: "❓",   # Questioned
}

def get_db():
    if not DB_PATH.exists():
        print(f"❌ Database not found at {DB_PATH}")
        sys.exit(1)
    return sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)

def date_filter(year):
    return f"""
        datetime(date/1000000000 + {APPLE_EPOCH_OFFSET}, 'unixepoch') >= '{year}-01-01' 
        AND datetime(date/1000000000 + {APPLE_EPOCH_OFFSET}, 'unixepoch') < '{year + 1}-01-01'
    """

def main():
    print(f"\n🎁 iMessage Wrapped {YEAR}\n")
    db = get_db()
    cur = db.cursor()
    
    # Message counts
    cur.execute(f"""
        SELECT COUNT(*), 
               SUM(CASE WHEN is_from_me = 1 THEN 1 ELSE 0 END),
               SUM(CASE WHEN is_from_me = 0 THEN 1 ELSE 0 END)
        FROM message WHERE {date_filter(YEAR)} AND associated_message_type = 0
    """)
    total, sent, received = cur.fetchone()
    print(f"📱 Messages: {total:,} ({sent:,} sent, {received:,} received)")
    
    # Reaction counts
    cur.execute(f"""
        SELECT SUM(CASE WHEN is_from_me = 1 THEN 1 ELSE 0 END),
               SUM(CASE WHEN is_from_me = 0 THEN 1 ELSE 0 END)
        FROM message WHERE {date_filter(YEAR)} AND associated_message_type >= 2000
    """)
    given, got = cur.fetchone()
    print(f"💬 Reactions: {given + got:,} ({given:,} given, {got:,} received)")
    
    # Your tapback style
    print(f"\n🏆 Your Reaction Style")
    cur.execute(f"""
        SELECT associated_message_type, COUNT(*) FROM message 
        WHERE {date_filter(YEAR)} AND associated_message_type BETWEEN 2000 AND 2005 AND is_from_me = 1
        GROUP BY associated_message_type ORDER BY COUNT(*) DESC
    """)
    for type_id, cnt in cur.fetchall():
        print(f"   {TAPBACKS.get(type_id, '?')}  {cnt:,}")
    
    # Custom emoji
    cur.execute(f"""
        SELECT associated_message_emoji, COUNT(*) FROM message 
        WHERE {date_filter(YEAR)} AND associated_message_emoji IS NOT NULL AND is_from_me = 1
        GROUP BY associated_message_emoji ORDER BY COUNT(*) DESC LIMIT 5
    """)
    customs = cur.fetchall()
    if customs:
        print(f"\n🎯 Custom Reactions: {', '.join(f'{e} ({c})' for e, c in customs)}")
    
    # Monthly volume
    print(f"\n📈 By Month")
    cur.execute(f"""
        SELECT strftime('%m', datetime(date/1000000000 + {APPLE_EPOCH_OFFSET}, 'unixepoch')), COUNT(*)
        FROM message WHERE {date_filter(YEAR)} AND associated_message_type = 0
        GROUP BY 1 ORDER BY 1
    """)
    months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
    data = {row[0]: row[1] for row in cur.fetchall()}
    max_cnt = max(data.values()) if data else 1
    for i, name in enumerate(months, 1):
        cnt = data.get(f"{i:02d}", 0)
        bar = "█" * int(20 * cnt / max_cnt)
        print(f"   {name} {bar} {cnt:,}")
    
    db.close()

if __name__ == "__main__":
    main()

Running It

  1. Save the script as imessage-wrapped.py
  2. Grant Full Disk Access to your terminal (System Settings → Privacy & Security → Full Disk Access)
  3. Run it:
python3 imessage-wrapped.py        # defaults to 2025
python3 imessage-wrapped.py 2024   # or any year

That’s it. Your data never leaves your machine.

What I’d Add Next

If I turn this into a proper app:

  • Shareable cards — export your stats as an image
  • Conversation breakdown — who do you text the most?
  • Time of day patterns — are you a morning texter or a midnight scroller?
  • Streak tracking — longest daily conversation streak

But honestly? The script is fun enough. Sometimes a quick hack that makes your daughter laugh is the whole point.


The script and a slightly more polished version are on GitHub if you want to grab it.

Fediverse reactions

Comments

4 responses to “I Built My Own iMessage Wrapped (And So Can You)”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.