diff --git a/CHANGELOG.md b/CHANGELOG.md index 31147c6..bc59e70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.2.0 + +- Added WhatsApp import from DB +- Added import reactions to signal +- Updates docs + ## 1.1.0 - Added Telegram messages import diff --git a/README.md b/README.md index 19dba5f..946a768 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ Import messages from other apps like Telegram or WhatsApp to Signal. ## Caveats - Only tested on Android -- No group text support - The commands were run on macOS ## Prerequisites @@ -21,6 +20,7 @@ See: [Signal](docs/Signal.md) Import messages from: - [Telegram](docs/Telegram.md) +- [WhatApp DB](docs/WhatApp_DB.md) - [WhatApp export](docs/WhatApp_Export.md) ### Available commands @@ -37,15 +37,34 @@ bin/move_to_signal.dart \ -o build/move_to_signal_$(uname -s)_$(uname -m) ``` +## Feature Map + +| Name | Telegram | WhatApp DB | WhatApp export | +| :------------------------- | :------: | :--------: | :------------: | +| All 1-on-1 text messages | ✅ | ✅ | ❌ | +| Group chats | ❌ | ❌ | ❌ | +| Original timestamps | ✅ | ✅ | ❌ | +| Reactions (emoji) | ❌ | ✅ | ❌ | +| Media (images/audio/links) | ❌ | ❌ | ❌ | + ## Known issues -### Language based date time format in Whatsapp exports +### Language based date time format in WhatsApp exports + +NOTE: This can be avoided by using the [WhatApp DB](docs/WhatApp_DB.md) import. -Whatsapp exports have language based time format in export file and for Android without seconds. In my case 01/12/2023, 23:59 +WhatsApp exports have language based time format in export file and for Android without seconds. In my case 01/12/2023, 23:59 The new WhatsApp macOS App has a more usable format [01.12.23, 23:59:42] for the same message as Android, but in my case only loads the last 3 years of chat history. Please open an issue with a small anonymized sample export and your device language. Or better open a PR with a fix. :) +### Missing messages form WhatsApp exports + +NOTE: This can be avoided by using the [WhatApp DB](docs/WhatApp_DB.md) import. + +If a message was part of a sent image the message won't be in the export. +I opened an issue with WhatsApp, but don't know if this will ever be fixed. + ## Sponsor this project [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://paypal.me/movetosignal/5) diff --git a/bin/move_to_signal.dart b/bin/move_to_signal.dart index fc9cf32..067e3f5 100644 --- a/bin/move_to_signal.dart +++ b/bin/move_to_signal.dart @@ -1,9 +1,10 @@ import 'package:move_to_signal/import/signal.dart'; import 'package:move_to_signal/source/telegram.dart'; -import 'package:move_to_signal/source/whats_app.dart'; +import 'package:move_to_signal/source/whats_app_db.dart'; +import 'package:move_to_signal/source/whats_app_export.dart'; void main(List arguments) { - String command = 'ImportWhatsApp'; + String command = 'ImportWhatsAppExports'; bool verbose = false; // Read all arguments @@ -23,8 +24,14 @@ void main(List arguments) { telegramImport.run(arguments); break; - case 'ImportWhatsApp': - final whatsAppImport = WhatsApp(); + case 'ImportWhatsAppDb': + final whatsAppImportDb = WhatsAppDb(); + whatsAppImportDb.verbose = verbose; + whatsAppImportDb.run(arguments); + + break; + case 'ImportWhatsAppExports': + final whatsAppImport = WhatsAppExport(); whatsAppImport.verbose = verbose; whatsAppImport.run(arguments); diff --git a/docs/Commands.md b/docs/Commands.md index cfc0fb2..ba16dc0 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -6,7 +6,8 @@ --command= [ImportTelegram] For Telegram exports - [ImportWhatsApp] For WhatsApp exports (default) + [ImportWhatsAppDb] For WhatsApp db import (needs decrypted db with wa-crypt-tools) + [ImportWhatsAppExports] For WhatsApp exports (default) [SignalDecrypt] Just to decrypt Signal backup file [SignalEncrypt] Just to encrypt Signal backup file @@ -35,6 +36,14 @@ [Prepare] Prepare the import by extracting all conversations from telegramJson into separate files to review. (default) [Import] Imports the files from step [Prepare] into the Signal database. ---whatsappExports= - Path to the WhatsApp export .txt file +--whatsAppDb= + Path to the WhatsApp msgstore.db file + +--whatsAppExports= + [ImportWhatsAppDb] Path where the WhatsApp .txt file are written + [ImportWhatsAppExports] Path to the WhatsApp export .txt files + +--whatsAppMode= + [Prepare] Prepare the import by extracting all conversations from msgstore.db into separate files to review. (default) + [Import] Imports the files from step [Prepare] into the Signal database. ``` diff --git a/docs/Install.md b/docs/Install.md index 29e1b36..9dba83d 100644 --- a/docs/Install.md +++ b/docs/Install.md @@ -3,4 +3,5 @@ - Dependencies - [Dart](https://dart.dev/) to run from source - Download/Clone this git or download arm macOS binary - - [signalbackup-tools](https://github.com/bepaald/signalbackup-tools) De and Encrypt the Signal backup + - [signalbackup-tools](https://github.com/bepaald/signalbackup-tools) De- and Encrypt the Signal backup + - [wa-crypt-tools](https://github.com/ElDavoo/wa-crypt-tools) Decrypt WhatsApp backup (Only needed to import from db) diff --git a/docs/WhatApp_DB.md b/docs/WhatApp_DB.md new file mode 100644 index 0000000..83b79eb --- /dev/null +++ b/docs/WhatApp_DB.md @@ -0,0 +1,92 @@ +# WhatApp DB + +Always start by creating a [Signal](docs/Signal.md) backup. + +1. Create WhatsApp backup, decrypt the msgstore.db.crypt15 with [wa-crypt-tools](https://github.com/ElDavoo/wa-crypt-tools) and copy the msgstore.db to the working folder. + +2. Run MoveToSignal in terminal for prepare the import + + Mac arm64 binary + + ```bash + cd path/to/working/folder/ + + path/to/MoveToSignal/move_to_signal_Darwin_arm64 \ + --command=ImportWhatsAppDb \ + --signalBackup=./signal-YYYY-MM-DD-HH-mm-ss.backup \ + --signalBackupKey=123451234512345123451234512345 \ + --signalPhoneNumber=+49123456789 \ + --whatsAppDb="path/to/msgstore.db" \ + --whatsAppExports=. \ + --whatsAppMode=Prepare \ + --verbose + ``` + + From source + + ```bash + cd path/to/working/folder/ + + dart run path/to/MoveToSignal/bin/move_to_signal.dart \ + --command=ImportWhatsAppDb \ + --signalBackup=./signal-YYYY-MM-DD-HH-mm-ss.backup \ + --signalBackupKey=123451234512345123451234512345 \ + --signalPhoneNumber=+49123456789 \ + --whatsAppDb="path/to/msgstore.db" \ + --whatsAppExports=. \ + --whatsAppMode=Prepare \ + --verbose + ``` + + A new folder named WhatsAppExportsFolder will be created for the export files. + WhatsApp exports will be named eg: +4912345678-(Screen name if found).txt + +3. Rename exports + + Please review the all .txt files and make sure to file names start with the contact phone number the user uses with Signal. + At this point you can also merge files into one, if a user had multiple WhatsApp identities. + Please delete all files you don't want to import. + + All WhatsApp export files must be renamed like: + contactPhoneNumber-Screen Name.txt + + eg: +49123456789-Max ExampleName.txt + + Only the phone number important for WhatsApp DB imports. + The phone number needs to in international format starting with + and must only contain numbers. + +4. Run MoveToSignal in terminal to import the prepared messages + + Mac arm64 binary + + ```bash + cd path/to/working/folder/ + + path/to/MoveToSignal/move_to_signal_Darwin_arm64 \ + --command=ImportWhatsAppDb \ + --signalBackup=./signal-YYYY-MM-DD-HH-mm-ss.backup \ + --signalBackupKey=123451234512345123451234512345 \ + --signalPhoneNumber=+49123456789 \ + --whatsAppExports=. \ + --whatsAppMode=Import \ + --verbose + ``` + + From source + + ```bash + cd path/to/working/folder/ + + dart run path/to/MoveToSignal/bin/move_to_signal.dart \ + --command=ImportWhatsAppDb \ + --signalBackup=./signal-YYYY-MM-DD-HH-mm-ss.backup \ + --signalBackupKey=123451234512345123451234512345 \ + --signalPhoneNumber=+49123456789 \ + --whatsAppExports=. \ + --whatsAppMode=Import \ + --verbose + ``` + + Once done, a new Signal backup file is created, like: signal-signal-YYYY-MM-DD-HH-mm-ss.backup (new timestamp) + +5. Follow the "After importing all messages" steps from [Signal](docs/Signal.md) diff --git a/docs/WhatApp_Export.md b/docs/WhatApp_Export.md index b99d67d..8d11c4f 100644 --- a/docs/WhatApp_Export.md +++ b/docs/WhatApp_Export.md @@ -39,7 +39,7 @@ Always start by creating a [Signal](docs/Signal.md) backup. --signalBackup=./signal-YYYY-MM-DD-HH-mm-ss.backup \ --signalBackupKey=123451234512345123451234512345 \ --signalPhoneNumber=+49123456789 \ - --whatsappExports=./whatsapp \ + --whatsAppExports=./whatsapp \ --verbose ``` @@ -53,7 +53,7 @@ Always start by creating a [Signal](docs/Signal.md) backup. --signalBackup=./signal-YYYY-MM-DD-HH-mm-ss.backup \ --signalBackupKey=123451234512345123451234512345 \ --signalPhoneNumber=+49123456789 \ - --whatsappExports=./whatsapp \ + --whatsAppExports=./whatsapp \ --verbose ``` diff --git a/lib/import/signal.dart b/lib/import/signal.dart index 8a17cef..c63e4ca 100644 --- a/lib/import/signal.dart +++ b/lib/import/signal.dart @@ -200,6 +200,15 @@ class Signal { 'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', ); + // Prepare a statement to run it multiple times: + final reactionImport = _database!.prepare( + 'INSERT INTO reaction ' + '(' + 'message_id, author_id, emoji, date_sent, date_received' + ') ' + 'VALUES (?, ?, ?, ?, ?)', + ); + // Import all messages var counter = 0; var step = (_signalMessages.length / 10).ceil(); @@ -251,6 +260,25 @@ class Signal { signalMessageImportSuccess = true; } + // Get last insert id as signalMessageId + int signalMessageId = + _database!.select('select last_insert_rowid()').first.columnAt(0); + + // Import reactions + for (var reaction in signalMessage.reactions) { + if (reaction.sendTimestamp == null) { + continue; + } + + reactionImport.execute([ + signalMessageId, + reaction.authorId, + reaction.reaction, + reaction.sendTimestamp, + reaction.receivedTimestamp ?? reaction.sendTimestamp! + 500, + ]); + } + if (!verbose) continue; counter++; diff --git a/lib/model/signal_message.dart b/lib/model/signal_message.dart index 7e542c0..fda2faa 100644 --- a/lib/model/signal_message.dart +++ b/lib/model/signal_message.dart @@ -1,3 +1,5 @@ +import 'package:move_to_signal/model/signal_reaction.dart'; + class SignalMessage { int messageDateTime = 0; int? dateSent; @@ -18,8 +20,10 @@ class SignalMessage { int unidentified = 1; int reactionsLastSeen = -1; int notifiedTimestamp = 0; + List reactions = []; - Map toJson() => { + @override + String toString() => { "dateSent": dateSent, "dateReceived": dateReceived, "dateServer": dateServer, @@ -38,11 +42,8 @@ class SignalMessage { "unidentified": unidentified, "reactionsLastSeen": reactionsLastSeen, "notifiedTimestamp": notifiedTimestamp, - }; - - @override - String toString() => - '$dateSent|$dateReceived|$dateServer|$threadId|$fromRecipientId|$fromDeviceId|$toRecipientId|$type|$body|$read|$mType|$st|$receiptTimestamp|$hasDeliveryReceipt|$hasReadReceipt|$unidentified|$reactionsLastSeen|$notifiedTimestamp'; + "reactions": reactions, + }.toString(); void setReceived() { dateSent = messageDateTime; diff --git a/lib/model/signal_reaction.dart b/lib/model/signal_reaction.dart new file mode 100644 index 0000000..d6b0dd3 --- /dev/null +++ b/lib/model/signal_reaction.dart @@ -0,0 +1,18 @@ +import 'dart:convert'; + +class SignalReaction { + int? authorId; + String? reaction; + bool? fromMe; + int? sendTimestamp; + int? receivedTimestamp; + + @override + String toString() => { + '"authorId"': authorId, + '"reaction"': jsonEncode(reaction), + '"fromMe"': fromMe, + '"sendTimestamp"': sendTimestamp, + '"receivedTimestamp"': receivedTimestamp, + }.toString(); +} diff --git a/lib/model/signal_thread.dart b/lib/model/signal_thread.dart index 88d387b..8cb8f0d 100644 --- a/lib/model/signal_thread.dart +++ b/lib/model/signal_thread.dart @@ -5,14 +5,12 @@ class SignalThread { int snippetType = 10485780; int lastSeen = 0; - Map toJson() => { + @override + String toString() => { "_id": threadId, "date": date, "snippet": snippet, "snippetType": snippetType, "lastSeen": lastSeen, - }; - - @override - String toString() => '$threadId|$date|$snippet|$snippetType|$lastSeen'; + }.toString(); } diff --git a/lib/model/whats_app_message.dart b/lib/model/whats_app_message.dart new file mode 100644 index 0000000..e4438c1 --- /dev/null +++ b/lib/model/whats_app_message.dart @@ -0,0 +1,24 @@ +import 'dart:convert'; + +import 'package:move_to_signal/model/whats_app_reaction.dart'; + +class WhatsAppMessage { + int timestamp = 0; + int receivedTimestamp = 0; + int receiptServerTimestamp = 0; + bool fromMe = true; + int read = 0; + String text = ''; + List reactions = []; + + @override + String toString() => { + '"timestamp"': timestamp, + '"receivedTimestamp"': receivedTimestamp, + '"receiptServerTimestamp"': receiptServerTimestamp, + '"fromMe"': fromMe, + '"read"': read, + '"text"': jsonEncode(text), + '"reactions"': reactions, + }.toString(); +} diff --git a/lib/model/whats_app_reaction.dart b/lib/model/whats_app_reaction.dart new file mode 100644 index 0000000..19a8615 --- /dev/null +++ b/lib/model/whats_app_reaction.dart @@ -0,0 +1,16 @@ +import 'dart:convert'; + +class WhatsAppReaction { + String? reaction; + bool? fromMe; + int? sendTimestamp; + int? receivedTimestamp; + + @override + String toString() => { + '"reaction"': jsonEncode(reaction), + '"fromMe"': fromMe, + '"sendTimestamp"': sendTimestamp, + '"receivedTimestamp"': receivedTimestamp, + }.toString(); +} diff --git a/lib/model/whats_app_thread.dart b/lib/model/whats_app_thread.dart new file mode 100644 index 0000000..2e0bc79 --- /dev/null +++ b/lib/model/whats_app_thread.dart @@ -0,0 +1,18 @@ +import 'package:move_to_signal/model/whats_app_message.dart'; + +class WhatsAppThread { + String id = ''; + String name = ''; + String fromId = ''; + String phoneNumber = ''; + List messages = []; + + @override + String toString() => { + "id": id, + "name": name, + "fromId": fromId, + "phoneNumber": phoneNumber, + "messages": messages, + }.toString(); +} diff --git a/lib/source/whats_app_db.dart b/lib/source/whats_app_db.dart new file mode 100644 index 0000000..26a91d3 --- /dev/null +++ b/lib/source/whats_app_db.dart @@ -0,0 +1,358 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:move_to_signal/model/signal_reaction.dart'; +import 'package:move_to_signal/model/whats_app_message.dart'; +import 'package:move_to_signal/model/whats_app_reaction.dart'; +import 'package:move_to_signal/model/whats_app_thread.dart'; +import 'package:path/path.dart' as path; +import 'package:move_to_signal/import/signal.dart'; +import 'package:move_to_signal/model/signal_message.dart'; +import 'package:sqlite3/sqlite3.dart'; + +class WhatsAppDb extends Signal { + String _whatsAppMode = 'Prepare'; + String _whatsAppExports = ''; + File? _whatsAppDb; + late Database _database; + + Directory _whatsAppExportsFolder = Directory('./WhatsAppExportsFolder'); + final List _whatsAppThreads = []; + + @override + run(List arguments) { + // Read all arguments + for (final argument in arguments) { + if (argument.startsWith('--whatsAppDb=')) { + _whatsAppDb = File(argument.split('=').last); + } + if (argument.startsWith('--whatsAppMode=')) { + _whatsAppMode = argument.split('=').last; + } + + if (argument.startsWith('--whatsAppExports=')) { + _whatsAppExports = argument.split('=').last; + } + } + + if (verbose) print('Check missing general WhatsApp arguments'); + + if (_whatsAppExports.isEmpty) { + print('Missing argument --whatsAppExports'); + return; + } + + if (verbose) print('Check WhatsApp Exports folder'); + + if (!Directory(_whatsAppExports).existsSync()) { + print('--whatsAppExports=$_whatsAppExports folder not found'); + return; + } + + _whatsAppExportsFolder = + Directory(path.join(_whatsAppExports, _whatsAppExportsFolder.path)); + + super.run(arguments); + + if (_whatsAppMode == 'Prepare') { + if (verbose) print('Run in WhatsApp prepare mode'); + + if (verbose) print('Check missing prepare WhatsApp arguments'); + + if (_whatsAppDb == null) { + print('Missing argument --whatsAppDb'); + return; + } + + if (!_whatsAppDb!.existsSync()) { + print('--whatsAppDb=${_whatsAppDb!.path} file not found'); + return; + } + + if (verbose) print('Parse WhatsApp DB'); + + _parseWhatsAppDb(); + + if (verbose) print('Write parsed WhatsApp DB to tmp folder'); + + _writeWhatsAppExport(); + + print(''); + print(''); + print('Messages threads exported to: ${_whatsAppExportsFolder.path}'); + print(''); + print( + 'Please review the all .txt files and make sure to file names start with the contact phone number the user uses with Signal.'); + print( + 'At this point you can also merge files into one, if a user had multiple WhatsApp identities.'); + print('Please delete all files you don\'t want to import.'); + print(''); + print('A valid file name looks like: +4912345678-Contact Name.txt'); + print( + 'The phone number needs to in international format starting with + and must only contain numbers.'); + print(''); + print('Once you are done, you can start the import process.'); + print(''); + + signalDbClose(); + } + + if (_whatsAppMode == 'Import') { + if (verbose) print('Run in WhatsApp import mode'); + + if (!_whatsAppExportsFolder.existsSync()) { + print( + 'Folder $_whatsAppExportsFolder not found. Did you run prepare mode first?'); + return; + } + + super.run(arguments); + + _whatsAppExportsFolder.listSync().forEach((whatsAppExport) { + if (whatsAppExport is File && whatsAppExport.path.endsWith('.txt')) { + _parseWhatsAppExport(whatsAppExport); + } + }); + + signalImport(); + } + } + + void _parseWhatsAppDb() { + if (verbose) print('Open the WhatsApp database'); + + _database = sqlite3.open( + _whatsAppDb!.path, + mode: OpenMode.readOnly, + ); + + // Get all 1 on 1 threads + ResultSet threads = _database.select( + 'SELECT _id, raw_string_jid FROM chat_view WHERE raw_string_jid like "%@s.whatsapp.net";'); + + if (verbose) print('Get all messages and reactions'); + for (final thread in threads) { + // Get phone number + final jid = thread['raw_string_jid']; + final jidSplit = jid.split('@'); + if (jidSplit.length != 2) { + // Something went wrong + if (verbose) print('Something went wrong: $thread'); + + continue; + } + + final WhatsAppThread whatsAppThread = WhatsAppThread(); + whatsAppThread.id = thread['_id'].toString(); + whatsAppThread.phoneNumber = '+${jidSplit[0]}'; + whatsAppThread.fromId = jid; + + whatsAppThread.name = + signalGetRecipientName(whatsAppThread.phoneNumber) ?? ''; + + // Get all messages for this thread + ResultSet messages = _database.select( + 'SELECT ' + 'message._id, ' + 'message.text_data, ' + 'message.from_me, ' + 'message.status, ' + 'message.timestamp, ' + 'message.received_timestamp, ' + 'message.receipt_server_timestamp ' + 'FROM message ' + 'WHERE message.chat_row_id=${whatsAppThread.id};', + ); + + for (final message in messages) { + final String? text = message['text_data']; + if (text == null || text.isEmpty) { + // Ignore empty messages + continue; + } + + final WhatsAppMessage whatsAppMessage = WhatsAppMessage(); + if (message['from_me'] != 1) { + whatsAppMessage.fromMe = false; + } + + if (whatsAppMessage.fromMe && message['status'] == 13) { + whatsAppMessage.read = 1; + } + + whatsAppMessage.text = message['text_data']; + whatsAppMessage.timestamp = message['timestamp']; + whatsAppMessage.receivedTimestamp = message['received_timestamp']; + whatsAppMessage.receiptServerTimestamp = + message['receipt_server_timestamp']; + + // Get all reactions for this message + ResultSet reactions = _database.select( + 'SELECT ' + 'message_add_on_reaction.reaction, ' + 'message_add_on_reaction.sender_timestamp, ' + 'message_add_on.received_timestamp, ' + 'message_add_on.from_me ' + 'FROM message_add_on ' + 'LEFT JOIN message_add_on_reaction ON message_add_on_reaction.message_add_on_row_id = message_add_on._id ' + 'WHERE message_add_on.parent_message_row_id=${message['_id']};', + ); + + for (final reaction in reactions) { + final WhatsAppReaction whatsAppReaction = WhatsAppReaction(); + + whatsAppReaction.reaction = reaction['reaction']; + if (reaction['from_me'] == 1) { + whatsAppReaction.fromMe = true; + } else if (reaction['from_me'] == 0) { + whatsAppReaction.fromMe = false; + } + whatsAppReaction.sendTimestamp = reaction['sender_timestamp']; + whatsAppReaction.receivedTimestamp = reaction['received_timestamp']; + + whatsAppMessage.reactions.add(whatsAppReaction); + } + + whatsAppThread.messages.add(whatsAppMessage); + } + + if (whatsAppThread.messages.isNotEmpty) { + _whatsAppThreads.add(whatsAppThread); + } + } + + _database.dispose(); + } + + void _parseWhatsAppExport(File whatsAppExport) { + if (verbose) { + print('Parse WhatsApp export: ${path.basename(whatsAppExport.path)}'); + } + + var filename = path.basenameWithoutExtension(whatsAppExport.path); + var filenameParts = filename.split('-'); + + if (filenameParts.length != 2) { + print('File name format error ${whatsAppExport.path}'); + return; + } + + // Get contact date from filename + final contactNumber = filenameParts[0]; + final contactSignalId = signalGetRecipientID(contactNumber); + if (contactSignalId == 0) { + print( + 'No RecipientID was found for contact "$contactNumber" in Signal backup'); + return; + } + + final contactSignalThreadId = signalGetThreadID(contactSignalId); + if (contactSignalThreadId == 0) { + print( + 'No ThreadId was found for contact "$contactNumber" in Signal backup'); + return; + } + + // Init new SignalMessage + var signalMessage = SignalMessage(); + + // Read WhatsApp export file + final messages = jsonDecode(whatsAppExport.readAsStringSync()); + + for (final message in messages) { + signalMessage.messageDateTime = message['timestamp']; + signalMessage.body = message['text']; + + if (message['fromMe']) { + // Message was sent + + signalMessage.threadId = contactSignalThreadId; + signalMessage.fromRecipientId = signalUserID; + signalMessage.toRecipientId = contactSignalId; + signalMessage.setSend(); + signalMessage.dateSent = message['timestamp']; + signalMessage.dateReceived = message['receivedTimestamp']; + signalMessage.receiptTimestamp = message['receiptServerTimestamp']; + } else { + // Message was received + + signalMessage.threadId = contactSignalThreadId; + signalMessage.fromRecipientId = contactSignalId; + signalMessage.toRecipientId = signalUserID; + + signalMessage.dateSent = message['timestamp']; + signalMessage.dateServer = signalMessage.dateSent! + 500; + signalMessage.dateReceived = message['receivedTimestamp']; + signalMessage.receiptTimestamp = message['receiptServerTimestamp']; + signalMessage.notifiedTimestamp = signalMessage.dateReceived! + 500; + signalMessage.reactionsLastSeen = + signalMessage.notifiedTimestamp + 5000; + } + + for (final reaction in message['reactions']) { + final signalReaction = SignalReaction(); + + if (reaction['fromMe'] == null || + reaction['reaction'] == null || + reaction['reaction'].isEmpty) { + continue; + } + + signalReaction.fromMe = reaction['fromMe']; + + if (signalReaction.fromMe!) { + signalReaction.authorId = signalUserID; + } else { + signalReaction.authorId = contactSignalId; + } + signalReaction.reaction = reaction['reaction']; + signalReaction.sendTimestamp = reaction['sendTimestamp']; + signalReaction.receivedTimestamp = reaction['receivedTimestamp']; + + signalMessage.reactions.add(signalReaction); + } + + signalAddMessage(signalMessage); + signalMessage = SignalMessage(); + } + } + + void _writeWhatsAppExport() { + if (verbose) print('Create WhatsApp export folder.'); + + if (_whatsAppExportsFolder.existsSync()) { + _whatsAppExportsFolder.deleteSync(recursive: true); + } + + _whatsAppExportsFolder.createSync(); + + if (verbose) print('Export WhatsApp threads to files.'); + + for (final whatsAppThread in _whatsAppThreads) { + String fileName = whatsAppThread.phoneNumber; + if (fileName.isEmpty) { + fileName = whatsAppThread.fromId; + } + + fileName = '$fileName-${whatsAppThread.name}.txt'; + + final filePath = path.join(_whatsAppExportsFolder.path, fileName); + final export = File(filePath).openSync(mode: FileMode.writeOnlyAppend); + + if (verbose) print('Export: $fileName'); + + export.writeStringSync("[\n"); + var firstLine = true; + for (final message in whatsAppThread.messages) { + if (!firstLine) { + export.writeStringSync(",\n"); + } else { + firstLine = false; + } + export.writeStringSync(message.toString()); + } + export.writeStringSync("\n]"); + + export.closeSync(); + } + } +} diff --git a/lib/source/whats_app.dart b/lib/source/whats_app_export.dart similarity index 90% rename from lib/source/whats_app.dart rename to lib/source/whats_app_export.dart index cf65203..566c7d0 100644 --- a/lib/source/whats_app.dart +++ b/lib/source/whats_app_export.dart @@ -3,15 +3,15 @@ import 'package:path/path.dart' as path; import 'package:move_to_signal/import/signal.dart'; import 'package:move_to_signal/model/signal_message.dart'; -class WhatsApp extends Signal { - Directory? whatsappExports; +class WhatsAppExport extends Signal { + Directory? _whatsAppExports; @override run(List arguments) { // Read all arguments for (final argument in arguments) { - if (argument.startsWith('--whatsappExports=')) { - whatsappExports = Directory(argument.split('=').last); + if (argument.startsWith('--whatsAppExports=')) { + _whatsAppExports = Directory(argument.split('=').last); } } @@ -21,15 +21,15 @@ class WhatsApp extends Signal { if (verbose) print('Check missing WhatsApp arguments'); - if (whatsappExports == null) { - print('Missing argument --whatsappExports'); + if (_whatsAppExports == null) { + print('Missing argument --whatsAppExports'); return; } if (verbose) print('Check WhatsApp Exports folder'); - if (!whatsappExports!.existsSync()) { - print('--whatsappExports=${whatsappExports!.path} folder not found'); + if (!_whatsAppExports!.existsSync()) { + print('--whatsappExports=${_whatsAppExports!.path} folder not found'); return; } @@ -37,7 +37,7 @@ class WhatsApp extends Signal { signalBackupDecrypt(); - whatsappExports!.listSync().forEach((whatsappExport) { + _whatsAppExports!.listSync().forEach((whatsappExport) { if (whatsappExport is File && whatsappExport.path.endsWith('.txt')) { _parseWhatsappExport(whatsappExport); } diff --git a/pubspec.yaml b/pubspec.yaml index 1a69a96..c35b005 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: move_to_signal description: Import messages from other apps like WhatsApp to Signal. -version: 1.1.0 +version: 1.2.0 repository: https://github.com/de-nets/MoveToSignal environment: