#!/bin/bash # Love Replay - iMessage Export Script # Usage: /bin/bash -c "$(curl -fsSL https://lovereplay.app/export.sh)" set -e # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' NC='\033[0m' # No Color BOLD='\033[1m' # Emoji support HEART="💕" CHECK="✅" WARN="⚠️" ROCKET="🚀" PHONE="📱" TEMP_EXPORT_DIR="" GUM_AVAILABLE=0 cleanup_temp_export_dir() { if [[ -n "$TEMP_EXPORT_DIR" ]] && [[ -d "$TEMP_EXPORT_DIR" ]]; then rm -rf "$TEMP_EXPORT_DIR" fi } lookup_one_to_one_msg_count_for_contact() { local contact_id="$1" local contact_id_sql="" if [[ -z "$contact_id" ]] || [[ -z "$CHAT_DB" ]] || [[ ! -r "$CHAT_DB" ]]; then return 0 fi contact_id_sql=$(printf "%s" "$contact_id" | sed "s/'/''/g") sqlite3 "$CHAT_DB" " WITH one_to_one_chats AS ( SELECT chj.chat_id, MIN(chj.handle_id) AS handle_id FROM chat_handle_join chj GROUP BY chj.chat_id HAVING COUNT(DISTINCT chj.handle_id) = 1 ) SELECT COUNT(m.rowid) AS msg_count FROM one_to_one_chats o JOIN handle h ON h.rowid = o.handle_id JOIN chat_message_join cmj ON cmj.chat_id = o.chat_id JOIN message m ON m.rowid = cmj.message_id WHERE h.id = '$contact_id_sql' OR COALESCE(NULLIF(h.uncanonicalized_id, ''), h.id) = '$contact_id_sql' GROUP BY o.chat_id ORDER BY msg_count DESC, MAX(m.date) DESC LIMIT 1; " 2>/dev/null | head -n1 } # Validate that an exported TXT file represents a one-to-one conversation. # Returns success (0) only when exactly one non-owner participant is detected # and no group-announcement lines are present. is_one_to_one_export_file() { local file_path="$1" awk ' BEGIN { expect_sender = 0 in_tapbacks = 0 partner_count = 0 has_group_markers = 0 } function canonicalize(raw, out, digits) { out = raw gsub(/^[[:space:]]+|[[:space:]]+$/, "", out) out = tolower(out) if (out == "" || out == "me" || out == "you") { return "" } # Keep stable canonical email identity when present. if (match(out, /[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z][a-z]+/)) { return substr(out, RSTART, RLENGTH) } # Canonicalize phone identifiers by last 10 digits. digits = out gsub(/[^0-9]/, "", digits) if (length(digits) >= 7) { if (length(digits) > 10) { digits = substr(digits, length(digits) - 9) } return "phone:" digits } # Last resort canonical form for display names. gsub(/[^a-z0-9]/, "", out) return out } function record_participant(raw, key) { key = canonicalize(raw) if (key == "") { return } if (!(key in participants)) { participants[key] = 1 partner_count += 1 } } # Timestamp lines from imessage-exporter TXT: # "May 17, 2022 5:29:42 PM" $0 ~ /^[A-Z][a-z][a-z]+ [0-9][0-9]*, [0-9][0-9][0-9][0-9][[:space:]][[:space:]]*[0-9][0-9]*:[0-9][0-9]:[0-9][0-9] [AP]M/ { if ($0 ~ /( added .* to the conversation\.| removed .* from the conversation\.| left the conversation\.| renamed the conversation to | changed the group photo\.| removed the group photo\.| changed the chat background\.| removed the chat background\.)/) { has_group_markers = 1 } expect_sender = 1 in_tapbacks = 0 next } expect_sender == 1 { record_participant($0) expect_sender = 0 next } $0 ~ /^(Tapbacks:|Reactions:)$/ { in_tapbacks = 1 next } in_tapbacks == 1 { if ($0 ~ / by /) { split($0, parts, / by /) record_participant(parts[length(parts)]) next } if ($0 == "") { in_tapbacks = 0 } next } END { if (has_group_markers || partner_count != 1) { exit 1 } }' "$file_path" } trap cleanup_temp_export_dir EXIT echo "" echo -e "${MAGENTA}${BOLD}════════════════════════════════════════${NC}" echo -e "${MAGENTA}${BOLD} ${HEART} Love Replay - iMessage Export ${HEART}${NC}" echo -e "${MAGENTA}${BOLD}════════════════════════════════════════${NC}" echo "" # Check if running on macOS if [[ "$(uname)" != "Darwin" ]]; then echo -e "${RED}${WARN} This script only works on macOS.${NC}" echo "" echo "iMessage data is stored locally on your Mac's hard drive." echo "You'll need to run this script on the Mac where your" echo "iMessages are synced." echo "" echo "If you use iCloud Messages, run this on any Mac signed" echo "into that iCloud account." exit 1 fi # Check macOS version (need 10.15+ for modern imessage-exporter) MACOS_VERSION=$(sw_vers -productVersion 2>/dev/null || echo "0") MAJOR_VERSION=$(echo "$MACOS_VERSION" | cut -d. -f1) if [[ "$MAJOR_VERSION" -lt 10 ]]; then echo -e "${RED}${WARN} Could not detect macOS version.${NC}" echo "This script requires macOS 10.15 (Catalina) or later." exit 1 fi echo -e "${GREEN}${CHECK} Running on macOS $MACOS_VERSION${NC}" echo "" # Check if Messages app has been used if [[ ! -d "$HOME/Library/Messages" ]]; then echo -e "${RED}${WARN} No Messages folder found.${NC}" echo "" echo "It looks like you haven't used the Messages app on this Mac." echo "Make sure iMessages are synced to this computer first." exit 1 fi # Check/install Homebrew if ! command -v brew &> /dev/null; then echo -e "${YELLOW}${WARN} Homebrew not found.${NC}" echo "" echo "Homebrew is a trusted package manager used by millions of developers." echo "It's required to install the iMessage export tool." echo "Learn more: https://brew.sh" echo "" read -p "Install Homebrew? (y/n) " -n 1 -r echo "" if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo "Cannot continue without Homebrew. Exiting." exit 1 fi echo "Installing Homebrew..." /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # Add brew to path for Apple Silicon Macs if [[ -f "/opt/homebrew/bin/brew" ]]; then eval "$(/opt/homebrew/bin/brew shellenv)" echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> "$HOME/.zprofile" fi # Intel Macs if [[ -f "/usr/local/bin/brew" ]]; then eval "$(/usr/local/bin/brew shellenv)" fi echo "" # Verify installation if ! command -v brew &> /dev/null; then echo -e "${RED}${WARN} Homebrew installation may have failed.${NC}" echo "Please restart your Terminal and run this script again." exit 1 fi fi echo -e "${GREEN}${CHECK} Homebrew installed${NC}" # Check/install imessage-exporter if ! command -v imessage-exporter &> /dev/null; then echo -e "${YELLOW}Installing imessage-exporter...${NC}" brew install imessage-exporter echo "" fi echo -e "${GREEN}${CHECK} imessage-exporter installed${NC}" echo "" # Check/install gum for prettier interactive prompts (arrow-key selection). if ! command -v gum &> /dev/null; then echo -e "${YELLOW}Installing gum for interactive prompts...${NC}" brew install gum >/dev/null 2>&1 || true fi if command -v gum &> /dev/null && [[ -t 0 ]] && [[ -t 1 ]]; then GUM_AVAILABLE=1 else echo -e "${YELLOW}${WARN} Pretty terminal UI unavailable; using classic prompts.${NC}" fi echo "" # Check Full Disk Access only when needed CHAT_DB="$HOME/Library/Messages/chat.db" if [[ -r "$CHAT_DB" ]]; then echo -e "${GREEN}${CHECK} Can access iMessage database${NC}" echo "" else echo -e "${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${CYAN}${BOLD} IMPORTANT: Full Disk Access Required${NC}" echo -e "${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e "${BOLD}Why do we need this?${NC}" echo "Your iMessages are stored in a protected database on your Mac." echo "This permission lets the export tool read that database." echo "" echo -e "${GREEN}${BOLD}🔒 PRIVACY PROMISE:${NC}" echo "┌────────────────────────────────────────────────────────────┐" echo "│ • This runs 100% locally on YOUR Mac │" echo "│ • We NEVER store your messages on our servers │" echo "│ • Messages are processed once, then immediately deleted │" echo "│ • We use Zero Data Retention (ZDR) with our AI provider │" echo "│ • You can verify: this script is open source │" echo "└────────────────────────────────────────────────────────────┘" echo "" # Detect which terminal app is being used TERMINAL_APP="Terminal" if [[ "$TERM_PROGRAM" == "iTerm.app" ]]; then TERMINAL_APP="iTerm" elif [[ "$TERM_PROGRAM" == "WarpTerminal" ]]; then TERMINAL_APP="Warp" elif [[ "$TERM_PROGRAM" == "Apple_Terminal" ]]; then TERMINAL_APP="Terminal" elif [[ -n "$TERM_PROGRAM" ]]; then TERMINAL_APP="$TERM_PROGRAM" fi echo -e "${BOLD}Steps to grant access:${NC}" echo " 1. System Settings will open automatically" echo " 2. Go to: Privacy & Security → Full Disk Access" echo " 3. Click the + button (you may need to unlock first 🔓)" if [[ "$TERMINAL_APP" == "Terminal" ]]; then echo " 4. Navigate to: Applications → Utilities → Terminal" elif [[ "$TERMINAL_APP" == "iTerm" ]]; then echo " 4. Navigate to: Applications → iTerm" elif [[ "$TERMINAL_APP" == "Warp" ]]; then echo " 4. Navigate to: Applications → Warp" else echo " 4. Find and add your terminal app: ${BOLD}$TERMINAL_APP${NC}" fi echo " 5. Add it to the list (toggle ON)" echo "" echo -e "${YELLOW}Opening System Settings now...${NC}" # Try to open the right settings pane (works on different macOS versions) if ! open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles" 2>/dev/null; then open "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles" 2>/dev/null || \ open "/System/Library/PreferencePanes/Security.prefPane" 2>/dev/null || \ echo "Please open System Settings → Privacy & Security → Full Disk Access manually" fi echo "" read -p "Press ENTER after adding $TERMINAL_APP to Full Disk Access to test access..." echo "" if [[ ! -r "$CHAT_DB" ]]; then echo -e "${RED}${WARN} Cannot read iMessage database yet.${NC}" echo "On some Macs, this permission only applies after restarting your terminal app." echo "Please quit $TERMINAL_APP (Cmd+Q), reopen it, and run this script again." echo "" echo "Database location: $CHAT_DB" exit 1 fi echo -e "${GREEN}${CHECK} Can access iMessage database${NC}" echo "" fi # Create export directory EXPORT_DIR="$HOME/Desktop/LoveReplay" mkdir -p "$EXPORT_DIR" # List conversations and let user pick SELECTED_CHAT_MSG_COUNT="" echo -e "${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${CYAN}${BOLD} ${PHONE} Your Conversations${NC}" echo -e "${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo "Fetching your conversations..." echo "" # Get list of one-to-one conversations with message counts. # We only include chats that have exactly one non-owner participant handle. CONVERSATIONS=$(sqlite3 "$CHAT_DB" " WITH one_to_one_chats AS ( SELECT chj.chat_id, MIN(chj.handle_id) AS handle_id FROM chat_handle_join chj GROUP BY chj.chat_id HAVING COUNT(DISTINCT chj.handle_id) = 1 ) SELECT o.chat_id, h.id, COALESCE(NULLIF(h.uncanonicalized_id, ''), h.id) AS display_id, COUNT(m.rowid) AS msg_count FROM one_to_one_chats o JOIN handle h ON h.rowid = o.handle_id JOIN chat_message_join cmj ON cmj.chat_id = o.chat_id JOIN message m ON m.rowid = cmj.message_id GROUP BY o.chat_id, h.id, display_id HAVING msg_count > 100 ORDER BY msg_count DESC, MAX(m.date) DESC LIMIT 30; " 2>/dev/null || echo "") if [[ -z "$CONVERSATIONS" ]]; then echo -e "${YELLOW}Couldn't find any one-to-one conversations directly from chat.db.${NC}" echo "" echo "Falling back to imessage-exporter contact listing." echo "Pick a single person (not a group) to export." echo "" imessage-exporter --list-contacts 2>/dev/null | head -30 || true echo "" echo -e "${BOLD}Enter the phone number or email for a one-to-one chat:${NC}" echo "(Format: +15551234567 or email@example.com)" if [[ "$GUM_AVAILABLE" -eq 1 ]]; then CONTACT_ID=$(gum input --prompt "> " --placeholder "+15551234567 or email@example.com" || true) if [[ -z "$CONTACT_ID" ]]; then echo "Cancelled." exit 0 fi else read -p "> " CONTACT_ID fi SELECTED_CHAT_MSG_COUNT=$(lookup_one_to_one_msg_count_for_contact "$CONTACT_ID") else echo -e "${BOLD}Top one-to-one conversations by message count:${NC}" echo "" # Parse and display declare -a CONTACT_IDS declare -a DISPLAY_IDS declare -a CHAT_IDS declare -a MSG_COUNTS declare -a OPTION_LABELS MAX_DISPLAY_LEN=0 MAX_MSG_COUNT_LEN=0 while IFS='|' read -r chat_id contact_id display_id msg_count; do CONTACT_IDS+=("$contact_id") DISPLAY_IDS+=("$display_id") CHAT_IDS+=("$chat_id") MSG_COUNTS+=("$msg_count") if [[ "${#display_id}" -gt "$MAX_DISPLAY_LEN" ]]; then MAX_DISPLAY_LEN="${#display_id}" fi if [[ "${#msg_count}" -gt "$MAX_MSG_COUNT_LEN" ]]; then MAX_MSG_COUNT_LEN="${#msg_count}" fi done <<< "$CONVERSATIONS" DISPLAY_COL_WIDTH="$MAX_DISPLAY_LEN" if [[ "$DISPLAY_COL_WIDTH" -lt 12 ]]; then DISPLAY_COL_WIDTH=12 fi if [[ "$MAX_MSG_COUNT_LEN" -lt 1 ]]; then MAX_MSG_COUNT_LEN=1 fi for idx in "${!CONTACT_IDS[@]}"; do padded_display=$(printf "%-${DISPLAY_COL_WIDTH}s" "${DISPLAY_IDS[$idx]}") padded_msg_count=$(printf "%${MAX_MSG_COUNT_LEN}s" "${MSG_COUNTS[$idx]}") OPTION_LABELS+=("$padded_display • $padded_msg_count messages • chat ${CHAT_IDS[$idx]}") done MANUAL_OPTION="Enter phone/email manually" OPTION_LABELS+=("$MANUAL_OPTION") CONTACT_ID="" if [[ "$GUM_AVAILABLE" -eq 1 ]]; then CHOICE=$(printf '%s\n' "${OPTION_LABELS[@]}" | gum choose \ --header "Select one one-to-one conversation (↑/↓ then Enter)" \ --cursor "❯ " \ --height 15 || true) if [[ -z "$CHOICE" ]]; then echo "Cancelled." exit 0 fi if [[ "$CHOICE" == "$MANUAL_OPTION" ]]; then CONTACT_ID=$(gum input --prompt "> " --placeholder "+15551234567 or email@example.com" || true) if [[ -z "$CONTACT_ID" ]]; then echo "Cancelled." exit 0 fi SELECTED_CHAT_MSG_COUNT=$(lookup_one_to_one_msg_count_for_contact "$CONTACT_ID") else for idx in "${!CONTACT_IDS[@]}"; do if [[ "$CHOICE" == "${OPTION_LABELS[$idx]}" ]]; then CONTACT_ID="${CONTACT_IDS[$idx]}" SELECTED_CHAT_MSG_COUNT="${MSG_COUNTS[$idx]}" break fi done fi else for idx in "${!CONTACT_IDS[@]}"; do printf " ${BOLD}%2d)${NC} %-${DISPLAY_COL_WIDTH}s ${CYAN}%${MAX_MSG_COUNT_LEN}s messages${NC} ${BLUE}[chat %s]${NC}\n" \ "$((idx + 1))" "${DISPLAY_IDS[$idx]}" \ "${MSG_COUNTS[$idx]}" \ "${CHAT_IDS[$idx]}" done echo "" MAX_SELECTION="${#CONTACT_IDS[@]}" echo -e "${BOLD}Select one one-to-one conversation to export:${NC}" echo "Type the number (1-$MAX_SELECTION) or paste the exact contact value from the list." echo "Type m to enter a phone number or email manually." echo "Type q to quit." while true; do read -r -p "> " SELECTION if [[ "$SELECTION" == "q" ]] || [[ "$SELECTION" == "quit" ]]; then echo "Cancelled." exit 0 fi if [[ "$SELECTION" == "m" ]] || [[ "$SELECTION" == "manual" ]]; then read -r -p "Phone number or email: " CONTACT_ID if [[ -z "$CONTACT_ID" ]]; then echo -e "${RED}No value entered. Try again, or type q to quit.${NC}" continue fi SELECTED_CHAT_MSG_COUNT=$(lookup_one_to_one_msg_count_for_contact "$CONTACT_ID") break fi if [[ "$SELECTION" =~ ^[0-9]+$ ]] && [[ "$SELECTION" -ge 1 ]] && [[ "$SELECTION" -le "$MAX_SELECTION" ]]; then selected_idx=$((SELECTION - 1)) CONTACT_ID="${CONTACT_IDS[$selected_idx]}" SELECTED_CHAT_MSG_COUNT="${MSG_COUNTS[$selected_idx]}" break fi for idx in "${!CONTACT_IDS[@]}"; do if [[ "$SELECTION" == "${DISPLAY_IDS[$idx]}" ]] || [[ "$SELECTION" == "${CONTACT_IDS[$idx]}" ]]; then CONTACT_ID="${CONTACT_IDS[$idx]}" SELECTED_CHAT_MSG_COUNT="${MSG_COUNTS[$idx]}" break fi done if [[ -n "$CONTACT_ID" ]]; then break fi echo -e "${RED}Invalid selection. Choose a number 1-$MAX_SELECTION or paste one value exactly as shown.${NC}" done fi if [[ -z "$CONTACT_ID" ]]; then echo -e "${RED}${WARN} Could not determine selected contact.${NC}" exit 1 fi fi CONTACT_ID="$(printf "%s" "$CONTACT_ID" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" if [[ -z "$CONTACT_ID" ]]; then echo -e "${RED}${WARN} Contact value cannot be empty.${NC}" exit 1 fi if [[ "$CONTACT_ID" == *","* ]]; then echo -e "${RED}${WARN} Contact value cannot contain commas.${NC}" echo "Use a single phone number or email address." exit 1 fi echo "" echo -e "${YELLOW}Exporting conversation with: ${BOLD}$CONTACT_ID${NC}" echo "This may take a moment for long conversations..." echo "" # Export the conversation TIMESTAMP=$(date +%Y%m%d_%H%M%S) SAFE_CONTACT=$(echo "$CONTACT_ID" | sed 's/[^a-zA-Z0-9]/_/g') OUTPUT_FILE="$EXPORT_DIR/messages_${SAFE_CONTACT}_${TIMESTAMP}.txt" # Run export with error capture IMESSAGE_HELP="$(imessage-exporter --help 2>&1 || true)" if [[ "$IMESSAGE_HELP" == *"--conversation-filter"* ]]; then CONTACT_FILTER_FLAG="--conversation-filter" elif [[ "$IMESSAGE_HELP" == *"--filter-contact"* ]]; then CONTACT_FILTER_FLAG="--filter-contact" else CONTACT_FILTER_FLAG="" fi if [[ "$IMESSAGE_HELP" == *"--format"* ]]; then FORMAT_FLAG="--format" elif [[ "$IMESSAGE_HELP" == *"--export-type"* ]]; then FORMAT_FLAG="--export-type" else FORMAT_FLAG="" fi if [[ -z "$CONTACT_FILTER_FLAG" ]] || [[ -z "$FORMAT_FLAG" ]]; then echo -e "${RED}${WARN} Unsupported imessage-exporter version.${NC}" echo "Could not find required CLI flags in: imessage-exporter --help" echo "Try updating with: brew upgrade imessage-exporter" exit 1 fi # Export into an isolated temporary directory so we can validate outputs # before surfacing a final file to the user. TEMP_EXPORT_DIR=$(mktemp -d "$EXPORT_DIR/.tmp_export_XXXXXX") if [[ -z "$TEMP_EXPORT_DIR" ]] || [[ ! -d "$TEMP_EXPORT_DIR" ]]; then echo -e "${RED}${WARN} Failed to create temporary export directory.${NC}" exit 1 fi EXPORT_ERROR=$(imessage-exporter \ "$FORMAT_FLAG" txt \ --export-path "$TEMP_EXPORT_DIR" \ "$CONTACT_FILTER_FLAG" "$CONTACT_ID" \ 2>&1) || true # Find exported conversation files produced for this run. # `Orphaned.txt` is always created by imessage-exporter and is not a real conversation export. declare -a EXPORTED_FILES while IFS= read -r file_path; do EXPORTED_FILES+=("$file_path") done < <(find "$TEMP_EXPORT_DIR" -maxdepth 1 -type f -name "*.txt" ! -iname "orphaned.txt" -print 2>/dev/null) if [[ "${#EXPORTED_FILES[@]}" -eq 0 ]]; then echo -e "${RED}${WARN} Export failed.${NC}" echo "" if [[ "$EXPORT_ERROR" == *"Full Disk Access"* ]] || [[ "$EXPORT_ERROR" == *"permission"* ]]; then echo "It looks like Full Disk Access isn't working yet." echo "Please make sure you:" echo " 1. Added Terminal to Full Disk Access" echo " 2. Completely quit Terminal (Cmd+Q)" echo " 3. Reopened Terminal and ran this script again" elif [[ "$EXPORT_ERROR" == *"No messages"* ]] || [[ "$EXPORT_ERROR" == *"empty"* ]]; then echo "No messages found for this contact." echo "Make sure you selected the right conversation." elif [[ "$EXPORT_ERROR" == *"across 0 chatrooms"* ]]; then echo "Could not match that conversation in your Messages database." echo "Try selecting the conversation by number from the list and run again." else echo "Error details: $EXPORT_ERROR" echo "" echo "Try:" echo " • Checking the phone number/email is correct" echo " • Making sure Messages app is synced" echo " • Restarting your Mac and trying again" fi exit 1 fi # If we have the exact selected chat's expected message count from chat.db, # pick the exported file whose count is closest to that target. # This avoids false positives from contact-level filtering that can include groups. if [[ "$SELECTED_CHAT_MSG_COUNT" =~ ^[0-9]+$ ]]; then target_count="$SELECTED_CHAT_MSG_COUNT" best_diff=-1 best_size=-1 best_candidate="" best_candidate_msg_count=0 for candidate in "${EXPORTED_FILES[@]}"; do candidate_msg_count=$(grep -c "^[A-Z][a-z]* [0-9]" "$candidate" 2>/dev/null || wc -l < "$candidate" | tr -d ' ') if [[ ! "$candidate_msg_count" =~ ^[0-9]+$ ]]; then candidate_msg_count=0 fi if [[ "$candidate_msg_count" -ge "$target_count" ]]; then diff=$((candidate_msg_count - target_count)) else diff=$((target_count - candidate_msg_count)) fi candidate_size=$(wc -c < "$candidate" | tr -d " ") if [[ -z "$best_candidate" ]] || [[ "$best_diff" -lt 0 ]] || [[ "$diff" -lt "$best_diff" ]] || [[ "$diff" -eq "$best_diff" && "$candidate_size" -gt "$best_size" ]]; then best_candidate="$candidate" best_diff="$diff" best_size="$candidate_size" best_candidate_msg_count="$candidate_msg_count" fi done if [[ -z "$best_candidate" ]]; then echo -e "${RED}${WARN} Export failed to produce a matching conversation file.${NC}" exit 1 fi # If the best match is still extremely far from the selected chat size, # abort instead of returning the wrong conversation. max_allowed_diff=$((target_count / 5)) if [[ "$max_allowed_diff" -lt 50 ]]; then max_allowed_diff=50 fi if [[ "$best_diff" -gt "$max_allowed_diff" ]]; then echo -e "${RED}${WARN} Could not isolate the selected one-to-one conversation.${NC}" echo "" echo "Selected chat expected about ${target_count} messages, but best export match has ${best_candidate_msg_count}." echo "This usually means imessage-exporter matched a different thread for that contact." echo "Please rerun and pick the conversation by number from the one-to-one list." exit 1 fi EXPORTED_FILE="$best_candidate" else # Without a selected chat count (fallback mode), keep the one-to-one validator # to reduce the chance of exporting group-chat content. declare -a VALID_EXPORTED_FILES for candidate in "${EXPORTED_FILES[@]}"; do if is_one_to_one_export_file "$candidate"; then VALID_EXPORTED_FILES+=("$candidate") fi done if [[ "${#VALID_EXPORTED_FILES[@]}" -eq 0 ]]; then echo -e "${RED}${WARN} Export included no valid one-to-one conversation files.${NC}" echo "" echo "The selected contact likely appears in group chats, and those exports were excluded." echo "Please choose another conversation from the one-to-one list." exit 1 fi EXPORTED_FILE="${VALID_EXPORTED_FILES[0]}" if [[ "${#VALID_EXPORTED_FILES[@]}" -gt 1 ]]; then max_size=$(wc -c < "$EXPORTED_FILE" | tr -d " ") for candidate in "${VALID_EXPORTED_FILES[@]:1}"; do candidate_size=$(wc -c < "$candidate" | tr -d " ") if [[ "$candidate_size" -gt "$max_size" ]]; then EXPORTED_FILE="$candidate" max_size="$candidate_size" fi done fi fi # Check file isn't empty FILE_SIZE=$(wc -c < "$EXPORTED_FILE" | tr -d ' ') if [[ "$FILE_SIZE" -lt 100 ]]; then echo -e "${RED}${WARN} Export file is too small (${FILE_SIZE} bytes).${NC}" echo "The conversation might be empty or only contain images/attachments." echo "We need text messages to generate your Replay." rm -f "$EXPORTED_FILE" exit 1 fi # Rename to our standard format if ! mv "$EXPORTED_FILE" "$OUTPUT_FILE" 2>/dev/null; then cp "$EXPORTED_FILE" "$OUTPUT_FILE" fi # Count messages (rough estimate - actual messages, not lines) MSG_COUNT=$(grep -c "^[A-Z][a-z]* [0-9]" "$OUTPUT_FILE" 2>/dev/null || wc -l < "$OUTPUT_FILE" | tr -d ' ') echo "" echo -e "${GREEN}${BOLD}═══════════════════════════════════════════════════════════${NC}" echo -e "${GREEN}${BOLD} ${CHECK} Export Complete!${NC}" echo -e "${GREEN}${BOLD}═══════════════════════════════════════════════════════════${NC}" echo "" echo -e " ${BOLD}File:${NC} $OUTPUT_FILE" echo -e " ${BOLD}Messages:${NC} ~$MSG_COUNT" echo -e " ${BOLD}Size:${NC} $(du -h "$OUTPUT_FILE" | cut -f1)" echo "" echo -e "${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${CYAN}${BOLD} ${ROCKET} Next Steps${NC}" echo -e "${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e " 1. Go to ${BOLD}https://lovereplay.app${NC}" echo "" echo " 2. Drag and drop this file into the upload area:" echo -e " ${YELLOW}${BOLD}$OUTPUT_FILE${NC}" echo "" echo " 3. Get your Love Replay! ${HEART}" echo "" echo -e "${GREEN}${BOLD}🔒 Privacy Reminder:${NC}" echo "┌────────────────────────────────────────────────────────────┐" echo "│ When you upload this file: │" echo "│ • We process it ONCE to generate your Replay │" echo "│ • We NEVER save your messages to any database │" echo "│ • We NEVER train AI models on your data │" echo "│ • Your file is deleted immediately after processing │" echo "│ │" echo "│ The exported file stays on YOUR computer. │" echo "│ Delete it anytime: just trash the file. │" echo "└────────────────────────────────────────────────────────────┘" echo "" # Open Finder to the file open -R "$OUTPUT_FILE" echo -e "${GREEN}Opening Finder to your exported file...${NC}" echo "" echo "Questions? Issues? Contact us: support@lovereplay.app" echo ""