Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
quick-checks:
runs-on: macos-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Build
run: swift build

- name: Run quick test suite with coverage
run: swift test --enable-code-coverage
22 changes: 22 additions & 0 deletions .github/workflows/full-regression.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Full Regression

on:
workflow_dispatch:
schedule:
- cron: "0 4 * * 1"

jobs:
full-fixture-regression:
runs-on: macos-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Build
run: swift build

- name: Run full regression suite
env:
SWIFT_WA_RUN_FULL_FIXTURE_TESTS: "1"
run: swift test
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ DerivedData/
.netrc
Package.resolved
.vscode
Tests/**
Tests/Data/**
!Tests/Data/JSONContract/
!Tests/Data/JSONContract/**
complete_project.txt
142 changes: 142 additions & 0 deletions Docs/JSONContract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# JSON Contract

This document defines the canonical JSON shape exposed by `SwiftWABackupAPI` when encoding the public `Encodable` models with:

- `JSONEncoder.dateEncodingStrategy = .iso8601`
- `JSONEncoder.outputFormatting = [.prettyPrinted, .sortedKeys]`

The contract is verified by the snapshot tests under `Tests/SwiftWABackupAPITests/JSONContractTests.swift` and the fixtures stored in `Tests/Data/JSONContract/`.

## Encoding Rules

- Dates are encoded as ISO 8601 strings with timezone information, for example `2024-04-03T11:24:16Z`.
- Object keys are sorted when using the recommended encoder configuration above.
- Optional properties are omitted when their value is `nil`.
- Arrays preserve the order returned by the API.

## `Reaction`

```json
{
"emoji": "👍",
"senderPhone": "34636104084"
}
```

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `emoji` | `String` | Yes | Emoji used in the reaction. |
| `senderPhone` | `String` | Yes | Phone number derived from the reacting JID, or `"Me"` for the owner. |

## `ChatInfo`

```json
{
"chatType": "individual",
"contactJid": "34636104084@s.whatsapp.net",
"id": 44,
"isArchived": false,
"lastMessageDate": "2024-04-03T11:24:16Z",
"name": "Aitor Medrano",
"numberMessages": 153,
"photoFilename": "chat_44.jpg"
}
```

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `Int` | Yes | Chat identifier from `ZWACHATSESSION.Z_PK`. |
| `contactJid` | `String` | Yes | Raw WhatsApp JID for the chat. |
| `name` | `String` | Yes | Resolved chat display name. |
| `numberMessages` | `Int` | Yes | Number of supported messages returned by the API. |
| `lastMessageDate` | `String` | Yes | ISO 8601 timestamp for the latest supported message. |
| `chatType` | `"group" | "individual"` | Yes | Chat type exposed by the API. |
| `isArchived` | `Bool` | Yes | Whether the chat is archived. |
| `photoFilename` | `String` | No | Copied avatar filename when photo export is requested and available. |

## `MessageInfo`

```json
{
"caption": "Example caption",
"chatId": 44,
"date": "2024-04-03T11:24:16Z",
"id": 125482,
"isFromMe": false,
"latitude": 38.3456,
"longitude": -0.4815,
"mediaFilename": "example.jpg",
"message": "Claro, cada vez que vaya a la UA te aviso.",
"messageType": "Text",
"reactions": [
{
"emoji": "👍",
"senderPhone": "Me"
}
],
"replyTo": 125479,
"seconds": 12,
"senderName": "Aitor Medrano",
"senderPhone": "34636104084"
}
```

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `Int` | Yes | Message identifier from `ZWAMESSAGE.Z_PK`. |
| `chatId` | `Int` | Yes | Parent chat identifier. |
| `message` | `String` | No | Message text or normalized event text. |
| `date` | `String` | Yes | ISO 8601 timestamp for the message. |
| `isFromMe` | `Bool` | Yes | Whether the message was sent by the owner. |
| `messageType` | `String` | Yes | Human-readable message type name. |
| `senderName` | `String` | No | Resolved display name for incoming messages. |
| `senderPhone` | `String` | No | Resolved phone number for incoming messages. |
| `caption` | `String` | No | Media caption or title. |
| `replyTo` | `Int` | No | Identifier of the replied-to message when it can be resolved. |
| `mediaFilename` | `String` | No | Exported media filename when media is copied or resolved. |
| `reactions` | `[Reaction]` | No | Reactions attached to the message. |
| `error` | `String` | No | Optional warning associated with media handling. |
| `seconds` | `Int` | No | Duration for audio and video messages. |
| `latitude` | `Double` | No | Latitude for location messages. |
| `longitude` | `Double` | No | Longitude for location messages. |

## `ContactInfo`

```json
{
"name": "Aitor Medrano",
"phone": "34636104084",
"photoFilename": "34636104084.jpg"
}
```

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | `String` | Yes | Resolved display name. |
| `phone` | `String` | Yes | Phone number derived from the contact JID. |
| `photoFilename` | `String` | No | Copied avatar filename when available. |

## `ChatDumpPayload`

```json
{
"chatInfo": { "...": "..." },
"contacts": [
{ "...": "..." }
],
"messages": [
{ "...": "..." }
]
}
```

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `chatInfo` | `ChatInfo` | Yes | Chat metadata for the export. |
| `messages` | `[MessageInfo]` | Yes | Messages returned for the chat. |
| `contacts` | `[ContactInfo]` | Yes | Contacts resolved for the chat. |

## Notes

- `ChatDump` remains available as the legacy tuple returned by `getChat(...)`.
- `ChatDumpPayload` is the recommended type for JSON export because it is stable, explicit, and directly `Encodable`.
2 changes: 1 addition & 1 deletion Docs/WhatsAppDatabase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ All schema checks live in `DatabaseHelpers.swift` and `DatabaseProtocols.swift`;
| 11 | GIF | Treated like video, stored as MP4 in the backup. |
| 15 | Sticker | Returns `.webp` filename. |

`SwiftWABackupAPITests.testChatMessages` verifies that the counts for each supported type are stable against the fixture (e.g. 5532 images, 489 videos, 310 statuses).
`SwiftWABackupAPITests.testChatMessages` verifies that the counts for each supported type are stable against the fixture (currently 5281 images, 489 videos, and 264 status messages).

### Status (`ZMESSAGETYPE = 10`) Subcodes (Fixture Snapshot)

Expand Down
171 changes: 163 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,170 @@
# SwiftWABackupAPI

Swift API to explore your WhatsApp chats in an unencrypted iPhone backup. It is used in the companion MacOS command line application [WABackupExtractor](https://github.com/domingogallardo/WABackupExtractor).
`SwiftWABackupAPI` is a Swift package for exploring WhatsApp data stored inside an unencrypted iPhone backup. It powers the companion macOS CLI application [WABackupExtractor](https://github.com/domingogallardo/WABackupExtractor), but it can also be consumed directly from your own Swift tools and apps.

## ⛔️Privacy Warning⛔️
## Privacy Warning

This tool is designed to access WhatsApp backup data for legitimate purposes such as data backup and
recovery or data analysis. It is crucial to remember that accessing, extracting, or analyzing chat data
without the explicit consent of all involved parties can violate privacy laws and regulations, as well
as WhatsApp's terms of service.
This package is intended for legitimate backup, recovery, export, and personal analysis workflows.

Any use of this tool should respect privacy laws and regulations, as well as WhatsApp's terms of service.
Accessing or processing WhatsApp conversations without the explicit consent of the people involved can violate privacy laws, workplace policies, and WhatsApp terms of service. Make sure you have the legal and ethical right to inspect the data before using this package.

Always respect the privacy and rights of others.☮️
## What The Package Exposes

The public API is centered on `WABackup`:

- Discover available iPhone backups with `getBackups()`
- Connect to a WhatsApp `ChatStorage.sqlite` database with `connectChatStorageDb(from:)`
- List chats with `getChats(directoryToSavePhotos:)`
- Export a chat with `getChat(chatId:directoryToSaveMedia:)`
- Export the same data as an `Encodable` wrapper with `getChatPayload(chatId:directoryToSaveMedia:)`

Returned models are `Encodable` and designed to be easy to serialize:

- `ChatInfo`
- `MessageInfo`
- `ContactInfo`
- `Reaction`
- `ChatDumpPayload`

## Requirements

- A macOS environment with access to an iPhone backup directory
- A non-encrypted backup that contains WhatsApp data
- Permission to read the backup folder

By default, `WABackup` looks under:

```text
~/Library/Application Support/MobileSync/Backup/
```

On many systems you will need to grant Full Disk Access to the host app or terminal.

## Installation

Add the package dependency in `Package.swift` using the release rule that matches how you publish or consume the package:

```swift
.package(url: "https://github.com/domingogallardo/SwiftWABackupAPI.git", from: "1.0.10")
```

Then add the product to your target dependencies:

```swift
.product(name: "SwiftWABackupAPI", package: "SwiftWABackupAPI")
```

## Basic Usage

```swift
import Foundation
import SwiftWABackupAPI

let backupAPI = WABackup()
let backups = try backupAPI.getBackups()
guard let backup = backups.validBackups.first else {
throw NSError(domain: "Example", code: 1)
}

try backupAPI.connectChatStorageDb(from: backup)

let chats = try backupAPI.getChats()
let payload = try backupAPI.getChatPayload(chatId: chats[0].id, directoryToSaveMedia: nil)
print(payload.chatInfo.name)
print(payload.messages.count)
```

If you want exported media and copied profile images:

```swift
let outputDirectory = URL(fileURLWithPath: "/tmp/wa-export", isDirectory: true)
let chats = try backupAPI.getChats(directoryToSavePhotos: outputDirectory)
let payload = try backupAPI.getChatPayload(chatId: chats[0].id, directoryToSaveMedia: outputDirectory)
```

## JSON Export

`ChatDumpPayload` exists specifically so consumers can serialize a full export without depending on the legacy tuple-based `ChatDump`.

Recommended JSON settings:

- `JSONEncoder.dateEncodingStrategy = .iso8601`
- `JSONEncoder.outputFormatting = [.prettyPrinted, .sortedKeys]`

Example:

```swift
let payload = try backupAPI.getChatPayload(chatId: 44, directoryToSaveMedia: nil)

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]

let jsonData = try encoder.encode(payload)
let jsonString = String(decoding: jsonData, as: UTF8.self)
print(jsonString)
```

The formal JSON contract is documented in [Docs/JSONContract.md](./Docs/JSONContract.md).

## Media And Profile Images

When an output directory is provided:

- Message media is copied using the original WhatsApp filename when possible
- Chat avatars are copied as `chat_<chatId>.jpg` or `chat_<chatId>.thumb`
- Contact avatars are copied as `<phone>.jpg` or `<phone>.thumb`

You can observe writes through `WABackupDelegate`:

```swift
final class ExportDelegate: WABackupDelegate {
func didWriteMediaFile(fileName: String) {
print("Processed media file: \(fileName)")
}
}

let delegate = ExportDelegate()
backupAPI.delegate = delegate
```

## Error Handling

The package exposes three error families:

- `BackupError` for backup discovery and file-copy issues
- `DatabaseErrorWA` for SQLite connectivity, missing rows, and unsupported schemas
- `DomainError` for higher-level WhatsApp interpretation problems

Example:

```swift
do {
let chats = try backupAPI.getChats()
print(chats.count)
} catch let error as BackupError {
print("Backup error: \(error.localizedDescription)")
} catch let error as DatabaseErrorWA {
print("Database error: \(error.localizedDescription)")
} catch {
print("Unexpected error: \(error)")
}
```

## Tests

The repository now ships two testing modes:

- Quick verification: `swift test`
- Full regression against the large bundled fixture:

```bash
SWIFT_WA_RUN_FULL_FIXTURE_TESTS=1 swift test
```

The quick suite covers smoke scenarios, JSON contract snapshots, helper behaviour, and failure handling. The full suite keeps the large regression checks for counts, fixtures, contacts, and message content.

## Additional Documentation

- [Database reference](./Docs/WhatsAppDatabase/README.md)
- [JSON contract](./Docs/JSONContract.md)
Loading
Loading