pax_global_header00006660000000000000000000000064147645043450014526gustar00rootroot0000000000000052 comment=ff4005ba02fdeb3d2e9ed73cb62f63b943afc6f8 signalbackup-tools-20250313-1/000077500000000000000000000000001476450434500157265ustar00rootroot00000000000000signalbackup-tools-20250313-1/.github/000077500000000000000000000000001476450434500172665ustar00rootroot00000000000000signalbackup-tools-20250313-1/.github/FUNDING.yml000066400000000000000000000001451476450434500211030ustar00rootroot00000000000000# These are supported funding model platforms custom: "https://www.paypal.me/bepaald" ko_fi: bepaald signalbackup-tools-20250313-1/.github/pull_request_template.md000066400000000000000000000002121476450434500242220ustar00rootroot00000000000000***NO PULL REQUESTS PLEASE*** This project does not currently accept pull requests. If you have suggestions, feel free to open an issue. signalbackup-tools-20250313-1/.github/workflows/000077500000000000000000000000001476450434500213235ustar00rootroot00000000000000signalbackup-tools-20250313-1/.github/workflows/delete-workflow-runs.yml000066400000000000000000000006601476450434500261470ustar00rootroot00000000000000name: Delete old workflow runs on: workflow_dispatch: schedule: - cron: '0 2 * * 1' # Run weekly, at 02:00 on the 1st day of week. jobs: del_runs: runs-on: ubuntu-latest steps: - name: Delete workflow runs uses: Mattraks/delete-workflow-runs@v2 with: token: ${{ github.token }} repository: ${{ github.repository }} retain_days: 0 keep_minimum_runs: 2 signalbackup-tools-20250313-1/.github/workflows/test-linux-build.yml000066400000000000000000000034221476450434500252600ustar00rootroot00000000000000name: Test Linux build on: workflow_dispatch: push: tags-ignore: - '[0-9]*' branches: - master paths-ignore: - '**/README.md' - '**/autoversion.h' - '.github/workflows/*.yml' # This allows a subsequently queued workflow run to interrupt previous runs concurrency: group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' cancel-in-progress: true jobs: do-build-tests-raw: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: | set -x sudo apt-get update sudo apt-get install --yes --no-install-recommends -V g++ libsqlite3-dev libssl-dev libdbus-1-dev - name: Build default run: | g++ --std=c++2b -I/usr/include/dbus-1.0 -I/usr/lib/x86_64-linux-gnu/dbus-1.0/include */*.cc *.cc -o signalbackup-tools -lcrypto -lsqlite3 -ldbus-1 do-build-tests-parallel-script: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: | set -x sudo apt-get update sudo apt-get install --yes --no-install-recommends -V g++ make libsqlite3-dev libssl-dev libdbus-1-dev - name: Build new script run: | bash BUILDSCRIPT.bash do-build-tests-legacy: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - name: Install dependencies run: | set -x sudo apt-get update sudo apt-get install --yes --no-install-recommends -V g++ make libsqlite3-dev libssl-dev libdbus-1-dev - name: Build new script (legacy) run: | CXXFLAGSEXTRA=-Wno-attributes CXXSTD="-std=c++2a" bash BUILDSCRIPT.bash signalbackup-tools-20250313-1/.github/workflows/test-macos-build.yml000066400000000000000000000013761476450434500252310ustar00rootroot00000000000000name: Test macOs build on: workflow_dispatch: push: tags-ignore: - '[0-9]*' branches: - master paths-ignore: - '**/README.md' - '**/autoversion.h' - '.github/workflows/*.yml' # This allows a subsequently queued workflow run to interrupt previous runs concurrency: group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' cancel-in-progress: true jobs: do-build-tests: runs-on: macos-latest steps: - uses: actions/checkout@v4 - name: Prepare run: | brew update - name: Build run: | brew install --HEAD --formula homebrew/signalbackup-tools.rb brew test homebrew/signalbackup-tools.rb signalbackup-tools-20250313-1/.gitignore000066400000000000000000000003011476450434500177100ustar00rootroot00000000000000* !README.md !LICENSE !BUILDSCRIPT.sh !BUILDSCRIPT.bash !CMakeLists.txt !signalbackup-tools.rb !*.cc !*.h !*.ih !*/ !*/*.cc !*/*.h !*/*.ih tests !.github/* !.github/workflows/*.yml !.gitignore signalbackup-tools-20250313-1/BUILDSCRIPT.bash000077500000000000000000000450321476450434500203600ustar00rootroot00000000000000#!/bin/bash CONFIG="${CONFIG:-default}" while [ $# -gt 0 ] ; do if [ "$1" = "--config" ] && [ $# -gt 1 ] ; then CONFIG="$2" shift fi shift done if ! command -v nproc &> /dev/null ; then NUMPROCS="${NUMPROCS:-1}" else NUMPROCS="${NUMPROCS:-$(nproc)}" fi if [ "$CONFIG" = "default" ] ; then if ! command -v pkg-config --cflags dbus-1 &> /dev/null ; then PKG_CONFIG___CFLAGS_DBUS__="-I/usr/include/dbus-1.0 -I/usr/lib/dbus-1.0/include" else PKG_CONFIG___CFLAGS_DBUS__=$(pkg-config --cflags dbus-1) fi if ! command -v pkg-config --libs dbus-1 &> /dev/null ; then PKG_CONFIG___LIBS_DBUS__="-ldbus-1" else PKG_CONFIG___LIBS_DBUS__=$(pkg-config --libs dbus-1) fi fi CXX="${CXX:-g++}" CXXFLAGS="${CXXFLAGS:--Wall -Wextra -Woverloaded-virtual -Wshadow -pedantic -O3 -flto $PKG_CONFIG___CFLAGS_DBUS__}" CXXARCH="${CXXARCH:--march=native}" CXXSTD="${CXXSTD:--std=c++2b}" CXXFLAGSEXTRA="${CXXFLAGSEXTRA:-}" LDFLAGS="${LDFLAGS:--Wall -Wextra -Wl,--as-needed -Wl,-z,now -O3 -flto=auto -s}" LDLIBS="${LDLIBS:-$PKG_CONFIG___LIBS_DBUS__ -lcrypto -lsqlite3}" BIN="${BIN:-signalbackup-tools}" # CONFIG: without_dbus if [ "$CONFIG" = "without_dbus" ] ; then CXXFLAGS="-Wall -Wextra -Woverloaded-virtual -Wshadow -pedantic -DWITHOUT_DBUS -O3 -flto" LDLIBS="-lcrypto -lsqlite3" fi SRC=("keyvalueframe/statics.cc" "signalbackup/tgmapcontacts.cc" "signalbackup/tgbuildbody.cc" "signalbackup/checkdbintegrity.cc" "signalbackup/mergegroups.cc" "signalbackup/writeencryptedframe.cc" "signalbackup/scanself.cc" "signalbackup/applyranges.cc" "signalbackup/prepareoutputdirectory.cc" "signalbackup/scramble.cc" "signalbackup/htmlescapestring.cc" "signalbackup/cleandatabasebymessages.cc" "signalbackup/setminimumid.cc" "signalbackup/croptodates.cc" "signalbackup/datetomsecssinceepoch.cc" "signalbackup/getavatarextension.cc" "signalbackup/updategroupmembers.cc" "signalbackup/dtimportlongtext.cc" "signalbackup/exporttofile.cc" "signalbackup/setcolumnnames.cc" "signalbackup/handledtgroupchangemessage.cc" "signalbackup/tgimportmessages.cc" "signalbackup/htmlwritecalllinkdiv.cc" "signalbackup/setlongmessagebody.cc" "signalbackup/dtinsertreactions.cc" "signalbackup/htmlwritefullcontacts.cc" "signalbackup/dtupdateprofile.cc" "signalbackup/insertrow.cc" "signalbackup/getgroupv1migrationrecipients.cc" "signalbackup/htmlwriteblockedlist.cc" "signalbackup/listrecipients.cc" "signalbackup/importwachat.cc" "signalbackup/dumpmedia.cc" "signalbackup/makeidsunique.cc" "signalbackup/dtimportstickerpacks.cc" "signalbackup/getallthreadrecipients.cc" "signalbackup/croptothread.cc" "signalbackup/initfromdir.cc" "signalbackup/fillthreadtablefrommessages.cc" "signalbackup/dtsetavatar.cc" "signalbackup/dtinsertattachments.cc" "signalbackup/unescapexmlstring.cc" "signalbackup/getrecipientidfrom.cc" "signalbackup/compactids.cc" "signalbackup/importfromdesktop.cc" "signalbackup/htmlwritecalllog.cc" "signalbackup/exporttxt.cc" "signalbackup/getdtreactions.cc" "signalbackup/signalbackup.cc" "signalbackup/decodestatusmessage.cc" "signalbackup/htmlwriteavatar.cc" "signalbackup/scanmissingattachments.cc" "signalbackup/statics_html.cc" "signalbackup/exporttodir.cc" "signalbackup/getfreedateformessage.cc" "signalbackup/getrecipientidfrommapped.cc" "signalbackup/statics_linkify.cc" "signalbackup/setfiletimestamp.cc" "signalbackup/exportcsv.cc" "signalbackup/setchatcolor.cc" "signalbackup/utf16tounicodecodepoint.cc" "signalbackup/handledtgroupv1migration.cc" "signalbackup/migratedatabase.cc" "signalbackup/listthreads.cc" "signalbackup/htmlgetemojipos.cc" "signalbackup/updatethreadsentries.cc" "signalbackup/getcustomcolor.cc" "signalbackup/updaterecipientid.cc" "signalbackup/updatesnippetextrasrecipient.cc" "signalbackup/getgroupinfo.cc" "signalbackup/deleteattachments.cc" "signalbackup/getgroupmembers.cc" "signalbackup/customs.cc" "signalbackup/getminmaxusedid.cc" "signalbackup/reordermmssmsids.cc" "signalbackup/importcsv.cc" "signalbackup/handledtcalltypemessage.cc" "signalbackup/htmlwriterevision.cc" "signalbackup/statics.cc" "signalbackup/getnamefromrecipientid.cc" "signalbackup/makeprintable.cc" "signalbackup/htmlwritemsgreceiptinfo.cc" "signalbackup/updaterows.cc" "signalbackup/importthread.cc" "signalbackup/htmlwritesettings.cc" "signalbackup/htmllinkify.cc" "signalbackup/htmlprepmsgbody.cc" "signalbackup/decodeprofilechangemessage.cc" "signalbackup/remaprecipients.cc" "signalbackup/htmlwriteindex.cc" "signalbackup/dumpavatars.cc" "signalbackup/summarize.cc" "signalbackup/ptcreaterecipient.cc" "signalbackup/removedoubles.cc" "signalbackup/getthreadidfromrecipient.cc" "signalbackup/cleanattachments.cc" "signalbackup/exporthtml.cc" "signalbackup/buildsqlstatementframe.cc" "signalbackup/htmlescapeurl.cc" "signalbackup/gettranslatedname.cc" "signalbackup/importtelegramjson.cc" "signalbackup/escapexmlstring.cc" "signalbackup/tgsetquote.cc" "signalbackup/migrate_to_191.cc" "signalbackup/htmlpreplinkpreviewdescription.cc" "signalbackup/utf8bytestohexstring.cc" "signalbackup/handlewamessage.cc" "signalbackup/addsmsmessage.cc" "signalbackup/tgsetbodyranges.cc" "signalbackup/handledtexpirationchangemessage.cc" "signalbackup/htmlwritestickerpacks.cc" "signalbackup/dtsetsharedcontactsjsonstring.cc" "signalbackup/htmlwrite.cc" "signalbackup/initfromfile.cc" "signalbackup/exportbackup.cc" "signalbackup/makefilenameunique.cc" "signalbackup/sanitizefilename.cc" "signalbackup/importfromplaintextbackup.cc" "signalbackup/mergerecipients.cc" "signalbackup/htmlwritesearchpage.cc" "signalbackup/missingattachmentexpected.cc" "signalbackup/dtcreaterecipient.cc" "signalbackup/getgroupupdaterecipients.cc" "signalbackup/dumpinfoonbadframe.cc" "signalbackup/dropbadframes.cc" "signalbackup/dtsetcolumnnames.cc" "signalbackup/tgsetattachment.cc" "signalbackup/exportxml.cc" "signalbackup/statics_emoji.cc" "signalbackup/updategv1migrationmessage.cc" "signalbackup/updatereactionauthors.cc" "signalbackup/setrecipientinfo.cc" "signalbackup/unicodetoutf8.cc" "signalbackup/findrecipient.cc" "signalbackup/updateavatars.cc" "signalbackup/htmlwriteattachment.cc" "signalbackup/dtsetmessagedeliveryreceipts.cc" "attachmentframe/statics.cc" "logger/isterminal.cc" "logger/supportsansi.cc" "logger/statics.cc" "logger/outputhead.cc" "mimetypes/statics.cc" "databaseversionframe/statics.cc" "xmldocument/xmldocument.cc" "endframe/statics.cc" "sqlcipherdecryptor/destructor.cc" "sqlcipherdecryptor/gethmackey.cc" "sqlcipherdecryptor/sqlcipherdecryptor.cc" "sqlcipherdecryptor/decryptdata.cc" "framewithattachment/setattachmentdata.cc" "sharedprefframe/statics.cc" "avatarframe/statics.cc" "sqlstatementframe/statics.cc" "sqlstatementframe/buildstatement.cc" "signalplaintextbackupdatabase/signalplaintextbackupdatabase.cc" "backupframe/init.cc" "desktopattachmentreader/getattachmentdata.cc" "memfiledb/statics.cc" "sqlitedb/valueasstring.cc" "sqlitedb/prettyprint.cc" "sqlitedb/databasewriteversion.cc" "sqlitedb/renamecolumn.cc" "sqlitedb/availablewidth.cc" "sqlitedb/removecolumn.cc" "sqlitedb/valueasint.cc" "sqlitedb/copydb.cc" "sqlitedb/printsingleline.cc" "sqlitedb/print.cc" "sqlitedb/printlinemode.cc" "stickerframe/statics.cc" "csvreader/readrow.cc" "csvreader/read.cc" "main.cc" "headerframe/statics.cc" "desktopdatabase/getkeyfromencrypted.cc" "desktopdatabase/decryptkey_mac_linux.cc" "desktopdatabase/init.cc" "desktopdatabase/getkey.cc" "desktopdatabase/getkeyfromencrypted_win.cc" "desktopdatabase/readencryptedkey.cc" "desktopdatabase/getsecrets_mac.cc" "desktopdatabase/getsecrets_linux_secretservice.cc" "desktopdatabase/getsecrets_linux_kwallet.cc" "desktopdatabase/getkeyfromencrypted_mac_linux.cc" "reactionlist/setauthor.cc" "jsondatabase/jsondatabase.cc" "attachmentmetadata/getattachmentmetadata.cc" "fileencryptor/init.cc" "fileencryptor/encryptframe.cc" "fileencryptor/fileencryptor.cc" "fileencryptor/encryptattachment.cc" "filedecryptor/getframe.cc" "filedecryptor/getframebrute.cc" "filedecryptor/filedecryptor.cc" "filedecryptor/customs.cc" "filedecryptor/initbackupframe.cc" "arg/usage.cc" "arg/arg.cc" "cryptbase/getbackupkey.cc" "cryptbase/getcipherandmac.cc") OBJ=("keyvalueframe/o/statics.o" "signalbackup/o/tgmapcontacts.o" "signalbackup/o/tgbuildbody.o" "signalbackup/o/checkdbintegrity.o" "signalbackup/o/mergegroups.o" "signalbackup/o/writeencryptedframe.o" "signalbackup/o/scanself.o" "signalbackup/o/applyranges.o" "signalbackup/o/prepareoutputdirectory.o" "signalbackup/o/scramble.o" "signalbackup/o/htmlescapestring.o" "signalbackup/o/cleandatabasebymessages.o" "signalbackup/o/setminimumid.o" "signalbackup/o/croptodates.o" "signalbackup/o/datetomsecssinceepoch.o" "signalbackup/o/getavatarextension.o" "signalbackup/o/updategroupmembers.o" "signalbackup/o/dtimportlongtext.o" "signalbackup/o/exporttofile.o" "signalbackup/o/setcolumnnames.o" "signalbackup/o/handledtgroupchangemessage.o" "signalbackup/o/tgimportmessages.o" "signalbackup/o/htmlwritecalllinkdiv.o" "signalbackup/o/setlongmessagebody.o" "signalbackup/o/dtinsertreactions.o" "signalbackup/o/htmlwritefullcontacts.o" "signalbackup/o/dtupdateprofile.o" "signalbackup/o/insertrow.o" "signalbackup/o/getgroupv1migrationrecipients.o" "signalbackup/o/htmlwriteblockedlist.o" "signalbackup/o/listrecipients.o" "signalbackup/o/importwachat.o" "signalbackup/o/dumpmedia.o" "signalbackup/o/makeidsunique.o" "signalbackup/o/dtimportstickerpacks.o" "signalbackup/o/getallthreadrecipients.o" "signalbackup/o/croptothread.o" "signalbackup/o/initfromdir.o" "signalbackup/o/fillthreadtablefrommessages.o" "signalbackup/o/dtsetavatar.o" "signalbackup/o/dtinsertattachments.o" "signalbackup/o/unescapexmlstring.o" "signalbackup/o/getrecipientidfrom.o" "signalbackup/o/compactids.o" "signalbackup/o/importfromdesktop.o" "signalbackup/o/htmlwritecalllog.o" "signalbackup/o/exporttxt.o" "signalbackup/o/getdtreactions.o" "signalbackup/o/signalbackup.o" "signalbackup/o/decodestatusmessage.o" "signalbackup/o/htmlwriteavatar.o" "signalbackup/o/scanmissingattachments.o" "signalbackup/o/statics_html.o" "signalbackup/o/exporttodir.o" "signalbackup/o/getfreedateformessage.o" "signalbackup/o/getrecipientidfrommapped.o" "signalbackup/o/statics_linkify.o" "signalbackup/o/setfiletimestamp.o" "signalbackup/o/exportcsv.o" "signalbackup/o/setchatcolor.o" "signalbackup/o/utf16tounicodecodepoint.o" "signalbackup/o/handledtgroupv1migration.o" "signalbackup/o/migratedatabase.o" "signalbackup/o/listthreads.o" "signalbackup/o/htmlgetemojipos.o" "signalbackup/o/updatethreadsentries.o" "signalbackup/o/getcustomcolor.o" "signalbackup/o/updaterecipientid.o" "signalbackup/o/updatesnippetextrasrecipient.o" "signalbackup/o/getgroupinfo.o" "signalbackup/o/deleteattachments.o" "signalbackup/o/getgroupmembers.o" "signalbackup/o/customs.o" "signalbackup/o/getminmaxusedid.o" "signalbackup/o/reordermmssmsids.o" "signalbackup/o/importcsv.o" "signalbackup/o/handledtcalltypemessage.o" "signalbackup/o/htmlwriterevision.o" "signalbackup/o/statics.o" "signalbackup/o/getnamefromrecipientid.o" "signalbackup/o/makeprintable.o" "signalbackup/o/htmlwritemsgreceiptinfo.o" "signalbackup/o/updaterows.o" "signalbackup/o/importthread.o" "signalbackup/o/htmlwritesettings.o" "signalbackup/o/htmllinkify.o" "signalbackup/o/htmlprepmsgbody.o" "signalbackup/o/decodeprofilechangemessage.o" "signalbackup/o/remaprecipients.o" "signalbackup/o/htmlwriteindex.o" "signalbackup/o/dumpavatars.o" "signalbackup/o/summarize.o" "signalbackup/o/ptcreaterecipient.o" "signalbackup/o/removedoubles.o" "signalbackup/o/getthreadidfromrecipient.o" "signalbackup/o/cleanattachments.o" "signalbackup/o/exporthtml.o" "signalbackup/o/buildsqlstatementframe.o" "signalbackup/o/htmlescapeurl.o" "signalbackup/o/gettranslatedname.o" "signalbackup/o/importtelegramjson.o" "signalbackup/o/escapexmlstring.o" "signalbackup/o/tgsetquote.o" "signalbackup/o/migrate_to_191.o" "signalbackup/o/htmlpreplinkpreviewdescription.o" "signalbackup/o/utf8bytestohexstring.o" "signalbackup/o/handlewamessage.o" "signalbackup/o/addsmsmessage.o" "signalbackup/o/tgsetbodyranges.o" "signalbackup/o/handledtexpirationchangemessage.o" "signalbackup/o/htmlwritestickerpacks.o" "signalbackup/o/dtsetsharedcontactsjsonstring.o" "signalbackup/o/htmlwrite.o" "signalbackup/o/initfromfile.o" "signalbackup/o/exportbackup.o" "signalbackup/o/makefilenameunique.o" "signalbackup/o/sanitizefilename.o" "signalbackup/o/importfromplaintextbackup.o" "signalbackup/o/mergerecipients.o" "signalbackup/o/htmlwritesearchpage.o" "signalbackup/o/missingattachmentexpected.o" "signalbackup/o/dtcreaterecipient.o" "signalbackup/o/getgroupupdaterecipients.o" "signalbackup/o/dumpinfoonbadframe.o" "signalbackup/o/dropbadframes.o" "signalbackup/o/dtsetcolumnnames.o" "signalbackup/o/tgsetattachment.o" "signalbackup/o/exportxml.o" "signalbackup/o/statics_emoji.o" "signalbackup/o/updategv1migrationmessage.o" "signalbackup/o/updatereactionauthors.o" "signalbackup/o/setrecipientinfo.o" "signalbackup/o/unicodetoutf8.o" "signalbackup/o/findrecipient.o" "signalbackup/o/updateavatars.o" "signalbackup/o/htmlwriteattachment.o" "signalbackup/o/dtsetmessagedeliveryreceipts.o" "attachmentframe/o/statics.o" "logger/o/isterminal.o" "logger/o/supportsansi.o" "logger/o/statics.o" "logger/o/outputhead.o" "mimetypes/o/statics.o" "databaseversionframe/o/statics.o" "xmldocument/o/xmldocument.o" "endframe/o/statics.o" "sqlcipherdecryptor/o/destructor.o" "sqlcipherdecryptor/o/gethmackey.o" "sqlcipherdecryptor/o/sqlcipherdecryptor.o" "sqlcipherdecryptor/o/decryptdata.o" "framewithattachment/o/setattachmentdata.o" "sharedprefframe/o/statics.o" "avatarframe/o/statics.o" "sqlstatementframe/o/statics.o" "sqlstatementframe/o/buildstatement.o" "signalplaintextbackupdatabase/o/signalplaintextbackupdatabase.o" "backupframe/o/init.o" "desktopattachmentreader/o/getattachmentdata.o" "memfiledb/o/statics.o" "sqlitedb/o/valueasstring.o" "sqlitedb/o/prettyprint.o" "sqlitedb/o/databasewriteversion.o" "sqlitedb/o/renamecolumn.o" "sqlitedb/o/availablewidth.o" "sqlitedb/o/removecolumn.o" "sqlitedb/o/valueasint.o" "sqlitedb/o/copydb.o" "sqlitedb/o/printsingleline.o" "sqlitedb/o/print.o" "sqlitedb/o/printlinemode.o" "stickerframe/o/statics.o" "csvreader/o/readrow.o" "csvreader/o/read.o" "o/main.o" "headerframe/o/statics.o" "desktopdatabase/o/getkeyfromencrypted.o" "desktopdatabase/o/decryptkey_mac_linux.o" "desktopdatabase/o/init.o" "desktopdatabase/o/getkey.o" "desktopdatabase/o/getkeyfromencrypted_win.o" "desktopdatabase/o/readencryptedkey.o" "desktopdatabase/o/getsecrets_mac.o" "desktopdatabase/o/getsecrets_linux_secretservice.o" "desktopdatabase/o/getsecrets_linux_kwallet.o" "desktopdatabase/o/getkeyfromencrypted_mac_linux.o" "reactionlist/o/setauthor.o" "jsondatabase/o/jsondatabase.o" "attachmentmetadata/o/getattachmentmetadata.o" "fileencryptor/o/init.o" "fileencryptor/o/encryptframe.o" "fileencryptor/o/fileencryptor.o" "fileencryptor/o/encryptattachment.o" "filedecryptor/o/getframe.o" "filedecryptor/o/getframebrute.o" "filedecryptor/o/filedecryptor.o" "filedecryptor/o/customs.o" "filedecryptor/o/initbackupframe.o" "arg/o/usage.o" "arg/o/arg.o" "cryptbase/o/getbackupkey.o" "cryptbase/o/getcipherandmac.o") num_jobs=${#SRC[@]} ## "wait -n" was introduced in bash 4.3 ## "${parameter@P}" was introduced in bash 4.4 (2016) ## both used in parallel for-loop # check we are using bash, and it is >= 4.4 if [ ! -z ${BASH+x} ] && ( { [ "${BASH_VERSINFO[0]}" -ge 4 ] && [ "${BASH_VERSINFO[1]}" -ge 4 ] ; } || [ "${BASH_VERSINFO[0]}" -gt 4 ]) ; then RUNNINGJOBS="\j" # The prompt escape for number of jobs currently running ### echo "${RUNNINGJOBS}" // parameter expansion ### \j ### echo "${RUNNINGJOBS@P}" // prompt expansion ### 0 // for example) for (( i = 0; i < num_jobs; i++ )); do while (( ${RUNNINGJOBS@P} >= NUMPROCS )); do wait -n done mkdir -p $(dirname "${OBJ[$i]}") if [ ! "${OBJ[$i]}" -nt "${SRC[$i]}" ] ; then echo "$CXX -c $CXXFLAGS $CXXFLAGSEXTRA $CXXARCH $CXXSTD ${SRC[$i]} -o ${OBJ[$i]}" $CXX -c $CXXFLAGS $CXXFLAGSEXTRA $CXXARCH $CXXSTD "${SRC[$i]}" -o "${OBJ[$i]}" & fi done wait else for (( i = 0; i < num_jobs; i++ )); do threads_free=$((num_jobs - i)) if [[ "$threads_free" -lt "$NUMPROCS" ]] ; then threads=$threads_free else threads=$NUMPROCS fi for (( t = 0; t < threads; t++ )); do ( curjob=$((i + t)) mkdir -p $(dirname "${OBJ[$curjob]}") if [ ! "${OBJ[$curjob]}" -nt "${SRC[$curjob]}" ] ; then echo "$CXX -c $CXXFLAGS $CXXFLAGSEXTRA $CXXARCH $CXXSTD ${SRC[$curjob]} -o ${OBJ[$curjob]}" $CXX -c $CXXFLAGS $CXXFLAGSEXTRA $CXXARCH $CXXSTD "${SRC[$curjob]}" -o "${OBJ[$curjob]}" fi ) & done wait i=$((i + t - 1)) done fi # Linking echo "$CXX ${OBJ[@]} $LDFLAGS -o $BIN $LDLIBS" $CXX "${OBJ[@]}" $LDFLAGS -o "$BIN" $LDLIBS signalbackup-tools-20250313-1/CMakeLists.txt000066400000000000000000000033501476450434500204670ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.14) if (APPLE) foreach (HOMEBREW_PKG openssl sqlite) execute_process(COMMAND brew --prefix ${HOMEBREW_PKG} OUTPUT_VARIABLE HOMEBREW_PREFIX OUTPUT_STRIP_TRAILING_WHITESPACE) list(APPEND CMAKE_PREFIX_PATH "${HOMEBREW_PREFIX}") endforeach () endif () project(signalbackup-tools) find_package(OpenSSL REQUIRED) find_package(SQLite3 REQUIRED) set(CMAKE_CXX_EXTENSIONS off) if (CMAKE_VERSION VERSION_LESS "3.30") # the CMAKE_CXX_STANDARD_LATEST variable was only introduced in 3.30 set(CMAKE_CXX_STANDARD 20) elseif(CMAKE_CXX_STANDARD_LATEST) # if cmake version is ok and the variable is set, use it set(CMAKE_CXX_STANDARD ${CMAKE_CXX_STANDARD_LATEST}) endif() set(CMAKE_CXX_STANDARD_REQUIRED on) # find and set DBUS include and library paths if (${CMAKE_SYSTEM_NAME} MATCHES "Linux") if (WITHOUT_DBUS) add_definitions(-DWITHOUT_DBUS) else() find_package(PkgConfig REQUIRED) pkg_check_modules(DBUS REQUIRED dbus-1) include_directories(${DBUS_INCLUDE_DIRS}) set(DBUS_LIBS_ABSOLUTE) foreach(lib ${DBUS_LIBRARIES}) set(tmp DBUS_${lib}_ABS) find_library(${tmp} ${lib} ${DBUS_LIBRARY_DIR}) list(APPEND DBUS_LIBS_ABSOLUTE ${${tmp}}) endforeach() endif() endif() if (APPLE) find_library(SECLIB Security) if (NOT SECLIB) message(FATAL_ERROR "Failed to find required framework 'Security'") endif() find_library(CFLIB CoreFoundation) if (NOT CFLIB) message(FATAL_ERROR "Failed to find required framework 'CoreFoundation'") endif() endif() file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS *.cc *.h) add_executable(signalbackup-tools ${SOURCES}) target_link_libraries(signalbackup-tools OpenSSL::Crypto SQLite::SQLite3 ${SECLIB} ${CFLIB} ${DBUS_LIBS_ABSOLUTE}) signalbackup-tools-20250313-1/LICENSE000066400000000000000000001045151476450434500167410ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . signalbackup-tools-20250313-1/README.md000066400000000000000000001752411476450434500172170ustar00rootroot00000000000000![Linux build test](https://github.com/bepaald/signalbackup-tools/actions/workflows/test-linux-build.yml/badge.svg) ![macOs build test](https://github.com/bepaald/signalbackup-tools/actions/workflows/test-macos-build.yml/badge.svg) # signalbackup-tools Tool to work with backup files generated by the Signal Android application (https://signal.org/). The tool is provided as-is, there may be bugs. The tool and I are not affiliated with or endorsed by the Signal Foundation. - [Important Note](#important-note) - [Requirements](#requirements) - [Obtaining](#obtaining) - [Windows binary](#windows) - [macOS](#macos) - [Linux packages](#linux_packages) - [Compiling](#compiling) - [Running](#running) - [Dump decrypted database to disk](#dump) - [Dump media to disk](#dumpmedia) - [Fixing broken backups](#fix) - [Export HTML, TXT, CSV & XML](#export) - [Cropping to certain conversations or dates](#crop) - [Merging backups](#merge) - [Importing conversations from Signal-Desktop](#desktop) - [Importing conversations from Telegram / JSON file](#json) - [Deleting/Replacing attachments](#deleting_attachments) - [Operations for Signal Desktop](#desktop_functions) - [Various](#various) - [Advanced options](#advanced) - [Future plans](#future-plans) - [Donate](#donate) ### Important Note Signal is an actively developed application and consequently, the database format changes regularly. Often the changes do not affect the backup file format or the working of this program, but every once in a while a change does break (some of) the functionality of this program. It has happened before and it will happen again. Sometimes I fix it within hours, but when I am short on time, it may take a little longer. Any breakage will be dealt with as soon as I have some spare time. ### Requirements To compile this project, current stable released versions of the following are needed: - A C++ compiler supporting at least the C++17 standard (tested with [GCC](https://gcc.gnu.org) 14.2.1 and [Clang](https://clang.llvm.org) 18.1.8, also tested and working with a few older compiler versions) - [OpenSSL](https://www.openssl.org/) (any reasonably recent version from either the 3.X or 1.1x series) - [SQLite3](https://www.sqlite.org/) (any reasonably recent version) - Only on Linux: [dbus](https://www.freedesktop.org/wiki/Software/dbus/). Optional, but required by default. See the [compiling](#compiling) section to build without `dbus`. If the program is compiled without `dbus`, operations that need to open the Signal Desktop client database will not work unless the decrypted encryption key is manually provided. ### Obtaining **Windows binary** For the most recent Windows executable, check [the releases page](https://github.com/bepaald/signalbackup-tools/releases). This executable is a static build, cross compiled from my Arch Linux system. It is only minimally tested, but generally appears to work just fine. Note for Windows users: this is a command line application. This means you can not just double-click the executable to run it, you need to run it from a terminal. Common terminals for Windows are `cmd` (Command Prompt) and `PowerShell`. An example of running the program on Windows 10 can be seen [here](https://github.com/bepaald/signalbackup-tools/issues/148#issuecomment-1732375861). **Linux packages** - For **Arch** users, an AUR package [is available](https://aur.archlinux.org/packages/signalbackup-tools-git). - A pre-built rpm for **openSUSE** is available [here](https://software.opensuse.org/package/signalbackup-tools?search_term=signalbackup-tools), thanks to @marfrh (https://github.com/bepaald/signalbackup-tools/issues/205). - The program is also available in `nixpkgs` as [`signalbackup-tools`](https://search.nixos.org/packages?channel=unstable&type=packages&query=signalbackup-tools) and can be installed on **NixOS** or any system that supports the [Nix package manager](https://nixos.org/manual/nix/stable/). For those looking for more information on installing and running the Nix package, or those wanting to help others, there is an issue where information can be found and posted [here](https://github.com/bepaald/signalbackup-tools/issues/149). - Alternatively, a Dockerfile has been kindly provided by David J. Meier, and is available at his gitlab page: . **macOS** A homebrew formula is provided in [homebrew/signalbackup-tools.rb](https://raw.githubusercontent.com/bepaald/signalbackup-tools/master/homebrew/signalbackup-tools.rb). Download this file to your machine. Then, on modern macOS versions, with [homebrew set up](https://brew.sh/), compiling should be as simple as running `brew install --HEAD --formula [path/to/signalbackup-tools.rb]`. Once installed, the program can be upgraded by running `brew upgrade --fetch-HEAD --formula signalbackup-tools`. Manually compiling should also be possible assuming the dependencies are installed, for more info see [here](https://github.com/bepaald/signalbackup-tools/issues/9), or more recently [here](https://github.com/bepaald/signalbackup-tools/issues/85). macOS users might also consider the aforementioned [Nix package](https://search.nixos.org/packages?channel=unstable&type=packages&query=signalbackup-tools). **Compiling** To compile the program, three main options are available: - CMake. Make sure to have `cmake` installed. On Linux this method also requires `pkg-config` (unless building without `dbus`). From the project directory, run: ```Shell $ cmake -B build -DCMAKE_BUILD_TYPE=Release $ cmake --build build -j $(nproc) ``` To build without `dbus` (and `pkg-config`), add `-DWITHOUT_DBUS=1` to the first command. - The bash script. In the project directory is a bash script `BUILDSCRIPT.bash`. Simply run it: ```Shell $ ./BUILDSCRIPT.bash ``` To build without `dbus`, add `--config without_dbus` to the above command. The script can of course be edited at will to change compilation behavior. The flags can also be changed on the command line when running, for example to build with `clang++` instead of `g++`, simply run `$ CXX=clang++ ./BUILDSCRIPT.bash`. - Manually. The program can be manually compiled simply by running `g++ -std=c++20 */*.cc *.cc -lcrypto -lsqlite3`. On linux, by default one needs to add the location of the dbus headers and libraries (simplest way, add: `$(pkg-config --cflags --libs dbus-1)`). Alternatively, to build on Linux without `dbus`, add `-DWITHOUT_DBUS=1`. On macOS, the program must be linked to the Security and CoreFoundation frameworks by adding `-framework Security -framework CoreFoundation` to the build command. Any compiler flags you feel useful can be added, personally I use at least `-O3 -Wall -Wextra`. When compiling with an old compiler version (gcc 8.x or clang <= 7), also add the -lstdc++fs flag and replace -std=c++20 with -std=c++17. ### Running > [!TIP] > In all examples below, one or more passphrases are provided on the command line. If so desired, these can be omitted in which case you are prompted for the passphrase at runtime. In its simplest form, this tool is run as such: ``` signalbackup-tools [input] [passphrase] ``` This will open the file `input` using the provided `passphrase`, and do nothing with it. If an output file is supplied the backup is written to that file: ``` signalbackup-tools [input] [passphrase] --output [output] ``` Optionally, a new passphrase can be provided for the output file with the option `--opassphrase`, or `-op` for short. If not provided, as in the above example, the input passphrase is used again. **Dump decrypted database to disk** The program can dump the decrypted backup components to a directory, or read the contents of a directory and pack and encrypt it back into a valid backup file. When dumping, make sure the directory to dump to is empty to start with. In theory, the decrypted files could be edited before re-encrypting. The tool can be called the same as above, except the output should be a directory: ``` signalbackup-tools [input] [passphrase] --output [outputdirectory] ``` To skip exporting media (like message attachments, avatars and stickers), add the option `--onlydb`. To re-encrypt the contents of a directory into a valid backup file, use the directory as `input` and provide the `--output` and `--opassphrase` options.
Example (click to show)

```Shell [~/programming/signalbackup-tools] $ mkdir RAWBACKUP [~/programming/signalbackup-tools] $ ll RAWBACKUP/ total 0 [~/programming/signalbackup-tools] $ ./signalbackup-tools ~/PHONE/signal-2019-07-14-06-59-26.backup 949543591444534240555456749437 --output RAWBACKUP/ IV: (hex:) 13 3f 94 13 be 5a 6d 1c 97 d0 20 88 4e f8 64 46 (size: 16) SALT: (hex:) 5e 89 ec d8 f3 99 68 5b 9b a6 8b d8 3b b7 7d 8f e5 6a 2a 03 bb 2c c0 b9 f6 a1 0e bc bf ba 1a 25 (size: 32) BACKUPKEY: (hex:) 38 4c a3 1c 17 9c f7 9b 27 30 98 bc 13 bf b6 5d 1d 90 df 13 c1 11 79 a4 ef d0 65 75 b9 55 cc 61 (size: 32) CIPHERKEY: (hex:) 25 15 18 5f ac 06 3f 13 b5 0d c6 eb 8b e0 84 34 13 3f 84 f7 77 9b f6 ec 44 00 cb c0 77 2d 70 1f (size: 32) MACKEY: (hex:) f3 00 34 77 1f a3 74 74 56 42 5e ad 6b d7 71 bf 40 7f e0 4f df 3a d1 1a 22 79 91 3a 97 73 88 28 (size: 32) COUNTER: 322933779 Reading backup file... FRAME 42337 (100.0%)... Read entire backup file... done! Writing HeaderFrame... Writing DatabaseVersionFrame... Writing Attachments... Writing Avatars... Writing SharedPrefFrame(s)... Writing StickerFrames... Writing EndFrame... [~/programming/signalbackup-tools] $ ll RAWBACKUP/ total 2204384 -rw-r--r-- 1 bepaald bepaald 118871 jul 19 15:40 Attachment_1000_1518474349909.bin -rw-r--r-- 1 bepaald bepaald 16 jul 19 15:40 Attachment_1000_1518474349909.sbf -rw-r--r-- 1 bepaald bepaald 30017 jul 19 15:40 Attachment_1001_1518475497752.bin [...] -rw-r--r-- 1 bepaald bepaald 9363456 jul 19 15:40 database.sqlite -rw-r--r-- 1 bepaald bepaald 4 jul 19 15:40 DatabaseVersion.sbf -rw-r--r-- 1 bepaald bepaald 2 jul 19 15:40 End.sbf -rw-r--r-- 1 bepaald bepaald 54 jul 19 15:40 Header.sbf -rw-r--r-- 1 bepaald bepaald 96 jul 19 15:40 SharedPreference_0.sbf -rw-r--r-- 1 bepaald bepaald 97 jul 19 15:40 SharedPreference_1.sbf [~/programming/signalbackup-tools] $ ./signalbackup-tools RAWBACKUP/ --output NEWBACKUPFILE --opassphrase 949023591444534240555368549425 Exporting backup to 'NEWBACKUPFILE' Writing HeaderFrame... Writing DatabaseVersionFrame... Writing SqlStatementFrame(s)... Dealing with table 'sms'... 34595/34595 entries...done Dealing with table 'mms'... 2370/2370 entries...done Dealing with table 'part'... 1934/1934 entries...done Dealing with table 'thread'... 29/29 entries...done Dealing with table 'identities'... 21/21 entries...done Dealing with table 'drafts'... 0/0 entries... Dealing with table 'push'... 0/0 entries... Dealing with table 'groups'... 10/10 entries...done Dealing with table 'recipient_preferences'... 67/67 entries...done Dealing with table 'group_receipts'... 1320/1320 entries...done Dealing with table 'job_spec'... 1/1 entries...done Dealing with table 'constraint_spec'... 0/0 entries... Dealing with table 'dependency_spec'... 0/0 entries... Dealing with table 'sticker'... 0/0 entries... Writing SharedPrefFrame(s)... Writing EndFrame... Done! [~/programming/signalbackup-tools] $ cmp ~/PHONE/signal-2019-07-14-06-59-26.backup NEWBACKUPFILE && echo "Files are identical" Files are identical [~/programming/signalbackup-tools] $ ``` _NOTE The original and new files are not actually guaranteed to be identical, it just so happens that in this case the AvatarFrames are read from the filesystem in the order they appeared in the original._

**Dump media to disk** ##### Dumping message attachments To only export media attachments from one or all of the threads in a backup, run with `--dumpmedia` as follows: ``` signalbackup-tools [input] [passphrase] --dumpmedia [outputdirectory] ``` Where `outputdirectory` is an empty directory, or does not exist (in which case it will be created). To limit the export to certain threads, the option `--limittothreads [LIST_OF_THREADS]` can be added. The list of threads can contain both ranges and comma separated values, e.g. `--limittothreads 1,2,3,8-16,20`. The thread numbers can be obtained from `--listthreads`. Additionally, threads can be identified by a string representing the display name, phone number or username of the recipient: `--limittothreadsbyname "Alice","Family Group","+14255550123"`. Similarly, the option `--limittodates [LIST_OF_DATES]` will limit the output to media from the time periods listed. For the format of the date list, see the [crop to dates](#crop) option. Normally, stickers are included in the media export, as they are normal attachments in the database. To prevent this, add the option `--excludestickers`. ##### Dumping avatars To only export avatars from one or all contacts in a backup, run with `--dumpavatars` as follows: ``` signalbackup-tools [input] [passphrase] --dumpavatars [outputdirectory] ``` Where `outputdirectory` is an empty directory, or does not exist (in which case it will be created). To limit the export to certain contacts, add the option `--limitcontacts [LIST_OF_CONTACTS]`. The list should look like this: `"Alice,Bob,John Doe(,...)"`, where each name is exactly as it appears in Signal's conversation overview or from this program's `--listrecipients` output. **Fixing broken backups** > [!IMPORTANT] > Around version 6.26 of Signal Android (circa July 2023), the backup format was changed in a way that makes it impossible to recover from data corruption that happens across frame boundaries. This functionality is disabled for newer backups. In other cases (corruption within a single frame, the occasional bug in Signal), part of the data could possibly still be recovered, though it might require a custom function. You could always open an issue if you need help. Note that this type of corruption, where only a single frame is affected, is rare and recent versions of Signal Android usually deal with this case quite well. At the moment it has been used successfully to fix backups that were corrupted for some reason (see https://github.com/signalapp/Signal-Android/issues/8355, and https://community.signalusers.org/t/tool-to-re-encrypt-signal-backup-optionally-changing-password-or-dropping-bad-frames/6497). If you want to fix a broken backup, run the tool as follows: ``` signalbackup-tools [input] [passphrase] --output [outputfile] (--opassphrase [newpassphrase]) ``` _NOTE: if the corruption happens outside of attachment data, which is usually unlikely, chances of recovery are much lower._ If the output passphrase is omitted, the input passphrase is used to encrypt the new backup file. If the 'input' is a directory, it is assumed to contain a decrypted dump of the backup (as made by this tool) and the input passphrase can be omitted. In this case the output passphrase is required, unless 'output' is also a directory. If the 'output' is omitted only the scan is done, and the broken message is identified, giving you the option to delete it from the phone. The corrupted attachment data is dumped to file.
Example (click to show)

```Shell [~/programming/signalbackup-tools] $ ./signalbackup-tools CORRUPTEDSIGNALBACKUPS/signal-2019-05-20-05-29-06.backup3 949543593573534240555368549437 --output NEWBACKUPFILE --opassphrase 949543593573534240555368549437 signalbackup-tools source version 20190926.164320 IV: (hex:) 12 16 72 95 7a 00 68 44 7e cf 7d 20 26 f9 d3 7d (size: 16) SALT: (hex:) cc 03 85 02 61 97 eb 5b ed 3e 05 00 c4 a8 77 40 28 08 aa 9f e5 a8 00 74 b4 f8 56 aa 24 57 a9 5d (size: 32) BACKUPKEY: (hex:) 8f ff df 2b 9f 96 73 9a 63 95 0f ea 3f b1 e5 a4 87 12 19 ca 93 31 86 2a 60 3f 41 ef 6d a4 08 44 (size: 32) CIPHERKEY: (hex:) ce 53 c1 f2 92 4b e3 b8 e1 56 85 61 14 96 82 8b 83 7f 07 21 83 52 1a c2 3f 6b 16 83 3e 33 94 a3 (size: 32) MACKEY: (hex:) c2 77 af 1e 4b 05 db 62 52 57 af 8a d6 a4 d4 e9 6c 93 53 81 9a e7 6f 12 2c ce 13 8f b3 5e 8d 3a (size: 32) COUNTER: 303461013 Reading backup file... FRAME 37636 (071.5%)... WARNING: Bad MAC in attachmentdata: theirMac: (hex:) 30 75 bb b3 fb 65 a5 2a 5f b5 ourMac: (hex:) ff f2 37 c1 f0 d4 2c 67 a3 cf 6c 41 55 bd 9c 1d 85 84 0e 66 96 ae 52 4e 90 b5 a3 37 33 3c b4 fc WARNING: Bad MAC in frame, trying to print frame info: Frame number: 37637 Type: ATTACHMENT - row id : 1317 (8 bytes) - attachment id : 1536842122829 (8 bytes) - length : 1516761 (8 bytes) - attachment : (hex:) 47 49 46 38 39 61 e0 01 09 01 f7 00 30 00 ff 00 01 00 02 01 00 05 01 00 05 ... (1516761 bytes total) Frame is attachment, it belongs to entry in the 'part' table of the database: - _id : 1317 - mid : 1552 - seq : 0 - ct : image/gif - name : (NULL) - chset : (NULL) - cd : (NULL) - fn : (NULL) - cid : (NULL) - cl : (NULL) - ctt_s : (NULL) - ctt_t : (NULL) - encrypted : (NULL) - pending_push : 0 - _data : /data/user/0/org.thoughtcrime.securesms/app_parts/part2625620938717109701.mms - data_size : 1516761 - file_name : (NULL) - thumbnail : (NULL) - aspect_ratio : 2 - unique_id : 1536842122829 - digest : (NULL) - fast_preflight_id : 5897879359555196456 - voice_note : 0 - data_random : (hex:) f7 1e 34 f3 ba 07 34 44 56 04 15 dc 80 88 b7 10 9e c1 18 80 65 c7 7f 60 d9 cc 0f c9 d4 95 ce b4 - thumbnail_random : (hex:) 14 f7 79 84 e5 a5 68 fe 98 a4 cb db 36 1f 6f c8 ca 3c 57 45 60 e2 d2 f2 f6 ee 42 71 42 7b 8e d7 - width : 480 - height : 265 - quote : 0 - caption : (NULL) Which belongs to entry in 'mms' table: - _id : 1552 - thread_id : 1 - date : 2018-09-13 14:35:22 +0200 (1536842122790) - date_received : 2018-09-13 14:35:22 +0200 (1536842122809) - msg_box : 10485783 - read : 1 - m_id : (NULL) - sub : (NULL) - sub_cs : (NULL) - body : - part_count : 1 - ct_t : (NULL) - ct_l : (NULL) - address : +316XXXXXXXX - address_device_id : (NULL) - exp : (NULL) - m_cls : (NULL) - m_type : 128 - v : (NULL) - m_size : (NULL) - pri : (NULL) - rr : (NULL) - rpt_a : (NULL) - resp_st : (NULL) - st : (NULL) - tr_id : (NULL) - retr_st : (NULL) - retr_txt : (NULL) - retr_txt_cs : (NULL) - read_status : (NULL) - ct_cls : (NULL) - resp_txt : (NULL) - d_tm : (NULL) - delivery_receipt_count : 1 - mismatched_identities : (NULL) - network_failures : (NULL) - d_rpt : (NULL) - subscription_id : -1 - expires_in : 0 - expire_started : 0 - notified : 0 - read_receipt_count : 0 - quote_id : 0 - quote_author : (NULL) - quote_body : (NULL) - quote_attachment : -1 - shared_contacts : (NULL) - quote_missing : 0 - unidentified : 0 - previews : (NULL) Trying to dump decoded attachment to file 'attachment_1552.bin' FRAME 37637 (071.6%)... Failed to read next frame (4294967295 bytes at filepos 1611402482) Starting bruteforcing offset to next valid frame... Checking offset 802590 bytes GOT GOOD MAC AT OFFSET 802591 BYTES! Now let's try and find out how many frames we skipped to get here.... Checking if we skipped 0 frames... nope! :( Checking if we skipped 1 frames... nope! :( Checking if we skipped 2 frames... nope! :( Checking if we skipped 3 frames... YEAH! Frame number: 37641 Type: SQLSTATEMENT - (statement: "INSERT INTO part VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" (83 bytes) - (uint64 parameter): "1319" - (uint64 parameter): "1554" - (uint64 parameter): "0" - (string parameter): "image/jpeg" - (bool parameter) : "true" (value: "1") - (bool parameter) : "true" (value: "1") - (bool parameter) : "true" (value: "1") - (bool parameter) : "true" (value: "1") - (bool parameter) : "true" (value: "1") - (bool parameter) : "true" (value: "1") - (bool parameter) : "true" (value: "1") - (bool parameter) : "true" (value: "1") - (bool parameter) : "true" (value: "1") - (uint64 parameter): "0" - (string parameter): "/data/user/0/org.thoughtcrime.securesms/app_parts/part7691613523019485618.mms" - (uint64 parameter): "133247" - (bool parameter) : "true" (value: "1") - (bool parameter) : "true" (value: "1") - (bool parameter) : "true" (value: "1") - (uint64 parameter): "1537091993419" - (bool parameter) : "true" (value: "1") - (bool parameter) : "true" (value: "1") - (uint64 parameter): "0" - (binary parameter): "(hex:) d3 a6 ea 3c 27 90 0f 12 74 71 54 ac 94 92 0f 08 30 04 e0 e1 b3 41 36 37 6d 8a 5d 44 fb 23 6e b5" - (bool parameter) : "true" (value: "1") - (uint64 parameter): "720" - (uint64 parameter): "1280" - (uint64 parameter): "0" - (bool parameter) : "true" (value: "1") Got frame, breaking FRAME 39960 (100.0%)... Read entire backup file... done! Removing 1 bad frames from database... Exporting backup to 'NEWBACKUPFILE' Writing HeaderFrame... Writing DatabaseVersionFrame... Writing SqlStatementFrame(s)... Dealing with table 'sms'... 32752/32752 entries...done Dealing with table 'mms'... 2212/2212 entries...done Dealing with table 'part'... 1814/1814 entries...done Dealing with table 'thread'... 27/27 entries...done Dealing with table 'identities'... 19/19 entries...done Dealing with table 'drafts'... 0/0 entries... Dealing with table 'push'... 0/0 entries... Dealing with table 'groups'... 10/10 entries...done Dealing with table 'recipient_preferences'... 63/63 entries...done Dealing with table 'group_receipts'... 1195/1195 entries...done Dealing with table 'job_spec'... 1/1 entries...done Dealing with table 'constraint_spec'... 0/0 entries... Dealing with table 'dependency_spec'... 0/0 entries... Writing SharedPrefFrame(s)... Writing EndFrame... Done! [~/programming/signalbackup-tools] $ ```

**Export HTML, TXT, CSV & XML** ##### Export to HTML _NOTE: Note that while the the generated HTML is heavily inspired by Signal's look it does not aim to be a perfect reproduction of it. The generated HTML and CSS are only tested on Firefox (but both pass W3C validation)._ To export your messages to HTML, use `--exporthtml [DIRECTORY]`. To limit the output to certain threads the option `--limittothreads [LIST_OF_THREADS]` can be added. The list of threads can contain both ranges and comma separated values, e.g. `--limittothreads 1,2,3,8-16,20`. The thread numbers can be obtained from `--listthreads`. Additionally, threads can be identified by a string representing the display name, phone number or username of the recipient: `--limittothreadsbyname "Alice","Family Group","+14255550123"`. Similarly, the option `--limittodates [LIST_OF_DATES]` will limit the output to messages within the time periods listed. For the format of the date list, see the [crop to dates](#crop) option. Because writing out all media files can be a long process, the option `--append` can be added to reuse any existing media files, only new media and the HTML-files will be rewritten. Example: ``` signalbackup-tools [input] [passphrase] --exporthtml [directory] ``` Because browsers may have difficulty loading an entire conversation if it consists of a large number of messages, the option `--split [N]` can be added to split the output HTML in multiple pages. The optional number `N` is the maximum number of messages on each generated page (default: 1000). Alternatively the option `--split-by [period]` will generate separate pages for each calender `[period]`. Currently supported periods are 'year', 'month', 'week', and 'day'. Note the `-split` and `--split-by` options are mutually exclusive. By default, the function will create a HTML page resembling Signal's dark mode. If you prefer a light theme, add the `--light` option. If you want to be able to switch between the two modes without generating a new HTML page, you could add the `--themeswitching` option to the command. This will add a button to switch themes. Be aware this causes the page to use JavaScript and cookies. Other options that can be used together with `--exporthtml`: - `--searchpage` Generates a page from which conversations can be searched. This page requires JavaScript and generates an extra file named `searchidx.js` in the directory to facilitate searching. - `--includecalllog` Generates a page showing the call-log. - `--stickerpacks` Generates an overview of installed and known stickerpacks. - `--includeblockedlist` Generates an overview of blocked contacts in the backup. - `--addexportdetails` Adds some meta information about the backup (like size, filename, and database version) and this tool to the generated pages when printing. - `--includesettings` Generates a page showing settings found in the backup file. - `--includefullcontactlist` Generates a page showing _all_ contacts present in the database, including contacts with whom no thread exists, who are blocked or hidden, or who appear in your system contact list and may not have Signal installed. - `--allhtmlpages` Enables all of the above options, plus `--themeswitching`. Any specific option can be excluded by adding `--no-(option)` after this option on the command line. - `--includereceipts` Adds available information from read/delivery receipts to outgoing messages as a popup when hovering the checkmarks. Be aware this has the potential to significantly slow down page loading for larger conversations. In this case it is recommended to also use the `--split [N]` option to limit the page size. - `--originalfilenames` By default, this tool uses a custom naming scheme for message attachments when exporting to HTML. With this option, the original filenames are used (if available). This option can not be used together with `--append`, and will only work with an empty output directory (or with `--overwrite`). - `--compactfilenames` Causes the tool to write (very) short filenames for the generated HTML pages. This option exists specifically for Windows users who might run into maximum path length limitations (which should be rare). - `--chatfolders` Generates a page for each chat folder in the input file. This option may interact poorly with the `--limittodates` and `--limittothreads` options. > [!NOTE] > A big thanks to [Gertjan van der Burg](https://github.com/GjjvdBurg)! While HTML export was always a planned feature of this program, it would not have happened this quickly without his project [signal2html](https://github.com/GjjvdBurg/signal2html). The HTML this function generates is modified from the template from his original project. > [!NOTE] > An experimental feature to export Signal Desktop data to HTML exists. See [Operations for Signal Desktop](#desktop_functions). ##### Export to TXT To export to plain text use `--exporttxt [DIRECTORY]`. Some data is omitted from this export, such as attachment data and quotes. To limit the output to certain threads the option `--limittothreads [LIST_OF_THREADS]` can be added. The list of threads can contain both ranges and comma separated values, e.g. `--limittothreads 1,2,3,8-16,20`. The thread numbers can be obtained from `--listthreads`. Additionally, threads can be specified by display name, phone number or username: `--limittothreadsbyname "Alice","Group Name","+14255550123"`. Similarly, the option `--limittodates [LIST_OF_DATES]` will limit the output to messages within the time periods listed. For the format of the date list, see the [crop to dates](#crop) option. Example: ``` signalbackup-tools [input] [passphrase] --exporttxt [directory] ``` The output will look something like this: ``` [2023-07-10 01:23] Where are you? [2023-07-10 01:25] I'm at the beach. [2023-07-10 01:26] *** sent "Signal-1.jpeg" [2023-07-10 01:27] Come home. You haven't washed the dishes. (Bob: 😮) ``` ##### Export to CSV To export the tables to a file of comma separated values (CSV), use `--exportcsv [table1]=[filename1],[table2]=[filename2],...`. To get all messages from the database, only the 'message' table needs to be exported. To get all messages out of older databases, the 'sms' and 'mms' tables need to be exported. ##### Export to XML To export to XML file, use `--exportxml [filename]`. The exported XML file is intended to be compatible with [SMS Backup & Restore](https://www.synctech.com.au/sms-backup-restore/)'s format (see the [schema](https://synctech.com.au/wp-content/uploads/2018/01/sms.xsd_.txt) and [description](https://www.synctech.com.au/sms-backup-restore/fields-in-xml-backup-files/)). It has been successfully used to import Signal messages into messaging apps on phones, and — when Signal still supported this — importing these SMS into Signal. This way some messages could be moved from Signal Android to Signal iOS (which does not currently support backups). The XML format (and SMS in general) does not support many features found in Signal (quotes, for example), so the exported file will not be a full representation of the backup's contents. The resulting XML file will likely be quite large, around 30% larger than the input backup file, due to the base64 encoding of attachments. A few things to keep in mind when using this function (also see [67#issuecomment-2572987061](https://github.com/bepaald/signalbackup-tools/issues/67#issuecomment-2572987061)): - The phone number is a required field of each message in the XML specification. In the past, phone numbers of all contacts were automatically shared in Signal, however in recent versions phone number sharing is optional. If a contact has no phone number associated, their messages are skipped when exporting to XML. To work around this, phone numbers can be set by adding the option `--runsqlquery "UPDATE recipient SET e164 = '+1234567890' WHERE _id = X"` to the export command. In this option, the `X` should be replaced by a recipients `_id` as reported by `--listrecipients`. The option can be repeated mutliple times to set phone numbers for all contacts. - While the function sets group titles to the `contact_name` field during export, this appears to be ignored by (at least some popular) messaging apps. These apps will group any messages in a single thread if their lists of recipients (= phone numbers) is equal. As a result, when multiple Signal groups have the exact same list of members, their messages will likely be merged into a single thread regardless of differing group titles. _NOTE: Over time changes in Signal's database format have broken specifically this feature multiple times. It is not very well tested and its current working status is not very well known._ **Cropping to certain conversations or dates** _NOTE: This feature is experimental (even more so than the others). I test it fairly well myself, but I have no knowledge of it being used by other people. If you use it, please let me know if it works for you._ ##### Crop to threads To crop a backup file to certain threads, run: ``` signalbackup-tools [input] [passphrase] --croptothreads [list-of-threads] --output [output] (--passphrase [newpassphrase]) ``` Where the list of threads are the ids as reported by `signalbackup-tools [input] [passphrase] --listthreads`. The list supports commas for single ids and hyphens for ranges, for example: `--croptothreads 1,2,5-8,10`. Additionally, threads can be specified by display name, phone number or username: `--croptothreadsbyname "Alice","Some Group","+14255550123"`. ##### Crop to dates To crop a backup file to certain dates, run: ``` signalbackup-tools [input] [passphrase] --croptodates begindate1,enddate2(,begindate2,enddate2(,...)) --output [output] (--opassphrase [newpassphrase]) ``` The 'begindate' and 'enddate' must always appear in pairs and can be either in "YYYY-MM-DD hh:mm:ss" format or as a single number of [milliseconds since epoch](https://en.wikipedia.org/wiki/Unix_time). For example, the following commands are equivalent (in my time zone) and both crop the database to the messages between Sept. 18 2019 and Sept 18 2020: `--croptodates "2019-09-18 00:00:00","2020-09-18 00:00:00"` or `--croptodates 1568761200000,1600383600000`. **Merging backups** _NOTE: Although this feature generally seems to work quite well, it requires constant maintenance to keep up with changes in Signal's internal database. You may encounter problems if this program happens to be slightly out of date when you run it. As always, feel free to open an issue to notify me of problems._ To merge two backups, the backups must be at compatible database versions. The database version can be found by running `signalbackup-tools [input] [passphrase] --listthreads`. Though many database versions work perfectly fine together, sometimes breaking changes are made. For example two databases at versions before and after 168 can not be merged successfully. Before opening an issue, if needed, import the backups into Signal and export them again to get them updated and at equal versions. To import all threads from a source database into a target (the 'input'), run: ``` signalbackup-tools [input] [passphrase] --importthreads ALL --source [second_database] --sourcepassphrase [passphrase] --output [output_file] (--opassphrase [output passphrase]) ``` As with all commands, if the optional output passphrase is omitted, the new backup file will have the same passphrase as the input backup file. It is recommended to use the larger (containing the most data (contacts, threads,...)) as the 'input' and the smaller one as the source. If not all threads should be imported from the source, a list of thread ids can be supplied (e.g. `--importthreads 1,2,3,8-16,20`). The thread ids can be determined from the output of `--listthreads`. Threads can additionally be specified by display name, phone number or username by using `--importthreadsbyname "Bob","Family Group","+14255550123"`. Note this function does not automatically discard duplicate messages. If the backups you are merging contain (partly) the same messages — for example if they originate from some common backup/installation — you will probably want to [crop the source backup by date](#crop) before merging so it only contains messages not present in the target. For newer databases, omitting this step will cause errors, as Signal does not allow duplicate messages in its database anymore. If you use this option and read this line, I would really appreciate it if you let me know the results. Either send me a mail (basjetimmer at yahoo-dot-com) or feel free to just open an issue on the tracker for feedback. **Importing conversations from Signal-Desktop** > [!IMPORTANT] > This feature is highly experimental, problems may occur. Make sure to always keep a copy of your original backup file. Feedback is appreciated > [!NOTE] > While this program will compile and work with almost any version of SQLite3, this specific feature requires that the SQLite3 version used is not too far behind the one used by the Signal Desktop client. Older versions may not be able to read Signal Desktop's database. For example, as of writing, the version available in Ubuntu is too old to read Signal Desktop's database. For Ubuntu(-like) distributions a PPA exists with a more up-to-date version [here](https://launchpad.net/~linuxgndu/+archive/ubuntu/sqlitebrowser) (disclaimer: I am not affiliated with this PPA, and never used it). To import conversations from a Signal-Desktop installation, run: ``` signalbackup-tools [input] [passphrase] --importfromdesktop --output [output] (--opassphrase [newpassphrase]) ``` As with all commands this program supports, `[input]` is an existing Signal Android backup file. The messages from the desktop are imported into this backup file. Make sure your Signal-Desktop instance is cleanly shut down before running, if this fails for some reason the option `--ignorewal` can be added (the program will warn about this and exit if necessary), but this may cause the database to appear in an out-of-date state. This function requires some files belonging to your Signal Desktop installation: `config.json` and `sql/db.sqlite`. It tries to locate them at their default location (Linux: `~/.config/Signal/`, macOS: `~/Library/Application Support/Signal/`, Windows: `C:/Users//AppData/Roaming/Signal/`). If this fails, the default location for Signal Beta is attempted. In some cases one may want to specify the location this tool should look for the files. For example if wanting to work with the Signal Desktop Beta data, while the non-Beta is also present (it would be found first), or the files are in some non standard location (a backup for example). In such a case, the directory containing the files (_not_ the files themselves) can be passed by using `--desktopdir `. To limit the message import to a certain time frame, the option `--limittodates ` can be added. The format of the list of dates is identical to that of the [croptodates function](#crop-to-dates). In most cases, the option `--autolimitdates` can be used to automatically only import messages from the Desktop database before the first, or after the last message in the input backup. This function has some limitations, most notably the contacts referenced in the data that is to be imported must be present in the Android backup. If a message is found that is sent by/to an unknown contact, it is skipped. For other limitations see [here](https://github.com/bepaald/signalbackup-tools/issues/57#issuecomment-1329475240). **NOTE:** An experimental option to import contacts from the Desktop client has been added (`--importdesktopcontacts`). If it works, the requirement that contacts need to be present in the backup file will be dropped, and Signal Desktop data can be imported into an otherwise completely empty Signal backup file. To my knowledge, as of writing (2024-11-02) this option is untested. If using this, feedback is very much appreciated. Please see [250#issuecomment-2414052506](https://github.com/bepaald/signalbackup-tools/issues/250#issuecomment-2414052506) for more details. **Importing conversations from Telegram / JSON file** The program has successfully been used to import messages from a Telegram export (in JSON format). Telegram's JSON format is [publically documented](https://core.telegram.org/import-export), so any data that can be converted to this format can be imported. This feature will be better documented in the future. For now, more details are available [here](https://github.com/bepaald/signalbackup-tools/issues/153), and any questions and remarks can be added there. General usage: ``` signalbackup-tools [INPUT] [PASSPHRASE] --importtelegram [JSONFILE] -o [OUTPUT] ``` The program will attempt to map the contacts present in the JSON file to those present in the Android backup. It is important all contacts exist in the Android backup, new contacts can not be created. For any JSON contact that the program can not automatically map, this mapping must be done manually using `--mapjsoncontacts`. Other related options: - `--importjson [JSONFILE]` Simply an alias for `--importtelegram`. - `--listjsonchats [JSONFILE]` Lists the chats found in the JSON file. This option does not require an Android backup to be passed as `[INPUT]`. - `--selectjsonchats [list-of-indices]` Only import chat in the list. The indices are obtained from `--listjsonchats`. - `--jsonprependforward` Forwarded messages are marked as such in Telegram, but not in Signal. This option prepends forwarded messages with the string "_Forwarded from NAME:_". - `--preventjsonmapping` If the auto mapping makes a mistake for any reason (for example, multiple contacts with the same name), `--preventjsonmapping "Bob Smith"` will prevent the auto mapping of that specific name. It will then need to be mapped manually (using a unique identifier such as the id) with `--mapjsoncontacts`. - `--jsonmarkdelivered` The Telegram export does not contain message delivery information. This option marks all messages imported from the JSON file as 'delivered'. Defaults to true. - `--jsonmarkread` This option marks all messages imported from the JSON file as 'read'. Defaults to false. **Deleting/Replacing attachments** _NOTE: This feature is highly experimental, problems may occur. Make sure to always keep a copy of your original backup file. Feedback is appreciated_ ##### Deleting attachments To remove attachments from the database, while keeping the message bodies (for example to shrink the size of the backup) the option `--deleteattachments` can be used: ``` signalbackup-tools [input] [passphrase] --deleteattachments --output [output] (--opassphrase [newpassphrase]) ``` To further specify precisely which attachments are to be deleted, the following options can be added: - `--onlyinthreads [list-of-threads]`. The list supports commas for single ids and hyphens for ranges, for example: `--onlyinthreads 1,2,5-8,10`. To obtain the number-id of threads use `--listthreads`. - `--onlyolderthan [date]`/`--onlynewerthan [date]`. Where 'date' supports the same format as the `--croptodates` option ([here](#crop)). - `--onlylargerthan [size]`. The size is specified in bytes. - `--onlytype [mime type]`. This argument can be repeated. Only selects attachments which match 'mime type*' (note the asterisk). For example `--onlytype image/j` will match both 'image/jpg' and 'image/jpeg'. To delete all image type attachments, simply use `--onlytype image`. - `--prependbody [string]`/`--appendbody [string]`. Prepend or append the message body with the supplied string. If the message was otherwise empty, the body will equal the supplied string. Otherwise, it will be appended or prepended and a blank line will be inserted automatically. Suggested use: `--prependbody "(One or more media attachments for this message were deleted)"`. When adding this specifying options, only attachments which match _all_ given options are deleted. ##### Replacing attachments There are two ways to replace attachments in a database. Currently attachments can only be replaced with image files of type jpeg, png or gif (non-animated). ###### Option 1 To replace attachments in a backup file one can use the option `--replaceattachments [type=image,type2=image2,...]`. Where '_type_' is a mime type and image is the new attachment. To narrow the selection of attachments being replaced, all the same options mentioned above can be used (`--onlyinthreads`, `--onlyolderthan`, `--onlylargerthan`, `--onlytype`).
Example and screenshots (click to show)

``` $ ls -lh total 3,0G -rw-r--r-- 1 bepaald bepaald 148 feb 5 21:23 GIF.png -rw-r--r-- 1 bepaald bepaald 195 feb 5 21:23 IMAGE.png -rw-r--r-- 1 bepaald bepaald 3,0G feb 13 15:46 signal-2022-02-14-00-00-00.backup -rw-r--r-- 1 bepaald bepaald 189 feb 5 21:23 VIDEO.png $ ../signalbackup-tools signal-2022-02-14-00-00-00.backup 111112222233333444445555566666 --replaceattachments "image=IMAGE.png,image/gif=GIF.png,video=VIDEO.png" -o signal-2022-02-14-00-00-01.backup signalbackup-tools (../signalbackup-tools) source version 20220111.170852 (OpenSSL) IV: (hex:) c3 05 25 [...] SALT: (hex:) 90 38 9e [...] BACKUPKEY: (hex:) 33 78 2f [...] CIPHERKEY: (hex:) bb dc b0 [...] MACKEY: (hex:) a3 92 76 [...] COUNTER: 3271894439 Reading backup file... FRAME 39538 (100.0%)... Read entire backup file... done! Checking to replace attachment: image/jpeg Replaced attachment at 1/2046 with file "IMAGE.png" Checking to replace attachment: image/jpeg Replaced attachment at 2/2046 with file "IMAGE.png" Checking to replace attachment: image/jpeg Replaced attachment at 3/2046 with file "IMAGE.png" Checking to replace attachment: image/jpeg Replaced attachment at 4/2046 with file "IMAGE.png" Checking to replace attachment: image/jpeg Replaced attachment at 5/2046 with file "IMAGE.png" Checking to replace attachment: image/jpeg Replaced attachment at 6/2046 with file "IMAGE.png" Checking to replace attachment: image/jpeg Replaced attachment at 7/2046 with file "IMAGE.png" Checking to replace attachment: image/png Replaced attachment at 8/2046 with file "IMAGE.png" Checking to replace attachment: image/jpeg Replaced attachment at 9/2046 with file "IMAGE.png" Checking to replace attachment: image/jpeg Replaced attachment at 10/2046 with file "IMAGE.png" Checking to replace attachment: image/jpeg Replaced attachment at 11/2046 with file "IMAGE.png" Checking to replace attachment: image/jpeg Replaced attachment at 12/2046 with file "IMAGE.png" Checking to replace attachment: video/x-matroska Replaced attachment at 13/2046 with file "VIDEO.png" Checking to replace attachment: image/jpeg Replaced attachment at 14/2046 with file "IMAGE.png" Checking to replace attachment: video/mp4 Replaced attachment at 15/2046 with file "VIDEO.png" Checking to replace attachment: image/jpeg Replaced attachment at 16/2046 with file "IMAGE.png" Checking to replace attachment: image/jpeg Replaced attachment at 17/2046 with file "IMAGE.png" Checking to replace attachment: image/gif Replaced attachment at 18/2046 with file "GIF.png" Checking to replace attachment: image/gif Replaced attachment at 19/2046 with file "GIF.png" [...] Checking to replace attachment: image/jpeg Replaced attachment at 2046/2046 with file "IMAGE.png" Exporting backup to 'signal-2022-02-14-00-00-01.backup' Writing HeaderFrame... Writing DatabaseVersionFrame... Writing SqlStatementFrame(s)... Dealing with table 'part'... 2046/2046 entries...done Dealing with table 'drafts'... 0/0 entries... Dealing with table 'push'... 0/0 entries... Dealing with table 'groups'... 1/1 entries...done Dealing with table 'group_receipts'... 9/9 entries...done Dealing with table 'sticker'... 31/31 entries...done Dealing with table 'recipient'... 7/7 entries...done Dealing with table 'storage_key'... 0/0 entries... Dealing with table 'remapped_recipients'... 0/0 entries... Dealing with table 'remapped_threads'... 0/0 entries... Dealing with table 'mention'... 3/3 entries...done Dealing with table 'payments'... 0/0 entries... Dealing with table 'chat_colors'... 0/0 entries... Dealing with table 'sender_key_shared'... 0/0 entries... Dealing with table 'pending_retry_receipts'... 0/0 entries... Dealing with table 'msl_payload'... 93/93 entries...done Dealing with table 'msl_recipient'... 94/94 entries...done Dealing with table 'msl_message'... 93/93 entries...done Dealing with table 'thread'... 6/6 entries...done Dealing with table 'mms'... 2097/2097 entries...done Dealing with table 'sms'... 32832/32832 entries...done Dealing with table 'avatar_picker'... 0/0 entries... Dealing with table 'identities'... 0/0 entries... Dealing with table 'group_call_ring'... 0/0 entries... Dealing with table 'sender_keys'... 0/0 entries... Dealing with table 'reaction'... 17/17 entries...done Dealing with table 'notification_profile'... 0/0 entries... Dealing with table 'notification_profile_schedule'... 0/0 entries... Dealing with table 'notification_profile_allowed_members'... 0/0 entries... Dealing with table 'emoji_search'... 0/0 entries... Writing SharedPrefFrame(s)... Writing KeyValueFrame(s)... Writing Avatars... Writing EndFrame... Done! $ ll -h total 3,0G -rw-r--r-- 1 bepaald bepaald 148 feb 5 21:23 GIF.png -rw-r--r-- 1 bepaald bepaald 195 feb 5 21:23 IMAGE.png -rw-r--r-- 1 bepaald bepaald 3,0G feb 13 15:46 signal-2022-02-14-00-00-00.backup -rw-r--r-- 1 bepaald bepaald 33M feb 13 15:48 signal-2022-02-14-00-00-01.backup -rw-r--r-- 1 bepaald bepaald 189 feb 5 21:23 VIDEO.png ``` Note the mime types do not have to be complete, and the longest type will be matched with highest precedence. In the above case, that means all `image/gif` images are replaced with _"GIF.png"_, while all other images are replaced with _"IMAGE.png"_. ![replace_example](https://user-images.githubusercontent.com/38437099/154285515-0ca20937-dab8-436d-a333-7a614060ed37.png)

###### Option 2 To more easily replace individual attachments with other files, one can first [export the decrypted backup to a directory](#dump), and then for each attachment to replace, place the new file in the directory and name it exactly like the attachment to be replaced, changing the extension to '_.new_'. Then call the program with the `--replaceattachments` option (without arguments).
Example (click to show)

``` $ # dump decrypted backup to directory $ mkdir RAW126 $ ./signalbackup-tools signal-2022-01-28-08-11-49.backup 123456789012345678901234567890 -o RAW126/ signalbackup-tools (./signalbackup-tools) source version 20220111.170852 (OpenSSL) IV: (hex:) c3 05 25 [...] SALT: (hex:) 90 38 9e [...] BACKUPKEY: (hex:) db ff af [...] CIPHERKEY: (hex:) 69 b5 7d [...] MACKEY: (hex:) 7c db e4 ed [...] COUNTER: 3271894439 Reading backup file... FRAME 80968 (100.0%)... Read entire backup file... done! Exporting backup into 'RAW126//' Writing HeaderFrame... Writing DatabaseVersionFrame... Writing Attachments... Writing Avatars... Writing SharedPrefFrame(s)... Writing KeyValueFrame(s)... Writing StickerFrames... Writing EndFrame... Writing database... Done! $ # Now place a new attachment in the directory $ cp ~/IMAGE.png RAW126/Attachment_4653_1643101250724.new $ # And re-encrypt, note the message saying 'replaced 1 attachment' when reading the attachments. $ ./signalbackup-tools RAW126/ --replaceattachments -o OUTPUT.backup -op 012345678901234567890123456789 signalbackup-tools (./signalbackup-tools) source version 20220111.170852 (OpenSSL) Opening from dir! Reading database... Reading HeaderFrame Reading DatabaseVersionFrame Reading SharedPreferenceFrame(s) Reading KeyValueFrame(s) Reading EndFrame Reading AvatarFrames: 20/20 Reading AttachmentFrames - Replaced 1 attachments Reading StickerFrames Done! Exporting backup to 'OUTPUT.backup' Writing HeaderFrame... Writing DatabaseVersionFrame... Writing SqlStatementFrame(s)... Dealing with table 'part'... 4377/4377 entries...done Dealing with table 'drafts'... 0/0 entries... Dealing with table 'push'... 0/0 entries... Dealing with table 'groups'... 25/25 entries...done Dealing with table 'group_receipts'... 4033/4033 entries...done Dealing with table 'sticker'... 31/31 entries...done Dealing with table 'recipient'... 103/103 entries...done Dealing with table 'storage_key'... 0/0 entries... Dealing with table 'remapped_recipients'... 1/1 entries...done Dealing with table 'remapped_threads'... 0/0 entries... Dealing with table 'mention'... 10/10 entries...done Dealing with table 'payments'... 0/0 entries... Dealing with table 'chat_colors'... 0/0 entries... Dealing with table 'emoji_search'... 0/0 entries... Dealing with table 'sender_key_shared'... 0/0 entries... Dealing with table 'pending_retry_receipts'... 0/0 entries... Dealing with table 'msl_payload'... 184/184 entries...done Dealing with table 'msl_recipient'... 190/190 entries...done Dealing with table 'msl_message'... 184/184 entries...done Dealing with table 'thread'... 38/38 entries...done Dealing with table 'mms'... 5876/5876 entries...done Dealing with table 'sms'... 61273/61273 entries...done Dealing with table 'avatar_picker'... 0/0 entries... Dealing with table 'identities'... 35/35 entries...done Dealing with table 'group_call_ring'... 0/0 entries... Dealing with table 'sender_keys'... 0/0 entries... Dealing with table 'reaction'... 52/52 entries...done Dealing with table 'notification_profile'... 0/0 entries... Dealing with table 'notification_profile_schedule'... 0/0 entries... Dealing with table 'notification_profile_allowed_members'... 0/0 entries... Writing SharedPrefFrame(s)... Writing KeyValueFrame(s)... Writing Avatars... Writing EndFrame... Done! ```

> [!TIP] > A handy python script that uses this option was developed to replace attachments with shrunk versions. It is available [here](https://github.com/cycneuramus/signal-backup-shrink). Thanks @cycneuramus! **Operations for Signal Desktop** While this tool only deals with backups from Signal Android, and there are no plans to change that, a small number of functions that operate on a Signal Desktop database is available. These options primarily exist to facilitate debugging the [import from Desktop](#desktop) function. Running with these options does not require an Android backup file to be provided as input (for example `signalbackup-tools --exportdesktophtml [TARGETDIR]`). These options support some of the same modifying options as `--importfromdesktop`, namely: `--desktopdirs`, and `--ignorewal`. - `--dumpdesktopdb [OUTPUTFILE]` Save the Desktop database to `[OUTPUTFILE]` without encryption. - `--rundtsqlquery [QUERY]` Run a query on the Desktop SQL database. Note that the database only resides in memory and any changes are _not_ saved to disk. - `--rundtprettysqlquery [QUERY]` As above, but tries to make the output a bit nicer to look at. Depending on the size of the query and the size of the output terminal, may make the output more ledgible (or less so). - `--exportdesktophtml [OUTPUTDIR]` Export the Signal Desktop database to HTML. This function works internally by creating an empty Android backup, importing the desktop into this and then exporting that internal Android backup to HTML. As a result it supports almost all modifying options mentioned in [import from Desktop](#desktop) and [export to HTML](#export-to-html) (excluding `--limittothreads`, and `--includesettings`). It also has the same limitations as both of these functions combined. - `--exportdesktoptxt [OUTPUTDIR]` Export the Signal Desktop database to plain text. Works as the above function, except the internal Android backup is [exported to TXT](#export-to-txt) instead. - `--desktopkey [HEXSTRING]` This is a modifying option for all desktop functions. Manually set the cipher key to use for decrypting the Signal Desktop database (see above note). - `--showdesktopkey` Shows the key used to decrypt the Signal Desktop database. > [!NOTE] > While this program will compile and work with almost any version of SQLite3, these features require that the SQLite3 version used is not too far behind the one used by the Signal Desktop client. Older versions will may not be able to read Signal Desktop's database. For example, the version available in Ubuntu is regularly too old to read Signal Desktop's database. For Ubuntu(-like) distributions a PPA exists with a more up-to-date version [here](https://launchpad.net/~linuxgndu/+archive/ubuntu/sqlitebrowser) (disclaimer: I am not affiliated with this PPA, and never used it). **Various** This program supports a small number of other options, most of which are of little to no use for everyday users. A select few that may be useful are mentioned here. A more complete list can be found by running with `--help`. - `--runsqlquery [QUERY]` Run any query on the SQL database in the backup file. If combined with `-o/--output` any changes made are saved in the new backup file. See also [advanced options](#advanced). - `--runprettysqlquery [QUERY]` As above, but tries to make make the output a bit nicer to look at. Depending on the size of the query and the size of the output terminal, may make the output more legible (or less so). - `-l/--logfile [FILE]` All terminal output is saved to file `[FILE]`. - `--no-truncate` Any SQL query results that are pretty-printed (see `--runprettysqlquery` above) are normally truncated to fit in the output terminal. This option will prevent this truncating. May be useful when redirecting to file (or using the `--logfile` option). - `--no-showprogress` Disable (most) progress indicators. Especially useful when trying to parse the programs output in a script. - `-v/--verbose` Run in verbose mode. This will print a _lot_ of text to output, may be useful in case of errors. - `--listrecipients` Lists all recipients found in the database. - `--showdbinfo` Prints a list of all tables and their columns in the backups Sqlite database. - `--scanmissingattachments` If you see _"warning attachment data not found"_ messages, feel free to use this option and provide the output to the developer. - `--migrate214to215` Changes in the database prevent v214 and v215 from being compatible for merging. This function attempts to migrate the older database so it can be used as a source for `--importthreads`. See also https://github.com/bepaald/signalbackup-tools/issues/184. - `--migrate_to_191` _[DEPRECATED: This option should not be needed anymore since the Signal bug is fixed.]_ Work-around for [Signal issue 13034](https://github.com/signalapp/Signal-Android/issues/13034). If you are trying to restore an older backup (before database version 191), and Signal crashes right after the restore, try this. ([ref](https://github.com/signalapp/Signal-Android/issues/13034#issuecomment-2351447616), [ref](https://github.com/bepaald/signalbackup-tools/issues/233#issuecomment-2343769016)). - `--setchatcolors [rid=RRGGBB,rid2=RRGGBB,...]` This option allows you to set custom chat colors to any RGB value, even those not available in Signal's color picker. Here `rid` is a recipient-id as reported by the `--listrecipients` option. Use at your own risk.
    Example (click to show)

    ```ShellSession $ ./signalbackup-tools DEV2signal-2024-12-05-14-08-16.backup 000000000000000000000000000000 --listrecipients *** Starting log: 2024-12-05 15:57:28 *** signalbackup-tools (./signalbackup-tools) source version 20241203.085751 (SQlite: 3.47.1, OpenSSL: OpenSSL 3.4.0 22 Oct 2024) BACKUPFILE VERSION: 1 BACKUPFILE SIZE: 10940289 COUNTER: 1335720069 Reading backup file: 100.0%... done! Database version: 256 ------------------------------------------------------------------------------------------------------------------------------------------------ | _id | display_name | e164 | blocked | hidden | has_avatar | type | registered | has_id | has_thread | ------------------------------------------------------------------------------------------------------------------------------------------------ [...] | 10 | colorgroup_yellow | (NULL) | 0 | 0 | 1 | Group (v2) | (n/a) | 1 | 1 | | 11 | group color black | (NULL) | 0 | 0 | 1 | Group (v2) | (n/a) | 1 | 1 | ------------------------------------------------------------------------------------------------------------------------------------------------ $ ./signalbackup-tools DEV2signal-2024-12-05-14-08-16.backup 000000000000000000000000000000 --setchatcolors 10=ffff00,11=000000 --output BAD_COLORS.backup *** Starting log: 2024-12-05 15:57:35 *** signalbackup-tools (./signalbackup-tools) source version 20241203.085751 (SQlite: 3.47.1, OpenSSL: OpenSSL 3.4.0 22 Oct 2024) BACKUPFILE VERSION: 1 BACKUPFILE SIZE: 10940289 COUNTER: 1335720069 Reading backup file: 100.0%... done! Database version: 256 Exporting backup to 'BAD_COLORS.backup' [...] Done! Wrote 10940302 bytes. ``` ![signal-2024-12-05-155131](https://github.com/user-attachments/assets/25534bf9-ddca-4eb6-bbb1-41fd1603306e)

**Advanced options** The program can run any sql queries on the database in the backup file and save the output. If you know the schema of the database and know what you're doing, feel free to run any query and save the output. Examples: ``` # delete all sms and mms messages from one thread: signalbackup-tools [input] [passphrase] --runsqlquery "DELETE * FROM sms WHERE thread_id = 1" --runsqlquery "DELETE * FROM mms WHERE thread_id = 1" --output [output] (--opassphrase [newpassphrase]) ``` ``` # list all messages in the sms database where the message body was 'Yes' $ ./signalbackup-tools [input] [passphrase] --runprettysqlquery "SELECT _id,body,DATETIME(ROUND(date / 1000), 'unixepoch') AS isodate,date FROM sms WHERE body == 'yes' COLLATE NOCASE" signalbackup-tools source version 20191219.175337 IV: (hex:) 12 16 72 95 7a 00 68 44 7e cf 7d 20 26 f9 d3 7d (size: 16) SALT: (hex:) cc 03 85 02 61 97 eb 5b ed 3e 05 00 c4 a8 77 40 28 08 aa 9f e5 a8 00 74 b4 f8 56 aa 24 57 a9 5d (size: 32) BACKUPKEY: (hex:) 8f ff df 2b 9f 96 73 9a 63 95 0f ea 3f b1 e5 a4 87 12 19 ca 93 31 86 2a 60 3f 41 ef 6d a4 08 44 (size: 32) CIPHERKEY: (hex:) ce 53 c1 f2 92 4b e3 b8 e1 56 85 61 14 96 82 8b 83 7f 07 21 83 52 1a c2 3f 6b 16 83 3e 33 94 a3 (size: 32) MACKEY: (hex:) c2 77 af 1e 4b 05 db 62 52 57 af 8a d6 a4 d4 e9 6c 93 53 81 9a e7 6f 12 2c ce 13 8f b3 5e 8d 3a (size: 32) COUNTER: 2907636 Reading backup file... FRAME 4852 (100.0%)... Read entire backup file... done! * Executing query: SELECT _id,body,DATETIME(ROUND(date / 1000), 'unixepoch') AS isodate,date FROM sms WHERE body == 'yes' COLLATE NOCASE ------------------------------------------------------ | _id | body | isodate | date | ------------------------------------------------------ | 3235 | Yes | 2017-10-21 17:10:15 | 1508605815286 | | 9345 | Yes | 2017-12-18 22:18:36 | 1513635516440 | | 17125 | Yes | 2018-02-02 15:46:16 | 1517586376228 | | 21300 | Yes | 2018-05-10 21:14:49 | 1525986889325 | | 26317 | Yes | 2018-10-25 15:16:58 | 1540480618238 | | 32433 | Yes | 2019-05-10 14:22:25 | 1557498145794 | ------------------------------------------------------ # now change a specific message: [~/programming/signalbackup-tools] $ ./signalbackup-tools [input] [passphrase] --runsqlquery "UPDATE sms SET body = 'No' WHERE _id == 21300" --ouput [output] ``` If you also need to edit the attachments, dump the backup to directory first ([as described above](#dump)) and do whatever you want, but realize when editing the .bin file, it will usually require changes to also be made to the .sbf file and the sql database to end up with a valid database. ## Future plans - ~~merging existing backups (successful tests have been done)~~ _DONE_ - exporting to other formats (~~csv~~, xml, ~~html~~) _WIP_ - ~~cropping backup to certain conversations and time spans (successfully done in testing)~~ _DONE_ - ~~replacing/deleting attachments without changing/deleting messages. For example, replacing with thumbnails or tiny placeholders to save space.~~ _DONE (pretty much hopefully)_ - ~~importing databases from the desktop app. I have no experience with that yet.~~ _DONE (I think, mostly)_ Development will be slow at times. ## Donate If this tool was helpful to you or you appreciate my work and you can spare it, you might consider donating: Paypal: [![Paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=U523FZFW3BQBQ¤cy_code=USD&source=url) Ko-fi: [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/bepaald) BTC: 17RqHi9XBeUAEShbp2RnbmkCSAU2R94tH4 Donations will help development in that they will put food in my mouth, and I need food to write code :smile: You might also consider helping out the Signal Foundation here: https://support.signal.org/hc/en-us/articles/360007319831-How-can-I-contribute-to-Signal- signalbackup-tools-20250313-1/androidattachmentreader/000077500000000000000000000000001476450434500226025ustar00rootroot00000000000000signalbackup-tools-20250313-1/androidattachmentreader/androidattachmentreader.h000066400000000000000000000273501476450434500276360ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef ANDROIDATTACHMENTREADER_H_ #define ANDROIDATTACHMENTREADER_H_ #include #include #include #include "../common_be.h" #include "../common_bytes.h" #include "../baseattachmentreader/baseattachmentreader.h" #include "../framewithattachment/framewithattachment.h" #include "../cryptbase/cryptbase.h" class AndroidAttachmentReader : public AttachmentReader { uint32_t d_attachmentdata_size; uint64_t d_filepos; uint32_t d_iv_size; unsigned char *d_iv; uint64_t d_mackey_size; unsigned char *d_mackey; uint64_t d_cipherkey_size; unsigned char *d_cipherkey; std::string d_filename; public: inline AndroidAttachmentReader(unsigned char const *iv, uint32_t iv_size, unsigned char const *mackey, uint64_t mackey_size, unsigned char const *cipherkey, uint64_t cipherkey_size, uint32_t attsize, std::string const &filename, uint64_t filepos); inline AndroidAttachmentReader(AndroidAttachmentReader const &other); inline AndroidAttachmentReader(AndroidAttachmentReader &&other); inline AndroidAttachmentReader &operator=(AndroidAttachmentReader const &other); inline AndroidAttachmentReader &operator=(AndroidAttachmentReader &&other); inline virtual ~AndroidAttachmentReader() override; inline virtual int getAttachment(FrameWithAttachment *frame, bool verbose) override; }; inline AndroidAttachmentReader::AndroidAttachmentReader(unsigned char const *iv, uint32_t iv_size, unsigned char const *mackey, uint64_t mackey_size, unsigned char const *cipherkey, uint64_t cipherkey_size, uint32_t attsize, std::string const &filename, uint64_t filepos) : d_attachmentdata_size(0), d_filepos(0), d_iv_size(0), d_iv(nullptr), d_mackey_size(0), d_mackey(nullptr), d_cipherkey_size(0), d_cipherkey(nullptr) { d_iv_size = iv_size; if (iv) { d_iv = new unsigned char[d_iv_size]; std::memcpy(d_iv, iv, d_iv_size); } d_cipherkey_size = cipherkey_size; if (cipherkey) { d_cipherkey = new unsigned char[d_cipherkey_size]; std::memcpy(d_cipherkey, cipherkey, d_cipherkey_size); } d_mackey_size = mackey_size; if (mackey) { d_mackey = new unsigned char[d_mackey_size]; std::memcpy(d_mackey, mackey, d_mackey_size); } d_attachmentdata_size = attsize; d_filename = filename; d_filepos = filepos; } inline AndroidAttachmentReader::AndroidAttachmentReader(AndroidAttachmentReader const &other) : AttachmentReader(other), d_attachmentdata_size(other.d_attachmentdata_size), d_filepos(other.d_filepos), d_iv_size(other.d_iv_size), d_iv(nullptr), d_mackey_size(other.d_mackey_size), d_mackey(nullptr), d_cipherkey_size(other.d_cipherkey_size), d_cipherkey(nullptr), d_filename(other.d_filename) { if (other.d_iv) { d_iv = new unsigned char[d_iv_size]; std::memcpy(d_iv, other.d_iv, d_iv_size); } if (other.d_mackey) { d_mackey = new unsigned char[d_mackey_size]; std::memcpy(d_mackey, other.d_mackey, d_mackey_size); } if (other.d_cipherkey) { d_cipherkey = new unsigned char[d_cipherkey_size]; std::memcpy(d_cipherkey, other.d_cipherkey, d_cipherkey_size); } } inline AndroidAttachmentReader::AndroidAttachmentReader(AndroidAttachmentReader &&other) : AttachmentReader(other), d_attachmentdata_size(std::move(other.d_attachmentdata_size)), d_filepos(std::move(other.d_filepos)), d_iv_size(std::move(other.d_iv_size)), d_iv(std::move(other.d_iv)), d_mackey_size(std::move(other.d_mackey_size)), d_mackey(std::move(other.d_mackey)), d_cipherkey_size(std::move(other.d_cipherkey_size)), d_cipherkey(std::move(other.d_cipherkey)), d_filename(std::move(other.d_filename)) { other.d_attachmentdata_size = 0; other.d_iv_size = 0; other.d_iv = nullptr; other.d_mackey_size = 0; other.d_mackey = nullptr; other.d_cipherkey_size = 0; other.d_cipherkey = nullptr; } inline AndroidAttachmentReader &AndroidAttachmentReader::operator=(AndroidAttachmentReader const &other) { if (this != &other) { bepaald::destroyPtr(&d_iv, &d_iv_size); bepaald::destroyPtr(&d_mackey, &d_mackey_size); bepaald::destroyPtr(&d_cipherkey, &d_cipherkey_size); d_iv_size = other.d_iv_size; d_mackey_size = other.d_mackey_size; d_cipherkey_size = other.d_cipherkey_size; if (other.d_iv) { d_iv = new unsigned char[d_iv_size]; std::memcpy(d_iv, other.d_iv, d_iv_size); } if (other.d_mackey) { d_mackey = new unsigned char[d_mackey_size]; std::memcpy(d_mackey, other.d_mackey, d_mackey_size); } if (other.d_cipherkey) { d_cipherkey = new unsigned char[d_cipherkey_size]; std::memcpy(d_cipherkey, other.d_cipherkey, d_cipherkey_size); } d_filename = other.d_filename; d_filepos = other.d_filepos; d_attachmentdata_size = other.d_attachmentdata_size; } return *this; } inline AndroidAttachmentReader &AndroidAttachmentReader::operator=(AndroidAttachmentReader &&other) { if (this != &other) { // destroy any data this already owns bepaald::destroyPtr(&d_iv, &d_iv_size); bepaald::destroyPtr(&d_mackey, &d_mackey_size); bepaald::destroyPtr(&d_cipherkey, &d_cipherkey_size); // take over other's data d_iv = std::move(other.d_iv); d_iv_size = std::move(other.d_iv_size); d_mackey = std::move(other.d_mackey); d_mackey_size = std::move(other.d_mackey_size); d_cipherkey = std::move(other.d_cipherkey); d_cipherkey_size = std::move(other.d_cipherkey_size); d_filename = std::move(other.d_filename); d_filepos = std::move(other.d_filepos); d_attachmentdata_size = std::move(other.d_attachmentdata_size); // invalidate other other.d_iv = nullptr; other.d_iv_size = 0; other.d_mackey = nullptr; other.d_mackey_size = 0; other.d_cipherkey = nullptr; other.d_cipherkey_size = 0; } return *this; } inline AndroidAttachmentReader::~AndroidAttachmentReader() { bepaald::destroyPtr(&d_iv, &d_iv_size); bepaald::destroyPtr(&d_mackey, &d_mackey_size); bepaald::destroyPtr(&d_cipherkey, &d_cipherkey_size); } inline int AndroidAttachmentReader::getAttachment(FrameWithAttachment *frame, bool verbose) // virtual { //std::cout << " *** REALLY GETTING ATTACHMENT (ANDROID) ***" << std::endl; std::ifstream file(d_filename, std::ios_base::binary | std::ios_base::in); if (!file.is_open()) { Logger::error("Failed to open backup file '", d_filename, "' for reading attachment"); return 1; } if (d_attachmentdata_size == 0) [[unlikely]] Logger::warning("Asked to read 0-byte attachment"); if (verbose) [[unlikely]] Logger::message("Decrypting attachment data, length: ", d_attachmentdata_size); //std::cout << "Getting attachment: " << frame->filepos() << " + " << frame->length() << std::endl; file.seekg(d_filepos, std::ios_base::beg); // to decrypt the data // create context std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); // disable padding EVP_CIPHER_CTX_set_padding(ctx.get(), 0); // init if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr, d_cipherkey, d_iv) != 1) { Logger::error("CTX INIT FAILED"); return 1; } // to calculate the MAC #if OPENSSL_VERSION_NUMBER >= 0x30000000L std::unique_ptr mac(EVP_MAC_fetch(nullptr, "hmac", nullptr), &::EVP_MAC_free); std::unique_ptr hctx(EVP_MAC_CTX_new(mac.get()), &::EVP_MAC_CTX_free); char digest[] = "SHA256"; OSSL_PARAM params[] = {OSSL_PARAM_construct_utf8_string("digest", digest, 0), OSSL_PARAM_construct_end()}; #else std::unique_ptr hctx(HMAC_CTX_new(), &::HMAC_CTX_free); #endif #if OPENSSL_VERSION_NUMBER >= 0x30000000L if (EVP_MAC_init(hctx.get(), d_mackey, d_mackey_size, params) != 1) #else if (HMAC_Init_ex(hctx.get(), d_mackey, d_mackey_size, EVP_sha256(), nullptr) != 1) #endif { Logger::error("Failed to initialize HMAC context"); return 1; } #if OPENSSL_VERSION_NUMBER >= 0x30000000L if (EVP_MAC_update(hctx.get(), d_iv, d_iv_size) != 1) #else if (HMAC_Update(hctx.get(), d_iv, d_iv_size) != 1) #endif { Logger::error("Failed to update HMAC"); return 1; } // read and process attachment data in 8MB chunks uint32_t const BUFFERSIZE = 8 * 1024; unsigned char encrypteddatabuffer[BUFFERSIZE]; uint32_t processed = 0; uint32_t size = d_attachmentdata_size; std::unique_ptr decryptedattachmentdata(new unsigned char[size]); // to hold the data while (processed < size) { if (!file.read(reinterpret_cast(encrypteddatabuffer), std::min(size - processed, BUFFERSIZE))) { Logger::error("STOPPING BEFORE END OF ATTACHMENT!!!", (file.eof() ? " (EOF) " : "")); return 1; } uint32_t read = file.gcount(); // update MAC with read data #if OPENSSL_VERSION_NUMBER >= 0x30000000L if (EVP_MAC_update(hctx.get(), encrypteddatabuffer, read) != 1) #else if (HMAC_Update(hctx.get(), encrypteddatabuffer, read) != 1) #endif { Logger::error("Failed to update HMAC"); return 1; } // decrypt the read data; int spaceleft = size - processed; if (EVP_DecryptUpdate(ctx.get(), decryptedattachmentdata.get() + processed, &spaceleft, encrypteddatabuffer, read) != 1) { Logger::error("Failed to decrypt data"); return 1; } processed += read; //return; } DEBUGOUT("Read ", processed, " bytes"); #if OPENSSL_VERSION_NUMBER >= 0x30000000L unsigned long int digest_size = SHA256_DIGEST_LENGTH; unsigned char hash[SHA256_DIGEST_LENGTH]; if (EVP_MAC_final(hctx.get(), hash, nullptr, digest_size) != 1) #else unsigned int digest_size = SHA256_DIGEST_LENGTH; unsigned char hash[SHA256_DIGEST_LENGTH]; if (HMAC_Final(hctx.get(), hash, &digest_size) != 1) #endif { Logger::error("Failed to finalize MAC"); return 1; } unsigned char theirMac[CryptBase::MACSIZE]; if (!file.read(reinterpret_cast(theirMac), CryptBase::MACSIZE)) { Logger::error("STOPPING BEFORE END OF ATTACHMENT!!! 2 "); return 1; } DEBUGOUT("theirMac : ", bepaald::bytesToHexString(theirMac, CryptBase::MACSIZE)); DEBUGOUT("ourMac : ", bepaald::bytesToHexString(hash, SHA256_DIGEST_LENGTH)); bool badmac = false; if (std::memcmp(theirMac, hash, CryptBase::MACSIZE) != 0) { Logger::warning("Bad MAC in attachmentdata: theirMac: ", bepaald::bytesToHexString(theirMac, CryptBase::MACSIZE)); Logger::warning_indent(" ourMac: ", bepaald::bytesToHexString(hash, SHA256_DIGEST_LENGTH)); badmac = true; } else badmac = false; if (frame->setAttachmentDataBacked(decryptedattachmentdata.release(), d_attachmentdata_size)) { if (badmac) return -1; return 0; } return 1; } #endif signalbackup-tools-20250313-1/arg/000077500000000000000000000000001476450434500164775ustar00rootroot00000000000000signalbackup-tools-20250313-1/arg/arg.cc000066400000000000000000001506351476450434500175710ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "arg.h" Arg::Arg(int argc, char *argv[]) : d_ok(false), d_positionals(0), d_maxpositional(2), d_progname(argv[0]), d_input(std::string()), d_passphrase(std::string()), d_importthreads(std::vector()), d_importthreadsbyname(std::vector()), d_limittothreads(std::vector()), d_limittothreadsbyname(std::vector()), d_output(std::string()), d_opassphrase(std::string()), d_source(std::string()), d_sourcepassphrase(std::string()), d_croptothreads(std::vector()), d_croptothreadsbyname(std::vector()), d_croptodates(std::vector()), d_mergerecipients(std::vector()), d_mergegroups(std::vector()), d_exportcsv(std::vector>()), d_exportxml(std::string()), d_querymode(std::string()), d_runsqlquery(std::vector()), d_runprettysqlquery(std::vector()), d_rundtsqlquery(std::vector()), d_rundtprettysqlquery(std::vector()), d_limitcontacts(std::vector()), d_assumebadframesizeonbadmac(false), d_editattachmentsize(std::vector()), d_dumpdesktopdb(std::string()), d_desktopdir(std::string()), d_desktopdirs_1(std::string()), d_desktopdirs_2(std::string()), d_rawdesktopdb(std::string()), d_desktopkey(std::string()), d_showdesktopkey(false), d_dumpmedia(std::string()), d_excludestickers(false), d_dumpavatars(std::string()), d_devcustom(false), d_importcsv(std::string()), d_mapcsvfields(std::vector>()), d_setselfid(std::string()), d_onlydb(bool()), d_overwrite(false), d_listthreads(false), d_listrecipients(false), d_showprogress(true), d_removedoubles(0), d_removedoubles_bool(false), d_reordermmssmsids(false), d_stoponerror(false), d_verbose(false), d_dbusverbose(false), d_strugee(-1), d_strugee3(-1), d_ashmorgan(false), d_strugee2(false), d_hiperfall(-1), d_arc(-1), d_deleteattachments(false), d_onlyinthreads(std::vector()), d_onlyolderthan(std::string()), d_onlynewerthan(std::string()), d_onlylargerthan(-1), d_onlytype(std::vector()), d_appendbody(std::string()), d_prependbody(std::string()), d_replaceattachments(std::vector>()), d_replaceattachments_bool(false), d_help(false), d_scanmissingattachments(false), d_showdbinfo(false), d_scramble(false), d_importfromdesktop(false), d_limittodates(std::vector()), d_autolimitdates(false), d_ignorewal(false), d_includemms(true), d_checkdbintegrity(false), d_interactive(false), d_exporthtml(std::string()), d_exportdesktophtml(std::string()), d_exportplaintextbackuphtml(std::vector()), d_importplaintextbackup(std::vector()), d_addexportdetails(false), d_includecalllog(false), d_includeblockedlist(false), d_includesettings(false), d_includefullcontactlist(false), d_themeswitching(false), d_searchpage(false), d_stickerpacks(false), d_includereceipts(false), d_chatfolders(false), d_split(1000), d_split_bool(false), d_split_by(std::string()), d_originalfilenames(false), d_addincompletedataforhtmlexport(false), d_importdesktopcontacts(false), d_light(false), d_exporttxt(std::string()), d_exportdesktoptxt(std::string()), d_append(false), d_desktopdbversion(4), d_migratedb(false), d_importstickers(false), d_findrecipient(-1), d_importtelegram(std::string()), d_listjsonchats(std::string()), d_selectjsonchats(std::vector()), d_mapjsoncontacts(std::vector>()), d_preventjsonmapping(std::vector()), d_jsonprependforward(false), d_jsonmarkdelivered(true), d_jsonmarkread(false), d_xmlmarkdelivered(true), d_xmlmarkread(false), d_fulldecode(false), d_logfile(std::string()), d_custom_hugogithubs(false), d_truncate(true), d_skipmessagereorder(false), d_migrate_to_191(false), d_mapxmlcontacts(std::vector>()), d_listxmlcontacts(std::vector()), d_selectxmlchats(std::vector()), d_linkify(true), d_setchatcolors(std::vector>()), d_mapxmlcontactnames(std::vector>()), d_mapxmlcontactnamesfromfile(std::string()), d_mapxmladdresses(std::vector>()), d_mapxmladdressesfromfile(std::string()), d_xmlautogroupnames(false), d_setcountrycode(std::string()), d_compactfilenames(false), d_generatedummy(std::string()), d_targetisdummy(false), d_htmlignoremediatypes(std::vector()), d_pagemenu(false), d_input_required(false) { // vector to hold arguments std::vector config; // add command line options. config.insert(config.end(), argv + 1, argv + argc); d_ok = parseArgs(config); } bool Arg::parseArgs(std::vector const &arguments) { bool ok = true; int argsize = arguments.size(); for (int i = 0; i < argsize; ++i) { std::string option = arguments[i]; if (option == "-i" || option == "--input") { if (i < argsize - 1) { d_input = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "-p" || option == "--passphrase" || option == "--password") { if (i < argsize - 1) { d_passphrase = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--importthreads") { if (i < argsize - 1) { if (arguments[i + 1] == "all" || arguments[i + 1] == "ALL") { long long int tmp; if (!ston(&tmp, std::string("-1"))) { std::cerr << "Bad special value in argument spec file!" << std::endl; ok = false; } d_importthreads.clear(); d_importthreads.push_back(tmp); ++i; d_input_required = true; continue; } if (!parseNumberList(arguments[++i], &d_importthreads, true)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--importthreadsbyname") { if (i < argsize - 1) { if (!parseStringList(arguments[++i], &d_importthreadsbyname)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--limittothreads") { if (i < argsize - 1) { if (!parseNumberList(arguments[++i], &d_limittothreads, true)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--limittothreadsbyname") { if (i < argsize - 1) { if (!parseStringList(arguments[++i], &d_limittothreadsbyname)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "-o" || option == "--output") { if (i < argsize - 1) { d_output = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "-op" || option == "--opassphrase" || option == "--opassword") { if (i < argsize - 1) { d_opassphrase = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "-s" || option == "--source") { if (i < argsize - 1) { d_source = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "-sp" || option == "--sourcepassphrase" || option == "--sourcepassword") { if (i < argsize - 1) { d_sourcepassphrase = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--croptothreads") { if (i < argsize - 1) { if (!parseNumberList(arguments[++i], &d_croptothreads, true)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--croptothreadsbyname") { if (i < argsize - 1) { if (!parseStringList(arguments[++i], &d_croptothreadsbyname)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--croptodates") { if (i < argsize - 1) { std::regex validator("^(?:(?:[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})|[0-9]+), *(?:(?:[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})|[0-9]+)(?:, *(?:(?:[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})|[0-9]+), *(?:(?:[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})|[0-9]+))*$"); if (!std::regex_match(arguments[i + 1], validator)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; continue; } if (!parseStringList(arguments[++i], &d_croptodates)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--mergerecipients") { if (i < argsize - 1) { if (!parseStringList(arguments[++i], &d_mergerecipients)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--mergegroups") { if (i < argsize - 1) { if (!parseStringList(arguments[++i], &d_mergegroups)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--exportcsv") { if (i < argsize - 1) { std::string error; if (!parsePairList(arguments[++i], "=", &d_exportcsv, &error)) { std::cerr << "[ Error parsing command line option `" << option << "': " << error << " ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--exportxml") { if (i < argsize - 1) { d_exportxml = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--querymode") { if (i < argsize - 1) { std::regex validator("line|pretty|single", std::regex::icase); if (!std::regex_match(arguments[i + 1], validator)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; continue; } d_querymode = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--runsqlquery") { if (i < argsize - 1) { d_runsqlquery.emplace_back(std::move(arguments[++i])); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--runprettysqlquery") { if (i < argsize - 1) { d_runprettysqlquery.emplace_back(std::move(arguments[++i])); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--rundtsqlquery") { if (i < argsize - 1) { d_rundtsqlquery.emplace_back(std::move(arguments[++i])); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--rundtprettysqlquery") { if (i < argsize - 1) { d_rundtprettysqlquery.emplace_back(std::move(arguments[++i])); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--limitcontacts") { if (i < argsize - 1) { if (!parseStringList(arguments[++i], &d_limitcontacts)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--assumebadframesizeonbadmac") { d_assumebadframesizeonbadmac = true; continue; } if (option == "--no-assumebadframesizeonbadmac") { d_assumebadframesizeonbadmac = false; continue; } if (option == "--editattachmentsize") { if (i < argsize - 1) { if (!parseNumberList(arguments[++i], &d_editattachmentsize, false)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--dumpdesktopdb") { if (i < argsize - 1) { d_dumpdesktopdb = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--desktopdir") { if (i < argsize - 1) { d_desktopdir = std::move(arguments[++i]); d_desktopdirs_1 = d_desktopdir; d_desktopdirs_2 = d_desktopdir; } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--desktopdirs") { if (i < argsize - 2) { d_desktopdirs_1 = std::move(arguments[++i]); d_desktopdirs_2 = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--rawdesktopdb") { if (i < argsize - 1) { d_rawdesktopdb = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--desktopkey") { if (i < argsize - 1) { std::regex validator("^[0-9a-fA-F]{64}$", std::regex::icase); if (!std::regex_match(arguments[i + 1], validator)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; continue; } d_desktopkey = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--showdesktopkey") { d_showdesktopkey = true; continue; } if (option == "--no-showdesktopkey") { d_showdesktopkey = false; continue; } if (option == "--dumpmedia") { if (i < argsize - 1) { d_dumpmedia = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--excludestickers") { d_excludestickers = true; continue; } if (option == "--no-excludestickers") { d_excludestickers = false; continue; } if (option == "--dumpavatars") { if (i < argsize - 1) { d_dumpavatars = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--devcustom") { d_devcustom = true; continue; } if (option == "--no-devcustom") { d_devcustom = false; continue; } if (option == "--importcsv") { if (i < argsize - 1) { d_importcsv = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--mapcsvfields") { if (i < argsize - 1) { std::string error; if (!parsePairList(arguments[++i], "=", &d_mapcsvfields, &error)) { std::cerr << "[ Error parsing command line option `" << option << "': " << error << " ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--setselfid") { if (i < argsize - 1) { d_setselfid = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--onlydb") { d_onlydb = true; continue; } if (option == "--no-onlydb") { d_onlydb = false; continue; } if (option == "--overwrite") { d_overwrite = true; continue; } if (option == "--no-overwrite") { d_overwrite = false; continue; } if (option == "--listthreads") { d_listthreads = true; d_input_required = true; continue; } if (option == "--no-listthreads") { d_listthreads = false; continue; } if (option == "--listrecipients") { d_listrecipients = true; d_input_required = true; continue; } if (option == "--no-listrecipients") { d_listrecipients = false; continue; } if (option == "--showprogress") { d_showprogress = true; continue; } if (option == "--no-showprogress") { d_showprogress = false; continue; } if (option == "--removedoubles") { d_removedoubles_bool = true; if (i < argsize - 1 && !isOption(arguments[i + 1])) { if (!ston(&d_removedoubles, arguments[++i])) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } d_input_required = true; continue; } if (option == "--reordermmssmsids") { d_reordermmssmsids = true; d_input_required = true; continue; } if (option == "--no-reordermmssmsids") { d_reordermmssmsids = false; continue; } if (option == "--stoponerror") { d_stoponerror = true; continue; } if (option == "--no-stoponerror") { d_stoponerror = false; continue; } if (option == "-v" || option == "--verbose") { d_verbose = true; continue; } if (option == "--no-verbose") { d_verbose = false; continue; } if (option == "--dbusverbose") { d_dbusverbose = true; continue; } if (option == "--no-dbusverbose") { d_dbusverbose = false; continue; } if (option == "--strugee") { if (i < argsize - 1) { if (!ston(&d_strugee, arguments[++i])) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--strugee3") { if (i < argsize - 1) { if (!ston(&d_strugee3, arguments[++i])) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--ashmorgan") { d_ashmorgan = true; continue; } if (option == "--no-ashmorgan") { d_ashmorgan = false; continue; } if (option == "--strugee2") { d_strugee2 = true; continue; } if (option == "--no-strugee2") { d_strugee2 = false; continue; } if (option == "--hiperfall") { if (i < argsize - 1) { if (!ston(&d_hiperfall, arguments[++i])) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--arc") { if (i < argsize - 1) { if (!ston(&d_arc, arguments[++i])) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--deleteattachments") { d_deleteattachments = true; d_input_required = true; continue; } if (option == "--no-deleteattachments") { d_deleteattachments = false; continue; } if (option == "--onlyinthreads") { if (i < argsize - 1) { if (!parseNumberList(arguments[++i], &d_onlyinthreads, true)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--onlyolderthan") { if (i < argsize - 1) { std::regex validator("^(?:(?:[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})|[0-9]+)$", std::regex::icase); if (!std::regex_match(arguments[i + 1], validator)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; continue; } d_onlyolderthan = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--onlynewerthan") { if (i < argsize - 1) { std::regex validator("^(?:(?:[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})|[0-9]+)$", std::regex::icase); if (!std::regex_match(arguments[i + 1], validator)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; continue; } d_onlynewerthan = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--onlylargerthan") { if (i < argsize - 1) { if (!ston(&d_onlylargerthan, arguments[++i])) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--onlytype") { if (i < argsize - 1) { if (!parseStringList(arguments[++i], &d_onlytype)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--appendbody") { if (i < argsize - 1) { d_appendbody = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--prependbody") { if (i < argsize - 1) { d_prependbody = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--replaceattachments") { if (i < argsize - 1 && !isOption(arguments[i + 1])) { std::string error; if (!parsePairList(arguments[++i], "=", &d_replaceattachments, &error)) { std::cerr << "[ Error parsing command line option `" << option << "': " << error << " ]" << std::endl; ok = false; } d_replaceattachments_bool = true; } else d_replaceattachments_bool = true; d_input_required = true; continue; } if (option == "-h" || option == "--help") { d_help = true; continue; } if (option == "--no-help") { d_help = false; continue; } if (option == "--scanmissingattachments") { d_scanmissingattachments = true; d_input_required = true; continue; } if (option == "--no-scanmissingattachments") { d_scanmissingattachments = false; continue; } if (option == "--showdbinfo") { d_showdbinfo = true; d_input_required = true; continue; } if (option == "--no-showdbinfo") { d_showdbinfo = false; continue; } if (option == "--scramble") { d_scramble = true; d_input_required = true; continue; } if (option == "--no-scramble") { d_scramble = false; continue; } if (option == "--importfromdesktop") { d_importfromdesktop = true; d_input_required = true; continue; } if (option == "--no-importfromdesktop") { d_importfromdesktop = false; continue; } if (option == "--limittodates") { if (i < argsize - 1) { std::regex validator("^(?:(?:[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})|[0-9]+), *(?:(?:[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})|[0-9]+)(?:, *(?:(?:[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})|[0-9]+), *(?:(?:[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})|[0-9]+))*$"); if (!std::regex_match(arguments[i + 1], validator)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; continue; } if (!parseStringList(arguments[++i], &d_limittodates)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--autolimitdates") { d_autolimitdates = true; continue; } if (option == "--no-autolimitdates") { d_autolimitdates = false; continue; } if (option == "--ignorewal") { d_ignorewal = true; continue; } if (option == "--no-ignorewal") { d_ignorewal = false; continue; } if (option == "--includemms") { d_includemms = true; continue; } if (option == "--no-includemms") { d_includemms = false; continue; } if (option == "--checkdbintegrity") { d_checkdbintegrity = true; d_input_required = true; continue; } if (option == "--no-checkdbintegrity") { d_checkdbintegrity = false; continue; } if (option == "--interactive") { d_interactive = true; continue; } if (option == "--no-interactive") { d_interactive = false; continue; } if (option == "--exporthtml") { if (i < argsize - 1) { d_exporthtml = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--exportdesktophtml") { if (i < argsize - 1) { d_exportdesktophtml = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--exportplaintextbackuphtml") { while (i < argsize - 1 && !isOption(arguments[i + 1])) { d_exportplaintextbackuphtml.emplace_back(std::move(arguments[++i])); } if (d_exportplaintextbackuphtml.size() < 2) { std::cerr << "[ Error parsing command line option `" << option << "': 2 arguments required, " << d_exportplaintextbackuphtml.size() << " provided ]" << std::endl; ok = false; } continue; } if (option == "--importplaintextbackup") { while (i < argsize - 1 && !isOption(arguments[i + 1])) { d_importplaintextbackup.emplace_back(std::move(arguments[++i])); } if (d_importplaintextbackup.size() < 1) { std::cerr << "[ Error parsing command line option `" << option << "': 1 arguments required, " << d_importplaintextbackup.size() << " provided ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--addexportdetails") { d_addexportdetails = true; continue; } if (option == "--no-addexportdetails") { d_addexportdetails = false; continue; } if (option == "--includecalllog") { d_includecalllog = true; continue; } if (option == "--no-includecalllog") { d_includecalllog = false; continue; } if (option == "--includeblockedlist") { d_includeblockedlist = true; continue; } if (option == "--no-includeblockedlist") { d_includeblockedlist = false; continue; } if (option == "--includesettings") { d_includesettings = true; continue; } if (option == "--no-includesettings") { d_includesettings = false; continue; } if (option == "--includefullcontactlist") { d_includefullcontactlist = true; continue; } if (option == "--no-includefullcontactlist") { d_includefullcontactlist = false; continue; } if (option == "--themeswitching") { d_themeswitching = true; continue; } if (option == "--no-themeswitching") { d_themeswitching = false; continue; } if (option == "--searchpage") { d_searchpage = true; continue; } if (option == "--no-searchpage") { d_searchpage = false; continue; } if (option == "--stickerpacks") { d_stickerpacks = true; continue; } if (option == "--no-stickerpacks") { d_stickerpacks = false; continue; } if (option == "--includereceipts") { d_includereceipts = true; continue; } if (option == "--no-includereceipts") { d_includereceipts = false; continue; } if (option == "--chatfolders") { d_chatfolders = true; continue; } if (option == "--no-chatfolders") { d_chatfolders = false; continue; } if (option == "--allhtmlpages") { d_includecalllog = true; d_includeblockedlist = true; d_includesettings = true; d_includefullcontactlist = true; d_themeswitching = true; d_searchpage = true; d_stickerpacks = true; d_addexportdetails = true; continue; } if (option == "--split") { d_split_bool = true; if (i < argsize - 1 && !isOption(arguments[i + 1])) { if (!ston(&d_split, arguments[++i])) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } d_split_by.clear(); continue; } if (option == "--split-by") { if (i < argsize - 1) { std::regex validator("year|month|week|day", std::regex::icase); if (!std::regex_match(arguments[i + 1], validator)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; continue; } d_split_by = std::move(arguments[++i]); d_split_bool = false; d_split = -1; } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--originalfilenames") { d_originalfilenames = true; continue; } if (option == "--no-originalfilenames") { d_originalfilenames = false; continue; } if (option == "--addincompletedataforhtmlexport") { d_addincompletedataforhtmlexport = true; continue; } if (option == "--no-addincompletedataforhtmlexport") { d_addincompletedataforhtmlexport = false; continue; } if (option == "--importdesktopcontacts") { d_importdesktopcontacts = true; continue; } if (option == "--no-importdesktopcontacts") { d_importdesktopcontacts = false; continue; } if (option == "--light") { d_light = true; continue; } if (option == "--no-light") { d_light = false; continue; } if (option == "--exporttxt") { if (i < argsize - 1) { d_exporttxt = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--exportdesktoptxt") { if (i < argsize - 1) { d_exportdesktoptxt = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--append") { d_append = true; continue; } if (option == "--no-append") { d_append = false; continue; } if (option == "--desktopdbversion") { if (i < argsize - 1) { if (!ston(&d_desktopdbversion, arguments[++i])) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--migratedb") { d_migratedb = true; continue; } if (option == "--no-migratedb") { d_migratedb = false; continue; } if (option == "--importstickers") { d_importstickers = true; continue; } if (option == "--no-importstickers") { d_importstickers = false; continue; } if (option == "--findrecipient") { if (i < argsize - 1) { if (!ston(&d_findrecipient, arguments[++i])) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--importtelegram" || option == "--importjson") { if (i < argsize - 1) { d_importtelegram = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } d_input_required = true; continue; } if (option == "--listjsonchats") { if (i < argsize - 1) { d_listjsonchats = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--selectjsonchats") { if (i < argsize - 1) { if (!parseNumberList(arguments[++i], &d_selectjsonchats, true)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--mapjsoncontacts") { if (i < argsize - 1) { std::string error; if (!parsePairList(arguments[++i], "=", &d_mapjsoncontacts, &error)) { std::cerr << "[ Error parsing command line option `" << option << "': " << error << " ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--preventjsonmapping") { if (i < argsize - 1) { if (!parseStringList(arguments[++i], &d_preventjsonmapping)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--jsonprependforward") { d_jsonprependforward = true; continue; } if (option == "--no-jsonprependforward") { d_jsonprependforward = false; continue; } if (option == "--jsonmarkdelivered") { d_jsonmarkdelivered = true; continue; } if (option == "--no-jsonmarkdelivered") { d_jsonmarkdelivered = false; continue; } if (option == "--jsonmarkread") { d_jsonmarkread = true; continue; } if (option == "--no-jsonmarkread") { d_jsonmarkread = false; continue; } if (option == "--xmlmarkdelivered") { d_xmlmarkdelivered = true; continue; } if (option == "--no-xmlmarkdelivered") { d_xmlmarkdelivered = false; continue; } if (option == "--xmlmarkread") { d_xmlmarkread = true; continue; } if (option == "--no-xmlmarkread") { d_xmlmarkread = false; continue; } if (option == "--fulldecode") { d_fulldecode = true; d_input_required = true; continue; } if (option == "--no-fulldecode") { d_fulldecode = false; continue; } if (option == "-l" || option == "--logfile") { if (i < argsize - 1) { d_logfile = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--custom_hugogithubs" || option == "--migrate214to215") { d_custom_hugogithubs = true; d_input_required = true; continue; } if (option == "--no-custom_hugogithubs") { d_custom_hugogithubs = false; continue; } if (option == "--truncate") { d_truncate = true; continue; } if (option == "--no-truncate") { d_truncate = false; continue; } if (option == "--skipmessagereorder") { d_skipmessagereorder = true; continue; } if (option == "--no-skipmessagereorder") { d_skipmessagereorder = false; continue; } if (option == "--migrate_to_191") { d_migrate_to_191 = true; d_input_required = true; continue; } if (option == "--no-migrate_to_191") { d_migrate_to_191 = false; continue; } if (option == "--mapxmlcontacts") { if (i < argsize - 1) { std::string error; if (!parsePairList(arguments[++i], "=", &d_mapxmlcontacts, &error)) { std::cerr << "[ Error parsing command line option `" << option << "': " << error << " ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--listxmlcontacts") { while (i < argsize - 1 && !isOption(arguments[i + 1])) { d_listxmlcontacts.emplace_back(std::move(arguments[++i])); } if (d_listxmlcontacts.size() < 1) { std::cerr << "[ Error parsing command line option `" << option << "': 1 arguments required, " << d_listxmlcontacts.size() << " provided ]" << std::endl; ok = false; } continue; } if (option == "--selectxmlchats") { if (i < argsize - 1) { if (!parseStringList(arguments[++i], &d_selectxmlchats)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--linkify") { d_linkify = true; continue; } if (option == "--no-linkify") { d_linkify = false; continue; } if (option == "--setchatcolors") { if (i < argsize - 1) { std::string error; if (!parsePairList(arguments[++i], "=", &d_setchatcolors, &error)) { std::cerr << "[ Error parsing command line option `" << option << "': " << error << " ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--mapxmlcontactnames") { if (i < argsize - 1) { std::string error; if (!parsePairList(arguments[++i], "=", &d_mapxmlcontactnames, &error)) { std::cerr << "[ Error parsing command line option `" << option << "': " << error << " ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--mapxmlcontactnamesfromfile") { if (i < argsize - 1) { d_mapxmlcontactnamesfromfile = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--mapxmladdresses") { if (i < argsize - 1) { std::string error; if (!parsePairList(arguments[++i], "=", &d_mapxmladdresses, &error)) { std::cerr << "[ Error parsing command line option `" << option << "': " << error << " ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--mapxmladdressesfromfile") { if (i < argsize - 1) { d_mapxmladdressesfromfile = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--xmlautogroupnames") { d_xmlautogroupnames = true; continue; } if (option == "--no-xmlautogroupnames") { d_xmlautogroupnames = false; continue; } if (option == "--setcountrycode") { if (i < argsize - 1) { d_setcountrycode = std::move(arguments[++i]); } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--compactfilenames") { d_compactfilenames = true; continue; } if (option == "--no-compactfilenames") { d_compactfilenames = false; continue; } if (option == "--generatedummy") { if (i < argsize - 1) { d_generatedummy = std::move(arguments[++i]); d_opassphrase = "000000000000000000000000000001"; } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--targetisdummy") { d_targetisdummy = true; d_passphrase = "000000000000000000000000000001"; d_opassphrase = "000000000000000000000000000001"; continue; } if (option == "--no-targetisdummy") { d_targetisdummy = false; continue; } if (option == "--htmlignoremediatypes") { if (i < argsize - 1) { if (!parseStringList(arguments[++i], &d_htmlignoremediatypes)) { std::cerr << "[ Error parsing command line option `" << option << "': Bad argument. ]" << std::endl; ok = false; } } else { std::cerr << "[ Error parsing command line option `" << option << "': Missing argument. ]" << std::endl; ok = false; } continue; } if (option == "--pagemenu") { d_pagemenu = true; continue; } if (option == "--no-pagemenu") { d_pagemenu = false; continue; } if (option[0] != '-') { if (d_positionals >= 2) { std::cerr << "[ Error parsing command line option `" << option << "': Unknown option. ]" << std::endl; ok = false; } if (i == 0) { d_input = std::move(option); //std::cout << "Got 'input' at pos " << i << std::endl; } else if (i == 1) { d_passphrase = std::move(option); //std::cout << "Got 'passphrase' at pos " << i << std::endl; } else { std::cerr << "[ Error parsing command line option `" << option << "': Unknown option. ]" << std::endl; ok = false; } ++d_positionals; continue; } std::cerr << "[ Error parsing command line option `" << option << "': Unknown option. ]" << std::endl; ok = false; } return ok; } signalbackup-tools-20250313-1/arg/arg.h000066400000000000000000001001231476450434500174160ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef ARGS_H_ #define ARGS_H_ #include #include #include #include #include #include #include #include #include #include class Arg { bool d_ok; std::array const d_alloptions{"-i", "--input", "-p", "--passphrase", "--importthreads", "--importthreadsbyname", "--limittothreads", "--limittothreadsbyname", "-o", "--output", "-op", "--opassphrase", "-s", "--source", "-sp", "--sourcepassphrase", "--croptothreads", "--croptothreadsbyname", "--croptodates", "--mergerecipients", "--mergegroups", "--exportcsv", "--exportxml", "--querymode", "--runsqlquery", "--runprettysqlquery", "--rundtsqlquery", "--rundtprettysqlquery", "--limitcontacts", "--assumebadframesizeonbadmac", "--no-assumebadframesizeonbadmac", "--editattachmentsize", "--dumpdesktopdb", "--desktopdir", "--desktopdirs", "--rawdesktopdb", "--desktopkey", "--showdesktopkey", "--no-showdesktopkey", "--dumpmedia", "--excludestickers", "--no-excludestickers", "--dumpavatars", "--devcustom", "--no-devcustom", "--importcsv", "--mapcsvfields", "--setselfid", "--onlydb", "--no-onlydb", "--overwrite", "--no-overwrite", "--listthreads", "--no-listthreads", "--listrecipients", "--no-listrecipients", "--showprogress", "--no-showprogress", "--removedoubles", "--reordermmssmsids", "--no-reordermmssmsids", "--stoponerror", "--no-stoponerror", "-v", "--verbose", "--no-verbose", "--dbusverbose", "--no-dbusverbose", "--strugee", "--strugee3", "--ashmorgan", "--no-ashmorgan", "--strugee2", "--no-strugee2", "--hiperfall", "--arc", "--deleteattachments", "--no-deleteattachments", "--onlyinthreads", "--onlyolderthan", "--onlynewerthan", "--onlylargerthan", "--onlytype", "--appendbody", "--prependbody", "--replaceattachments", "-h", "--help", "--no-help", "--scanmissingattachments", "--no-scanmissingattachments", "--showdbinfo", "--no-showdbinfo", "--scramble", "--no-scramble", "--importfromdesktop", "--no-importfromdesktop", "--limittodates", "--autolimitdates", "--no-autolimitdates", "--ignorewal", "--no-ignorewal", "--includemms", "--no-includemms", "--checkdbintegrity", "--no-checkdbintegrity", "--interactive", "--no-interactive", "--exporthtml", "--exportdesktophtml", "--exportplaintextbackuphtml", "--importplaintextbackup", "--addexportdetails", "--no-addexportdetails", "--includecalllog", "--no-includecalllog", "--includeblockedlist", "--no-includeblockedlist", "--includesettings", "--no-includesettings", "--includefullcontactlist", "--no-includefullcontactlist", "--themeswitching", "--no-themeswitching", "--searchpage", "--no-searchpage", "--stickerpacks", "--no-stickerpacks", "--includereceipts", "--no-includereceipts", "--chatfolders", "--no-chatfolders", "--allhtmlpages", "--split", "--split-by", "--originalfilenames", "--no-originalfilenames", "--addincompletedataforhtmlexport", "--no-addincompletedataforhtmlexport", "--importdesktopcontacts", "--no-importdesktopcontacts", "--light", "--no-light", "--exporttxt", "--exportdesktoptxt", "--append", "--no-append", "--desktopdbversion", "--migratedb", "--no-migratedb", "--importstickers", "--no-importstickers", "--findrecipient", "--importtelegram", "--listjsonchats", "--selectjsonchats", "--mapjsoncontacts", "--preventjsonmapping", "--jsonprependforward", "--no-jsonprependforward", "--jsonmarkdelivered", "--no-jsonmarkdelivered", "--jsonmarkread", "--no-jsonmarkread", "--xmlmarkdelivered", "--no-xmlmarkdelivered", "--xmlmarkread", "--no-xmlmarkread", "--fulldecode", "--no-fulldecode", "-l", "--logfile", "--custom_hugogithubs", "--no-custom_hugogithubs", "--truncate", "--no-truncate", "--skipmessagereorder", "--no-skipmessagereorder", "--migrate_to_191", "--no-migrate_to_191", "--mapxmlcontacts", "--listxmlcontacts", "--selectxmlchats", "--linkify", "--no-linkify", "--setchatcolors", "--mapxmlcontactnames", "--mapxmlcontactnamesfromfile", "--mapxmladdresses", "--mapxmladdressesfromfile", "--xmlautogroupnames", "--no-xmlautogroupnames", "--setcountrycode", "--compactfilenames", "--no-compactfilenames", "--generatedummy", "--targetisdummy", "--no-targetisdummy", "--htmlignoremediatypes", "--pagemenu", "--no-pagemenu"}; size_t d_positionals; size_t d_maxpositional; std::string d_progname; std::string d_input; std::string d_passphrase; std::vector d_importthreads; std::vector d_importthreadsbyname; std::vector d_limittothreads; std::vector d_limittothreadsbyname; std::string d_output; std::string d_opassphrase; std::string d_source; std::string d_sourcepassphrase; std::vector d_croptothreads; std::vector d_croptothreadsbyname; std::vector d_croptodates; std::vector d_mergerecipients; std::vector d_mergegroups; std::vector> d_exportcsv; std::string d_exportxml; std::string d_querymode; std::vector d_runsqlquery; std::vector d_runprettysqlquery; std::vector d_rundtsqlquery; std::vector d_rundtprettysqlquery; std::vector d_limitcontacts; bool d_assumebadframesizeonbadmac; std::vector d_editattachmentsize; std::string d_dumpdesktopdb; std::string d_desktopdir; std::string d_desktopdirs_1; std::string d_desktopdirs_2; std::string d_rawdesktopdb; std::string d_desktopkey; bool d_showdesktopkey; std::string d_dumpmedia; bool d_excludestickers; std::string d_dumpavatars; bool d_devcustom; std::string d_importcsv; std::vector> d_mapcsvfields; std::string d_setselfid; bool d_onlydb; bool d_overwrite; bool d_listthreads; bool d_listrecipients; bool d_showprogress; int d_removedoubles; bool d_removedoubles_bool; bool d_reordermmssmsids; bool d_stoponerror; bool d_verbose; bool d_dbusverbose; long long int d_strugee; long long int d_strugee3; bool d_ashmorgan; bool d_strugee2; long long int d_hiperfall; long long int d_arc; bool d_deleteattachments; std::vector d_onlyinthreads; std::string d_onlyolderthan; std::string d_onlynewerthan; long long int d_onlylargerthan; std::vector d_onlytype; std::string d_appendbody; std::string d_prependbody; std::vector> d_replaceattachments; bool d_replaceattachments_bool; bool d_help; bool d_scanmissingattachments; bool d_showdbinfo; bool d_scramble; bool d_importfromdesktop; std::vector d_limittodates; bool d_autolimitdates; bool d_ignorewal; bool d_includemms; bool d_checkdbintegrity; bool d_interactive; std::string d_exporthtml; std::string d_exportdesktophtml; std::vector d_exportplaintextbackuphtml; std::vector d_importplaintextbackup; bool d_addexportdetails; bool d_includecalllog; bool d_includeblockedlist; bool d_includesettings; bool d_includefullcontactlist; bool d_themeswitching; bool d_searchpage; bool d_stickerpacks; bool d_includereceipts; bool d_chatfolders; long long int d_split; bool d_split_bool; std::string d_split_by; bool d_originalfilenames; bool d_addincompletedataforhtmlexport; bool d_importdesktopcontacts; bool d_light; std::string d_exporttxt; std::string d_exportdesktoptxt; bool d_append; long long int d_desktopdbversion; bool d_migratedb; bool d_importstickers; long long int d_findrecipient; std::string d_importtelegram; std::string d_listjsonchats; std::vector d_selectjsonchats; std::vector> d_mapjsoncontacts; std::vector d_preventjsonmapping; bool d_jsonprependforward; bool d_jsonmarkdelivered; bool d_jsonmarkread; bool d_xmlmarkdelivered; bool d_xmlmarkread; bool d_fulldecode; std::string d_logfile; bool d_custom_hugogithubs; bool d_truncate; bool d_skipmessagereorder; bool d_migrate_to_191; std::vector> d_mapxmlcontacts; std::vector d_listxmlcontacts; std::vector d_selectxmlchats; bool d_linkify; std::vector> d_setchatcolors; std::vector> d_mapxmlcontactnames; std::string d_mapxmlcontactnamesfromfile; std::vector> d_mapxmladdresses; std::string d_mapxmladdressesfromfile; bool d_xmlautogroupnames; std::string d_setcountrycode; bool d_compactfilenames; std::string d_generatedummy; bool d_targetisdummy; std::vector d_htmlignoremediatypes; bool d_pagemenu; bool d_input_required; public: Arg(int argc, char *argv[]); inline Arg(Arg const &other) = delete; inline Arg &operator=(Arg const &other) = delete; inline bool ok() const; void usage() const; inline std::string const &input() const; inline std::string const &passphrase() const; inline void setpassphrase(std::string const &val); inline std::vector const &importthreads() const; inline std::vector const &importthreadsbyname() const; inline std::vector const &limittothreads() const; inline std::vector const &limittothreadsbyname() const; inline std::string const &output() const; inline std::string const &opassphrase() const; inline void setopassphrase(std::string const &val); inline std::string const &source() const; inline std::string const &sourcepassphrase() const; inline void setsourcepassphrase(std::string const &val); inline std::vector const &croptothreads() const; inline std::vector const &croptothreadsbyname() const; inline std::vector const &croptodates() const; inline std::vector const &mergerecipients() const; inline std::vector const &mergegroups() const; inline std::vector> const &exportcsv() const; inline std::string const &exportxml() const; inline std::string const &querymode() const; inline std::vector const &runsqlquery() const; inline std::vector const &runprettysqlquery() const; inline std::vector const &rundtsqlquery() const; inline std::vector const &rundtprettysqlquery() const; inline std::vector const &limitcontacts() const; inline bool assumebadframesizeonbadmac() const; inline std::vector const &editattachmentsize() const; inline std::string const &dumpdesktopdb() const; inline std::string const &desktopdir() const; inline std::string const &desktopdirs_1() const; inline std::string const &desktopdirs_2() const; inline std::string const &rawdesktopdb() const; inline std::string const &desktopkey() const; inline bool showdesktopkey() const; inline std::string const &dumpmedia() const; inline bool excludestickers() const; inline std::string const &dumpavatars() const; inline bool devcustom() const; inline std::string const &importcsv() const; inline std::vector> const &mapcsvfields() const; inline std::string const &setselfid() const; inline bool onlydb() const; inline bool overwrite() const; inline bool listthreads() const; inline bool listrecipients() const; inline bool showprogress() const; inline int removedoubles() const; inline bool removedoubles_bool() const; inline bool reordermmssmsids() const; inline bool stoponerror() const; inline bool verbose() const; inline bool dbusverbose() const; inline long long int strugee() const; inline long long int strugee3() const; inline bool ashmorgan() const; inline bool strugee2() const; inline long long int hiperfall() const; inline long long int arc() const; inline bool deleteattachments() const; inline std::vector const &onlyinthreads() const; inline std::string const &onlyolderthan() const; inline std::string const &onlynewerthan() const; inline long long int onlylargerthan() const; inline std::vector const &onlytype() const; inline std::string const &appendbody() const; inline std::string const &prependbody() const; inline std::vector> const &replaceattachments() const; inline bool replaceattachments_bool() const; inline bool help() const; inline bool scanmissingattachments() const; inline bool showdbinfo() const; inline bool scramble() const; inline bool importfromdesktop() const; inline std::vector const &limittodates() const; inline bool autolimitdates() const; inline bool ignorewal() const; inline bool includemms() const; inline bool checkdbintegrity() const; inline bool interactive() const; inline std::string const &exporthtml() const; inline std::string const &exportdesktophtml() const; inline std::vector const &exportplaintextbackuphtml() const; inline std::vector const &importplaintextbackup() const; inline bool addexportdetails() const; inline bool includecalllog() const; inline bool includeblockedlist() const; inline bool includesettings() const; inline bool includefullcontactlist() const; inline bool themeswitching() const; inline bool searchpage() const; inline bool stickerpacks() const; inline bool includereceipts() const; inline bool chatfolders() const; inline long long int split() const; inline bool split_bool() const; inline std::string const &split_by() const; inline bool originalfilenames() const; inline bool addincompletedataforhtmlexport() const; inline bool importdesktopcontacts() const; inline bool light() const; inline std::string const &exporttxt() const; inline std::string const &exportdesktoptxt() const; inline bool append() const; inline long long int desktopdbversion() const; inline bool migratedb() const; inline bool importstickers() const; inline long long int findrecipient() const; inline std::string const &importtelegram() const; inline std::string const &listjsonchats() const; inline std::vector const &selectjsonchats() const; inline std::vector> const &mapjsoncontacts() const; inline std::vector const &preventjsonmapping() const; inline bool jsonprependforward() const; inline bool jsonmarkdelivered() const; inline bool jsonmarkread() const; inline bool xmlmarkdelivered() const; inline bool xmlmarkread() const; inline bool fulldecode() const; inline std::string const &logfile() const; inline bool custom_hugogithubs() const; inline bool truncate() const; inline bool skipmessagereorder() const; inline bool migrate_to_191() const; inline std::vector> const &mapxmlcontacts() const; inline std::vector const &listxmlcontacts() const; inline std::vector const &selectxmlchats() const; inline bool linkify() const; inline std::vector> const &setchatcolors() const; inline std::vector> const &mapxmlcontactnames() const; inline std::string const &mapxmlcontactnamesfromfile() const; inline std::vector> const &mapxmladdresses() const; inline std::string const &mapxmladdressesfromfile() const; inline bool xmlautogroupnames() const; inline std::string const &setcountrycode() const; inline bool compactfilenames() const; inline std::string const &generatedummy() const; inline bool targetisdummy() const; inline std::vector const &htmlignoremediatypes() const; inline bool pagemenu() const; inline bool input_required() const; private: template bool ston(T *t, std::string const &str) const; bool parseArgs(std::vector const &args); inline bool parseStringList(std::string const &strlist, std::vector *list) const; template inline bool parsePairList(std::string const &pairlist, std::string const &delim, std::vector> *list, std::string *error) const; template bool parseNumberList(std::string const &strlist, std::vector *list, bool sort, std::vector *faillist = nullptr) const; template bool parsePair(std::string const &token, std::string const &delim, std::pair *pair, std::string *error) const; template bool parseNumberListToken(std::string const &token, std::vector *list) const; inline bool isOption(std::string const &str) const; }; inline std::string const &Arg::input() const { return d_input; } inline std::string const &Arg::passphrase() const { return d_passphrase; } inline void Arg::setpassphrase(std::string const &val) { d_passphrase = val; } inline std::vector const &Arg::importthreads() const { return d_importthreads; } inline std::vector const &Arg::importthreadsbyname() const { return d_importthreadsbyname; } inline std::vector const &Arg::limittothreads() const { return d_limittothreads; } inline std::vector const &Arg::limittothreadsbyname() const { return d_limittothreadsbyname; } inline std::string const &Arg::output() const { return d_output; } inline std::string const &Arg::opassphrase() const { return d_opassphrase; } inline void Arg::setopassphrase(std::string const &val) { d_opassphrase = val; } inline std::string const &Arg::source() const { return d_source; } inline std::string const &Arg::sourcepassphrase() const { return d_sourcepassphrase; } inline void Arg::setsourcepassphrase(std::string const &val) { d_sourcepassphrase = val; } inline std::vector const &Arg::croptothreads() const { return d_croptothreads; } inline std::vector const &Arg::croptothreadsbyname() const { return d_croptothreadsbyname; } inline std::vector const &Arg::croptodates() const { return d_croptodates; } inline std::vector const &Arg::mergerecipients() const { return d_mergerecipients; } inline std::vector const &Arg::mergegroups() const { return d_mergegroups; } inline std::vector> const &Arg::exportcsv() const { return d_exportcsv; } inline std::string const &Arg::exportxml() const { return d_exportxml; } inline std::string const &Arg::querymode() const { return d_querymode; } inline std::vector const &Arg::runsqlquery() const { return d_runsqlquery; } inline std::vector const &Arg::runprettysqlquery() const { return d_runprettysqlquery; } inline std::vector const &Arg::rundtsqlquery() const { return d_rundtsqlquery; } inline std::vector const &Arg::rundtprettysqlquery() const { return d_rundtprettysqlquery; } inline std::vector const &Arg::limitcontacts() const { return d_limitcontacts; } inline bool Arg::assumebadframesizeonbadmac() const { return d_assumebadframesizeonbadmac; } inline std::vector const &Arg::editattachmentsize() const { return d_editattachmentsize; } inline std::string const &Arg::dumpdesktopdb() const { return d_dumpdesktopdb; } inline std::string const &Arg::desktopdir() const { return d_desktopdir; } inline std::string const &Arg::desktopdirs_1() const { return d_desktopdirs_1; } inline std::string const &Arg::desktopdirs_2() const { return d_desktopdirs_2; } inline std::string const &Arg::rawdesktopdb() const { return d_rawdesktopdb; } inline std::string const &Arg::desktopkey() const { return d_desktopkey; } inline bool Arg::showdesktopkey() const { return d_showdesktopkey; } inline std::string const &Arg::dumpmedia() const { return d_dumpmedia; } inline bool Arg::excludestickers() const { return d_excludestickers; } inline std::string const &Arg::dumpavatars() const { return d_dumpavatars; } inline bool Arg::devcustom() const { return d_devcustom; } inline std::string const &Arg::importcsv() const { return d_importcsv; } inline std::vector> const &Arg::mapcsvfields() const { return d_mapcsvfields; } inline std::string const &Arg::setselfid() const { return d_setselfid; } inline bool Arg::onlydb() const { return d_onlydb; } inline bool Arg::overwrite() const { return d_overwrite; } inline bool Arg::listthreads() const { return d_listthreads; } inline bool Arg::listrecipients() const { return d_listrecipients; } inline bool Arg::showprogress() const { return d_showprogress; } inline int Arg::removedoubles() const { return d_removedoubles; } inline bool Arg::removedoubles_bool() const { return d_removedoubles_bool; } inline bool Arg::reordermmssmsids() const { return d_reordermmssmsids; } inline bool Arg::stoponerror() const { return d_stoponerror; } inline bool Arg::verbose() const { return d_verbose; } inline bool Arg::dbusverbose() const { return d_dbusverbose; } inline long long int Arg::strugee() const { return d_strugee; } inline long long int Arg::strugee3() const { return d_strugee3; } inline bool Arg::ashmorgan() const { return d_ashmorgan; } inline bool Arg::strugee2() const { return d_strugee2; } inline long long int Arg::hiperfall() const { return d_hiperfall; } inline long long int Arg::arc() const { return d_arc; } inline bool Arg::deleteattachments() const { return d_deleteattachments; } inline std::vector const &Arg::onlyinthreads() const { return d_onlyinthreads; } inline std::string const &Arg::onlyolderthan() const { return d_onlyolderthan; } inline std::string const &Arg::onlynewerthan() const { return d_onlynewerthan; } inline long long int Arg::onlylargerthan() const { return d_onlylargerthan; } inline std::vector const &Arg::onlytype() const { return d_onlytype; } inline std::string const &Arg::appendbody() const { return d_appendbody; } inline std::string const &Arg::prependbody() const { return d_prependbody; } inline std::vector> const &Arg::replaceattachments() const { return d_replaceattachments; } inline bool Arg::replaceattachments_bool() const { return d_replaceattachments_bool; } inline bool Arg::help() const { return d_help; } inline bool Arg::scanmissingattachments() const { return d_scanmissingattachments; } inline bool Arg::showdbinfo() const { return d_showdbinfo; } inline bool Arg::scramble() const { return d_scramble; } inline bool Arg::importfromdesktop() const { return d_importfromdesktop; } inline std::vector const &Arg::limittodates() const { return d_limittodates; } inline bool Arg::autolimitdates() const { return d_autolimitdates; } inline bool Arg::ignorewal() const { return d_ignorewal; } inline bool Arg::includemms() const { return d_includemms; } inline bool Arg::checkdbintegrity() const { return d_checkdbintegrity; } inline bool Arg::interactive() const { return d_interactive; } inline std::string const &Arg::exporthtml() const { return d_exporthtml; } inline std::string const &Arg::exportdesktophtml() const { return d_exportdesktophtml; } inline std::vector const &Arg::exportplaintextbackuphtml() const { return d_exportplaintextbackuphtml; } inline std::vector const &Arg::importplaintextbackup() const { return d_importplaintextbackup; } inline bool Arg::addexportdetails() const { return d_addexportdetails; } inline bool Arg::includecalllog() const { return d_includecalllog; } inline bool Arg::includeblockedlist() const { return d_includeblockedlist; } inline bool Arg::includesettings() const { return d_includesettings; } inline bool Arg::includefullcontactlist() const { return d_includefullcontactlist; } inline bool Arg::themeswitching() const { return d_themeswitching; } inline bool Arg::searchpage() const { return d_searchpage; } inline bool Arg::stickerpacks() const { return d_stickerpacks; } inline bool Arg::includereceipts() const { return d_includereceipts; } inline bool Arg::chatfolders() const { return d_chatfolders; } inline long long int Arg::split() const { return d_split; } inline bool Arg::split_bool() const { return d_split_bool; } inline std::string const &Arg::split_by() const { return d_split_by; } inline bool Arg::originalfilenames() const { return d_originalfilenames; } inline bool Arg::addincompletedataforhtmlexport() const { return d_addincompletedataforhtmlexport; } inline bool Arg::importdesktopcontacts() const { return d_importdesktopcontacts; } inline bool Arg::light() const { return d_light; } inline std::string const &Arg::exporttxt() const { return d_exporttxt; } inline std::string const &Arg::exportdesktoptxt() const { return d_exportdesktoptxt; } inline bool Arg::append() const { return d_append; } inline long long int Arg::desktopdbversion() const { return d_desktopdbversion; } inline bool Arg::migratedb() const { return d_migratedb; } inline bool Arg::importstickers() const { return d_importstickers; } inline long long int Arg::findrecipient() const { return d_findrecipient; } inline std::string const &Arg::importtelegram() const { return d_importtelegram; } inline std::string const &Arg::listjsonchats() const { return d_listjsonchats; } inline std::vector const &Arg::selectjsonchats() const { return d_selectjsonchats; } inline std::vector> const &Arg::mapjsoncontacts() const { return d_mapjsoncontacts; } inline std::vector const &Arg::preventjsonmapping() const { return d_preventjsonmapping; } inline bool Arg::jsonprependforward() const { return d_jsonprependforward; } inline bool Arg::jsonmarkdelivered() const { return d_jsonmarkdelivered; } inline bool Arg::jsonmarkread() const { return d_jsonmarkread; } inline bool Arg::xmlmarkdelivered() const { return d_xmlmarkdelivered; } inline bool Arg::xmlmarkread() const { return d_xmlmarkread; } inline bool Arg::fulldecode() const { return d_fulldecode; } inline std::string const &Arg::logfile() const { return d_logfile; } inline bool Arg::custom_hugogithubs() const { return d_custom_hugogithubs; } inline bool Arg::truncate() const { return d_truncate; } inline bool Arg::skipmessagereorder() const { return d_skipmessagereorder; } inline bool Arg::migrate_to_191() const { return d_migrate_to_191; } inline std::vector> const &Arg::mapxmlcontacts() const { return d_mapxmlcontacts; } inline std::vector const &Arg::listxmlcontacts() const { return d_listxmlcontacts; } inline std::vector const &Arg::selectxmlchats() const { return d_selectxmlchats; } inline bool Arg::linkify() const { return d_linkify; } inline std::vector> const &Arg::setchatcolors() const { return d_setchatcolors; } inline std::vector> const &Arg::mapxmlcontactnames() const { return d_mapxmlcontactnames; } inline std::string const &Arg::mapxmlcontactnamesfromfile() const { return d_mapxmlcontactnamesfromfile; } inline std::vector> const &Arg::mapxmladdresses() const { return d_mapxmladdresses; } inline std::string const &Arg::mapxmladdressesfromfile() const { return d_mapxmladdressesfromfile; } inline bool Arg::xmlautogroupnames() const { return d_xmlautogroupnames; } inline std::string const &Arg::setcountrycode() const { return d_setcountrycode; } inline bool Arg::compactfilenames() const { return d_compactfilenames; } inline std::string const &Arg::generatedummy() const { return d_generatedummy; } inline bool Arg::targetisdummy() const { return d_targetisdummy; } inline std::vector const &Arg::htmlignoremediatypes() const { return d_htmlignoremediatypes; } inline bool Arg::pagemenu() const { return d_pagemenu; } inline bool Arg::input_required() const { return d_input_required; } inline bool Arg::ok() const { return d_ok; } template bool Arg::ston(T *t, std::string const &str) const { std::istringstream iss(str); return !(iss >> *t).fail(); } inline bool Arg::parseStringList(std::string const &strlist, std::vector *list) const { std::string tr = strlist; size_t start = 0; size_t pos = 0; while ((pos = tr.find(',', start)) != std::string::npos) { list->emplace_back(tr.substr(start, pos - start)); start = pos + 1; } list->emplace_back(tr.substr(start)); return true; } template inline bool Arg::parsePairList(std::string const &pairlist, std::string const &delim, std::vector> *list, std::string *error) const { std::string tr = pairlist; size_t start = 0; size_t pos = 0; while ((pos = tr.find(',', start)) != std::string::npos) { std::pair tmp; if (!parsePair(tr.substr(start, pos - start), delim, &tmp, error)) return false; list->emplace_back(std::move(tmp)); start = pos + 1; } std::pair tmp; if (!parsePair(tr.substr(start), delim, &tmp, error)) return false; list->emplace_back(std::move(tmp)); return true; } template bool Arg::parseNumberList(std::string const &strlist, std::vector *list, bool sort, std::vector *faillist) const { std::string tr = strlist; size_t start = 0; size_t pos = 0; while ((pos = tr.find(',', start)) != std::string::npos) { if (!parseNumberListToken(tr.substr(start, pos - start), list)) // get&parse token { if (faillist) faillist->emplace_back(tr.substr(start, pos - start)); else return false; } start = pos + 1; } if (!parseNumberListToken(tr.substr(start), list)) // get last bit { if (faillist) faillist->emplace_back(tr.substr(start)); else return false; } if (sort) std::sort(list->begin(), list->end()); return true; } template bool Arg::parsePair(std::string const &token, std::string const &delim, std::pair *pair, std::string *error) const { std::string::size_type pos = token.find(delim); if (pos == std::string::npos) { *error = "Delimiter not found."; return false; } std::string first(token, 0, pos); std::string second(token, pos + 1); if (first.empty() || second.empty()) { *error = "Empty field in pair."; return false; } if constexpr (std::is_same::value) pair->first = first; else { if (!ston(&(pair->first), first)) { *error = "Bad argument."; return false; } } if constexpr (std::is_same::value) pair->second = second; else { if (!ston(&(pair->second), second)) { *error = "Bad argument."; return false; } } return true; } template bool Arg::parseNumberListToken(std::string const &token, std::vector *list) const { size_t pos = 0; T beg = -1; // try and get first number if ((pos = token.find('-')) != std::string::npos) { if (!ston(&beg, token.substr(0, pos))) return false; // then there must be a second number T end = -1; if (!ston(&end, token.substr(pos + 1))) return false; if (beg > end) return false; for (int i = beg; i <= end ; ++i) list->push_back(i); } else { if (!ston(&beg, token)) return false; else list->push_back(beg); } return true; } inline bool Arg::isOption(std::string const &str) const { return std::find(d_alloptions.begin(), d_alloptions.end(), str) != d_alloptions.end(); } #endif signalbackup-tools-20250313-1/arg/usage.cc000066400000000000000000000612271476450434500201220ustar00rootroot00000000000000/* Copyright (C) 2022-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "arg.h" void Arg::usage() const { std::cout << R"*( Usage: )*" + d_progname + R"*( [] [OPTIONS] must be either a regular file, a backup file as exported by the Signal Android app, or a directory containing an unpacked backup as created by this program. In case the input is a regular file, a is required. If it is omitted from the command line, a prompt is presented during runtime. Be sure to read the README at https://github.com/bepaald/signalbackup-tools/ for more detailed instructions for the core functions and examples. Note: this program never modifies the input, if you wish to alter the backup in any way and save your changes, you must provide one of the output options. = COMMON OPTIONS = -i, --input If for whatever reason you do not wish to pass the input as the first argument, you can use this option anywhere in the list of arguments -p, --passphrase If for whatever reason you do not wish to pass the input as the second argument, you can use this option anywhere in the list of arguments. If this option is omitted, but is a regular file, a prompt is presented to enter the passphrase at runtime (also see `--interactive'). --no-showprogress Do not output the progress percentage. Especially useful when redirecting output to file. -h, --help Show this help message -l, --logfile Write programs output to file . If the output file exists, it will be overwritten without warning. --interactive Prompt for all passphrases --runsqlquery Run against the backup's internal SQL database. --runprettysqlquery As above, but try show output in a pretty table. If the output is not too large for your terminal, this is often much more readable. --no-truncate By default, `--runprettysqlquery' truncates table columns to fit the terminal. With this option, this can be disabled, useful when redirecting output to file (or using the `--logfile' option. --listthreads List the threads in the database with their `_id' numbers. These id's are required input for various other options. --listrecipients List all recipients in the database with their `_id'. These id's are required input for various other options. --setselfid Various options need to know which recipient in the backup is 'self': the originator of the backup. These functions generally scan the backup to automatically determine the correct recipient. If this fails, this option can be used to set the of the backups owner, in the format it appears in the database, usually `+12345678901'. = OUTPUT OPTIONS = -o, --output Either a file or a directory. When output is a file, this will be a normal backup file, compatible with the Signal Android app. When output is a directory, the backup's separate parts (frames, SQL database and media) are written to that directory unencrypted. This directory can later be used as to create a working backup file. -op, --opassphrase When output is a file, this will be the backups passphrase. May be omitted (in which case the passphrase is used. --onlydb Optional modifier for `--output', when is a directory. This causes only the SQLite database to be written to disk. --dumpmedia Save all media attachments to DIRECTORY. An attempt is made to give each attachment a correct name and timestamp as well as to place the attachments in sub-folders for each conversation. --limittothreads Optional modifier for `--dumpmedia'. Only save the attachments from the listed threads. List format same as `--croptothreads' --limittothreadsbyname Optional modifier for `--dumpmedia'. Only save the attachments from the listed threads. List format same as `--croptothreadsbyname' --limittodates Optional modifier for `--dumpmedia'. Only export messages within the ranges defined by LIST_OF_DATES. List format is the same as `--croptodates'. --excludestickers Optional modifier for `--dumpmedia'. Exclude stickers from the media dump. --dumpavatars Save all avatars to DIRECTORY. --limitcontacts Optional modifier for `--dumpavatars'. Only the avatars of listed contacts are saved. CONTACTS is a list "Name 1,Name 2(,...)", where each name is exactly as it appears in Signal's conversation overview or from this program's `--listrecipients' output. --exportxml Export the messages from the internal sms table to XML file FILE.)*" // --includemms // --includeattachmentdata // --setselfid R"*( --exporthtml Export the messages to HTML files. Each conversation will be placed in a separate subdirectory. --limittothreads Optional modifier for `--exporthtml'. Only export the listed threads. List format same as `--croptothreads' --limittothreadsbyname Optional modifier for `--exporthtml'. Only save the attachments from the listed threads. List format same as `--croptothreadsbyname' --limittodates Optional modifier for `--exporthtml'. Only export messages within the ranges defined by LIST_OF_DATES. List format is the same as `--croptodates'. --migratedb Optional modifier for `--exporthtml'. Some older databases require this option to (attempt) to update the database format to a newer supported format. The program will tell you when you need to add this option. --append Optional modifier for `--exporthtml'. Causes `--exporthtml' to not show an error when DIRECTORY is not empty, but also not overwrite existing media files. Still regenerates and overwrites existing HTML files. --split [N] Optional modifier for `--exporthtml'. Splits the generated HTML files to a maximum of N messages per page. By default, the pages are not split. When this option is given without a value for N, N is 1000. Can not be combined with `--split-by`. --split-by [PERIOD] Optional modifier for `--exporthtml'. Splits the generated HTML files by calendar PERIOD. Supported values for PERIOD are 'year', 'month', 'week', and 'day'. Can not be combined with `--split`. --light By default a dark theme is used for the rendered HTML. Add this option to output in a light theme instead. --includereceipts Optional modifier for `--exporthtml'. Adds available info from message receipts to the HTML page. Note, this potentionally slows down page loading for large conversations significantly. --originalfilenames Optional modifier for `--exporthtml'. Use the original filenames for attached media when available. This option can not be used together with `--append'. --compactfilenames Use (very) short filenames for the generated HTML pages. May be of use when running into maximum path length limitations on Windows. --allhtmlpages Optional modifier for `--exporthtml'. Convenience option that enables all the modifying options for `--exporthtml' listed below. --themeswitching Optional modifier for `--exporthtml'. Adds a button to the HTML to switch the theme between light and dark. This adds a bit of JavaScript to the page, and sets a cookie when switching. --searchpage Optional modifier for `--exporthtml'. Generates a page from where conversations can be searched. This adds JavaScript to the page. Also, a search index is generated to facilitate searching. --includecalllog Optional modifier for `--exporthtml'. Generate a call log-page. --stickerpacks Optional modifier for `--exporthtml'. Generate an overview of installed and known stickerpacks. --includeblockedlist Optional modifier for `--exporthtml'. Generate an overview of blocked contacts in the database. --includesettings Optional modifier for `--exporthtml'. Generate a page showing settings found in the backup file. --includefullcontactlist Optional modifier for `--exporthtml'. Generate a page showing all contacts present in the backups database. These include hidden and blocked contacts, but also system contacts who might not use Signal at all. --addexportdetails Optional modifier for `--exporthtml'. Adds some metadata about this tools and the backup to the pages when printing. --linkify Optional modifier for `--exporthtml'. Attempts to turn URLs in messages actual clickable links. This option is enabled by deault, and can be disabled by `--no-linkify'. --chatfolders Optional modifier for `--exporthtml'. Exports chat folders. This option may interact poorly with the `--limitto[xxx]' options. --exporttxt Export the messages to plain text file. Attachments are omitted. This option also supports the `--limittothreads', `--limittothreadbybame', `--limittodates', and `--migratedb' modifiers as mentioned above. --exportcsv Export the database to file of comma separated values. Argument: "tablename1=filename1,tablename2=filename2(,...)" --overwrite Optional modifier for all output operations. Overwrite output files if they exist. When is a directory this will delete ALL directory contents. = EDITING OPTIONS = --croptothreads Crop database to list of threads. The list supports comma separated numbers or ranges (for example: "1,3,4-8,13") or the special keyword `ALL'. Threads are specified by their id (see: `--listthreads'). --croptothreadsbyname Crop database to list of threads. The list is a comma separated list of conversation names (for example: "Alice","Bobby C.") --croptodates Crop database to certain time periods. The list of dates is structured as `begindate1,enddate1(,begindate2,enddate2,...)', where a date is either "YYYY-MM-DD hh:mm:ss" or a date in milliseconds since epoch --importthreads Import LIST_OF_THREADS into database, the list format is the same as `--croptothreads'. This operation requires the `--source' option to be passed as well. -s, --source Required modifier for `--importthreads'. The source backup from which to import threads (see `--importthreads'). The input can be a file or directory. When it is a file, a passphrase is required -sp, --sourcepassphrase The 30 digit passphrase for the backup file specified by `--source'. --importfromdesktop Import messages from Signal Desktop. If the program fails to find your Signal-Desktop installation or it is in a non-standard location, the optional [DIR1] and [DIR2] can be provided. See the README for more information. --dumpdesktopdb Decrypt the Signal Desktop database and saves it to . --desktopdir Optional modifier for `--importfromdesktop` and `--dumpdesktopdb`. If the program fails to find your Signal-Desktop installation or it is in a non-standard location can be provided. See the README for more information about default locations. --ignorewal Optional modifier for `--importfromdesktop' and `--dumpdesktopdb`. Ignores an existing WAL file when opening Signal Desktop database. --limittodates Optional modifier for `--importfromdesktop'. Limit the messages imported to the specified date ranges. The format of the list of list of dates is the same as `--croptodates'. --autolimitdates Optional modifier for `--importfromdesktop'. Automatically limit the import of messages to those older than the first message in the INPUT backup file. --desktopkey Provide the decrypted SQLCipher key for decrypting the desktop database (see README). --showdesktopkey Show the (hex) SQLCipher key used for the desktop database. --deleteattachments Delete attachments from backup file. --onlyinthreads Optional modifier for `--deleteattachments' and `--replaceattachments'. Only deal with attachments within these threads. For list format see `--croptothreads'. --onlyolderthan Optional modifier for `--deleteattachments' and `--replaceattachments'. Only deal with attachments for messages older than DATE. Date format is same as with `--croptodates'. --onlynewerthan Optional modifier for `--deleteattachments' and `--replaceattachments'. Only deal with attachments for messages newer than DATE. Date format is same as with `--croptodates'. --onlylargerthan Optional modifier for `--deleteattachments' and `--replaceattachments'. Delete attachments only if larger than SIZE bytes. --onlytype Optional modifier for `--deleteattachments' and `--replaceattachments'. Delete attachments only if matching mime type FILETYPE. The FILETYPE does not need to be complete (i.e. `video/m' will match both `video/mp4' and `video/mpeg'). --appendbody Optional modifier for `--deleteattachments' and `--replaceattachments'. For each message whose attachment is deleted/replaced, append STRING to the message body. --prependbody Optional modifier for `--deleteattachments' and `--replaceattachments'. For each message whose attachment is deleted/replaced, prepend STRING to the message body. --replaceattachments [LIST] Replace attachments of type with placeholder image. Argument: "default=filename,mimetype1=filename1,mimetype2=filename2,.." = VARIOUS = The following options are also supported in this program, and listed here for completeness. Some of them are mostly useful for the developer, others were custom functions for specific problems that are not expected to be very useful to other people. Most of these functions are poorly tested (if at all) and possibly outdated. Some will probably eventually be renamed and more thoroughly documented others will be removed. --showdbinfo Prints a list of all tables and columns in the backups SQLite database. --scramble Poorly censors backups, replacing all characters with 'x'. Useful to make screenshots. --scanmissingattachments If you see "warning attachment data not found" messages, feel free to use this option and provide the output to the developer.)*"; // --hiperfall Switch sender and recipient. See // https://github.com/bepaald/signalbackup-tools/issues/44 // --setselfid Optional modifier for `--hiperfall' and `--importwachat' // --importwachat Import whatsapp data. See // https://github.com/bepaald/signalbackup-tools/issues/19 // --setwatimefmt Required modifier for `--importwachat'. // --dumpdesktopdb Decrypts the Signal Desktop database and saves it to the // file `desktop.db'. PATH is the base path of Signal // Desktop's data (eg `~/.config/Signal' on Linux. The program // stupidly still needs an and parameter // to actually run. std::cout << R"*( --assumebadframesizeonbadmac Used to fix a specific (long fixed) bug in Signal. See https://github.com/signalapp/Signal-Android/issues/9154 --editattachmentsize Modifier for `--assumebadframesizeonbadmac' --removedoubles [N] Attempt to remove doubled messages in the database. May be useful when importing partially overlapping backup files. Optional N: time in milliseconds for messages to be considered duplicates (default 0). --reordersmsmmsids Makes sure sms and mms entries are sorted chronologically in the database. This option exists for backups edited by this program before this was done automatically (as it is now) --stoponerror Do not try to recover automatically when encountering bad data. -v, --verbose Makes the output even more verbose than it already is. --mergerecipients Can be used to change a contacts number (for example when they get a new phone). Messages from OLDNUMBER are changed so they appear as coming from NEWNUMBER, and the threads are merged. --migrate214to215 Migrate a v214 database to v215. Changes in the database prevent v214 and v215 from being compatible for merging. This function attempts to migrate the older database so it can be used as a source for `--importthreads'. See also https://github.com/bepaald/signalbackup-tools/issues/184 --importtelegram Import messages from a JSON file as exported by Telegram. This may be a somewhat complicated procedure. For details, see https://github.com/bepaald/signalbackup-tools/issues/153)*"; // --editgroupmembers Optional modifier for `--mergerecipients'. Also changes // groups members from OLDNUMBER to NEWNUMBER. Might not // always be wanted if the NEWNUMBER was already added to the // group. std::cout << R"*( --mergegroups Merge all messages from OLD_GROUP into NEW_GROUP. )*"; //--sleepyh34d Try to import messages from a truncated backup file into a // complete one. See // https://github.com/bepaald/signalbackup-tools/issues/32 //std::cout << R"*( //--hhenkel See https://github.com/bepaald/signalbackup-tools/issues/17 //)*"; } // checkdbintegrity signalbackup-tools-20250313-1/attachmentframe/000077500000000000000000000000001476450434500210715ustar00rootroot00000000000000signalbackup-tools-20250313-1/attachmentframe/attachmentframe.h000066400000000000000000000245661476450434500244220ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef ATTACHMENTFRAME_H_ #define ATTACHMENTFRAME_H_ #include #include #include "../common_bytes.h" #include "../framewithattachment/framewithattachment.h" class AttachmentFrame : public FrameWithAttachment { enum FIELD { INVALID = 0, ROWID = 1, // uint64 ATTACHMENTID = 2, // uint64 LENGTH = 3 // uint32 }; static Registrar s_registrar; public: inline explicit AttachmentFrame(uint64_t count = 0); inline AttachmentFrame(unsigned char const *bytes, size_t length, uint64_t count = 0); inline AttachmentFrame(AttachmentFrame &&other) = default; inline AttachmentFrame &operator=(AttachmentFrame &&other) = default; inline AttachmentFrame(AttachmentFrame const &other) = default; inline AttachmentFrame &operator=(AttachmentFrame const &other) = default; inline virtual ~AttachmentFrame() override = default; inline virtual AttachmentFrame *clone() const override; inline virtual AttachmentFrame *move_clone() override; inline static BackupFrame *create(unsigned char const *bytes, size_t length, uint64_t count = 0); inline virtual void printInfo() const override; inline virtual FRAMETYPE frameType() const override; inline uint32_t length() const; inline void setLength(uint32_t l); inline virtual uint32_t attachmentSize() const override; inline uint64_t rowId() const; inline void setRowId(uint64_t rid); inline uint64_t attachmentId() const; inline void setAttachmentId(uint64_t rid); inline std::pair getData() const override; inline virtual bool validate(uint64_t available) const override; inline std::string getHumanData() const override; inline unsigned int getField(std::string_view const &str) const; inline void setLengthField(uint32_t newlength); private: inline uint64_t dataSize() const override; }; inline AttachmentFrame::AttachmentFrame(uint64_t count) : FrameWithAttachment(count) {} inline AttachmentFrame::AttachmentFrame(unsigned char const *bytes, size_t length, uint64_t count) : FrameWithAttachment(bytes, length, count) {} inline AttachmentFrame *AttachmentFrame::clone() const { return new AttachmentFrame(*this); } inline AttachmentFrame *AttachmentFrame::move_clone() { return new AttachmentFrame(std::move(*this)); } inline BackupFrame *AttachmentFrame::create(unsigned char const *bytes, size_t length, uint64_t count) // static { return new AttachmentFrame(bytes, length, count); } inline void AttachmentFrame::printInfo() const // virtual override { Logger::message("Frame number: ", d_count); Logger::message(" Size: ", d_constructedsize); Logger::message(" Type: ATTACHMENT"); for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::ROWID) Logger::message(" - row id : ", bytesToUint64(std::get<1>(p), std::get<2>(p)), " (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::ATTACHMENTID) Logger::message(" - attachment id : ", bytesToUint64(std::get<1>(p), std::get<2>(p)), " (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::LENGTH) Logger::message(" - length : ", bytesToUint32(std::get<1>(p), std::get<2>(p)), " (", std::get<2>(p), " bytes)"); } if (d_attachmentdata) { uint32_t size = length(); if (size < 25) Logger::message(" - attachment : ", bepaald::bytesToHexString(d_attachmentdata, size)); else Logger::message(" - attachment : ", bepaald::bytesToHexString(d_attachmentdata, 25), " ... (", size, " bytes total)"); } } inline BackupFrame::FRAMETYPE AttachmentFrame::frameType() const // virtual override { return FRAMETYPE::ATTACHMENT; } inline uint32_t AttachmentFrame::length() const { if (!d_attachmentdata_size) for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::LENGTH) return bytesToUint32(std::get<1>(p), std::get<2>(p)); return d_attachmentdata_size; } inline void AttachmentFrame::setLength(uint32_t l) { for (auto &p : d_framedata) if (std::get<0>(p) == FIELD::LENGTH) { uint64_t val = bepaald::swap_endian(static_cast(l)); std::memcpy(std::get<1>(p), reinterpret_cast(&val), sizeof(val)); d_attachmentdata_size = l; return; } } inline uint32_t AttachmentFrame::attachmentSize() const // virtual override { return length(); } inline uint64_t AttachmentFrame::rowId() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::ROWID) return bytesToUint64(std::get<1>(p), std::get<2>(p)); return 0; } inline void AttachmentFrame::setRowId(uint64_t rid) { for (auto &p : d_framedata) if (std::get<0>(p) == FIELD::ROWID) { if (sizeof(rid) != std::get<2>(p)) [[unlikely]] { //std::cout << " ************ DAMN! ********** " << std::endl; return; } uint64_t val = bepaald::swap_endian(rid); std::memcpy(std::get<1>(p), reinterpret_cast(&val), sizeof(val)); return; } } inline uint64_t AttachmentFrame::attachmentId() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::ATTACHMENTID) return bytesToUint64(std::get<1>(p), std::get<2>(p)); return 0; } inline void AttachmentFrame::setAttachmentId(uint64_t aid) { for (auto &p : d_framedata) if (std::get<0>(p) == FIELD::ATTACHMENTID) { if (sizeof(aid) != std::get<2>(p)) [[unlikely]] { //std::cout << " ************ DAMN! ********** " << std::endl; return; } uint64_t val = bepaald::swap_endian(aid); std::memcpy(std::get<1>(p), reinterpret_cast(&val), sizeof(val)); return; } } inline uint64_t AttachmentFrame::dataSize() const { uint64_t size = 0; for (auto const &fd : d_framedata) { uint64_t value = bytesToUint64(std::get<1>(fd), std::get<2>(fd)); size += varIntSize(value); size += 1; // for fieldtype + wiretype } // for size of this frame. size += varIntSize(size); return ++size; // for frametype and wiretype } inline std::pair AttachmentFrame::getData() const { uint64_t size = dataSize(); unsigned char *data = new unsigned char[size]; uint64_t datapos = 0; datapos += setFieldAndWire(FRAMETYPE::ATTACHMENT, WIRETYPE::LENGTHDELIM, data + datapos); datapos += setFrameSize(size, data + datapos); /* ROWID = 1, // uint64 ATTACHMENTID = 2, // uint64 LENGTH = 3 // uint32 */ for (auto const &fd : d_framedata) datapos += putVarIntType(fd, data + datapos); return {data, size}; } inline bool AttachmentFrame::validate(uint64_t available) const { if (d_framedata.empty()) return false; int foundrowid = 0; int rowid_fieldsize = 0; int foundlength = 0; int length_fieldsize = 0; unsigned int length = 0; int foundattachmentid = 0; for (auto const &p : d_framedata) { if (std::get<0>(p) != FIELD::ROWID && std::get<0>(p) != FIELD::ATTACHMENTID && std::get<0>(p) != FIELD::LENGTH) return false; if (std::get<0>(p) == FIELD::ROWID) { ++foundrowid; rowid_fieldsize += std::get<2>(p); } else if (std::get<0>(p) == FIELD::ATTACHMENTID) ++foundattachmentid; // zero for newer backups... else if (std::get<0>(p) == FIELD::LENGTH) { ++foundlength; length += bytesToUint32(std::get<1>(p), std::get<2>(p)); length_fieldsize += std::get<2>(p); } } return foundlength == 1 && foundattachmentid <= 1 && foundrowid == 1 && length_fieldsize <= 8 && rowid_fieldsize <= 8 && length <= available && length < 1 * 1024 * 1024 * 1024; // lets cap a valid attachment size at 1 gigabyte. // From what I've found, the current (theoretical) maximum is 500Mb for video on // Android. // From reading the source, the real maximum 100Mb (even less, as this is the // length of the padded ciphertext), and 500Mb video is only allowed _to be transcoded // to a smaller size_. (https://github.com/signalapp/Signal-Android/blob/28c280947fd75c48268200638bb80117647ce5cf/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt#L867) // Obviously these values have changed in the past, and will likely change in the future. // But this frame validation is only relevant to older databases (with even older limits) anyway } inline std::string AttachmentFrame::getHumanData() const { std::string data; for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::ROWID) data += "ROWID:uint64:" + bepaald::toString(bytesToUint64(std::get<1>(p), std::get<2>(p))) + "\n"; else if (std::get<0>(p) == FIELD::ATTACHMENTID) data += "ATTACHMENTID:uint64:" + bepaald::toString(bytesToUint64(std::get<1>(p), std::get<2>(p))) + "\n"; else if (std::get<0>(p) == FIELD::LENGTH) data += "LENGTH:uint32:" + bepaald::toString(bytesToUint32(std::get<1>(p), std::get<2>(p))) + "\n"; } return data; } inline unsigned int AttachmentFrame::getField(std::string_view const &str) const { if (str == "ROWID") return FIELD::ROWID; if (str == "ATTACHMENTID") return FIELD::ATTACHMENTID; if (str == "LENGTH") return FIELD::LENGTH; return FIELD::INVALID; } inline void AttachmentFrame::setLengthField(uint32_t newlength) { for (auto &p : d_framedata) { if (std::get<0>(p) == FIELD::LENGTH) { // out with the old if (std::get<1>(p)) delete[] std::get<1>(p); // in with the new uint32_t tmp = bepaald::swap_endian(newlength); std::get<1>(p) = new unsigned char[sizeof(uint32_t)]; std::memcpy(std::get<1>(p), reinterpret_cast(&tmp), sizeof(uint32_t)); std::get<2>(p) = sizeof(uint32_t); } } } #endif signalbackup-tools-20250313-1/attachmentframe/attachmentframe.ih000066400000000000000000000014071476450434500245600ustar00rootroot00000000000000/* Copyright (C) 2019-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "attachmentframe.h" signalbackup-tools-20250313-1/attachmentframe/statics.cc000066400000000000000000000015411476450434500230530ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "attachmentframe.ih" AttachmentFrame::Registrar AttachmentFrame::s_registrar(FRAMETYPE::ATTACHMENT, create); signalbackup-tools-20250313-1/attachmentmetadata/000077500000000000000000000000001476450434500215575ustar00rootroot00000000000000signalbackup-tools-20250313-1/attachmentmetadata/attachmentmetadata.h000066400000000000000000000025751476450434500255720ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef ATTACHMENTMETADATA_H_ #define ATTACHMENTMETADATA_H_ #include #include struct AttachmentMetadata { int width; int height; std::string filetype; uint64_t filesize; std::string hash; std::string filename; operator bool() const { return (width != -1 && height != -1 && !filetype.empty() && filesize != 0); } static AttachmentMetadata getAttachmentMetaData(std::string const &filename, bool skiphash = false); static AttachmentMetadata getAttachmentMetaData(std::string const &filename, unsigned char *data, uint64_t data_size, bool skiphash = false); }; #endif signalbackup-tools-20250313-1/attachmentmetadata/getattachmentmetadata.cc000066400000000000000000000266661476450434500264370ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "attachmentmetadata.h" #include #include "../base64/base64.h" #include "../common_filesystem.h" AttachmentMetadata AttachmentMetadata::getAttachmentMetaData(std::string const &file, unsigned char *data, uint64_t data_size, bool skiphash) // static { //struct AttachmentMetadata //{ // int width; // int height; // std::string filetype; // unsigned long filesize; // std::string hash; // std::string filename; // operator bool() const { return (width != -1 && height != -1 && !filetype.empty() && filesize != 0); } //}; if (data_size == 0) { Logger::warning("Attachment '", file, "' is zero bytes"); return AttachmentMetadata{-1, -1, std::string(), data_size, std::string(), file}; } std::string hash; if (!skiphash) { // gethash unsigned char rawhash[SHA256_DIGEST_LENGTH]; std::unique_ptr sha256(EVP_MD_CTX_new(), &::EVP_MD_CTX_free); if (!sha256 || EVP_DigestInit_ex(sha256.get(), EVP_sha256(), nullptr) != 1 || EVP_DigestUpdate(sha256.get(), data, data_size) != 1 || EVP_DigestFinal_ex(sha256.get(), rawhash, nullptr) != 1) [[unlikely]] { Logger::warning("Failed to set hash"); hash = std::string(); } hash = Base64::bytesToBase64String(rawhash, SHA256_DIGEST_LENGTH); //std::cout << bepaald::bytesToHexString(rawhash, SHA256_DIGEST_LENGTH) << std::endl; //std::cout << "GOT HASH: " << hash << std::endl; } // set buffer for file header int bufsize = std::min(data_size, uint64_t(30)); unsigned char *buf = data; // PNG: the first frame is by definition an IHDR frame, which gives dimensions // NEED 24 bytes if (bufsize >= 24 && buf[0] == 0x89 && buf[1] == 'P' && buf[2] == 'N' && buf[3] == 'G' && buf[4] == 0x0D && buf[5] == 0x0A && buf[6] == 0x1A && buf[7] == 0x0A && buf[12] == 'I' && buf[13] == 'H' && buf[14] == 'D' && buf[15] == 'R') { //*x = (buf[16] << 24) + (buf[17] << 16) + (buf[18] << 8) + (buf[19] << 0); //*y = (buf[20] << 24) + (buf[21] << 16) + (buf[22] << 8) + (buf[23] << 0); return AttachmentMetadata{(buf[16] << 24) + (buf[17] << 16) + (buf[18] << 8) + (buf[19] << 0), (buf[20] << 24) + (buf[21] << 16) + (buf[22] << 8) + (buf[23] << 0), "image/png", data_size, hash, file}; } // GIF: first three bytes say "GIF", next three give version number. Then dimensions // NEEDS 10 bytes if (bufsize >= 10 && buf[0] == 'G' && buf[1] == 'I' && buf[2] == 'F') { //*x = buf[6] + (buf[7] << 8); //*y = buf[8] + (buf[9] << 8); return AttachmentMetadata{buf[8] + (buf[9] << 8), buf[6] + (buf[7] << 8), "image/gif", data_size, hash, file}; } // WEBP // https://developers.google.com/speed/webp/docs/riff_container // Starts with 'R','I','F','F','?','?','?','?','W','E','B','P' if (bufsize >= 30 && buf[0] == 'R' && buf[1] == 'I' && buf[2] == 'F' && buf[3] == 'F' && buf[8] == 'W' && buf[9] == 'E' && buf[10] == 'B' && buf[11] == 'P') { if (std::memcmp(buf + 12, "VP8 ", 4) == 0) // 'lossless' { //std::cout << "lossy" << std::endl; int w = ((buf[26] | buf[27] << 8) & 0x3fff); int h = ((buf[28] | buf[29] << 8) & 0x3fff); //std::cout << "WidhtxHeight: " << w << "x" << h << std::endl<< std::endl; return AttachmentMetadata{w, h, "image/webp", data_size, hash, file}; } else if (std::memcmp(buf + 12, "VP8L", 4) == 0) // 'lossy' { //std::cout << "lossless" << std::endl; uint32_t size = (buf[21] | (buf[22] << 8) | (buf[23] << 16) | (buf[24] << 24)); int w = (size & 0x3fff) + 1; int h = ((size >> 14) & 0x3fff) + 1; //std::cout << "WidhtxHeight: " << w << "x" << h << std::endl<< std::endl; return AttachmentMetadata{w, h, "image/webp", data_size, hash, file}; } else if (std::memcmp(buf + 12, "VP8X", 4) == 0) // 'extended' { //std::cout << "extended" << std::endl; // first byte of VPX8 header = RrILEXAR // where Rr = reserved 00 // R = reserved 0 // (ILEXA are all 1 bit flags for something) // then 24 bits reserved 0 // then 24 bits canvas width - 1 // then 24 bits canvas height - 1 if ((buf[20] & 0b11000000) == 0 && (buf[20] & 0b00000001) == 0) { int w = (buf[24] | (buf[25] << 8) | (buf[26] << 16)) + 1; int h = (buf[27] | (buf[28] << 8) | (buf[29] << 16)) + 1; //std::cout << "WidhtxHeight: " << w << "x" << h << std::endl<< std::endl; return AttachmentMetadata{w, h, "image/webp", data_size, hash, file}; } else return AttachmentMetadata{-1, -1, "image/webp", data_size, hash, file}; } else return AttachmentMetadata{-1, -1, "image/webp", data_size, hash, file}; } // JPEG // For jpeg we read the width and height from JPEG header, more precisely, the frame marked 0xFFC0. // At buf[4] + buf[5], we find the block length of the first block (which is never the block // with size information, so can be skipped. // Then, we can just skip to the start of the next block, read 9 bytes and get: // 0xFF(*) | 0xC0(**) | ushort length | uchar precision | ushort x | ushort y // if first byte is not 0xff, something is wrong // if second byte is not 0xc0, this is not our frame and we use 3rd and 4th to skip // else bytes 6-9 are the wanted numbers. // // * Note, apparently, markers can start with any number of 0xff's // ** Apparently, frames marked C0-C3 & C9-CB all contain the desired resolution // // Note, though I think it is required the first frame is a JFIF, or Exif frame, // I have images that don't have this. They just start with 0xff0xd8 : 'start-of-image' // // from : (https://web.archive.org/web/20131016210645/)http://www.64lines.com/jpeg-width-height //std::cout << "DATA: " << bepaald::bytesToHexString(buf, 100) << std::endl; if (/*(buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF && buf[3] == 0xE0 && buf[6] == 'J' && buf[7] == 'F' && buf[8] == 'I' && buf[9] == 'F' && buf[10] == 0x00) || (buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF && buf[3] == 0xE1 && buf[6] == 'E' && buf[7] == 'x' && buf[8] == 'i' && buf[9] == 'f' && buf[10] == 0x00) ||*/ (buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF)) { int jpeg_bufsize = 9; int seekpos = 0; int pos = 2; // offset for 0xff 0xd8 which always seem to be the first two bytes while (true) { // check frame marker if (buf[pos] != 0xFF) { Logger::warning("Failed to find start of JPEG header frame"); return AttachmentMetadata{-1, -1, std::string(), data_size, hash, file}; } // skip any extra frame markers while (buf[pos + 1] == 0xFF) { //std::cout << "Skipping extra frame marker" << std::endl; ++pos; if (pos >= jpeg_bufsize) { Logger::message("This could be fixed..."); return AttachmentMetadata{-1, -1, std::string(), data_size, hash, file}; } } if (buf[pos + 1] == 0xC0 || buf[pos + 1] == 0xC1 || buf[pos + 1] == 0xC2 || buf[pos + 1] == 0xC3 || buf[pos + 1] == 0xC9 || buf[pos + 1] == 0xCA || buf[pos + 1] == 0xCB) // FOUND OUR MARKER! { //*y = (buf[pos + 5] << 8) + buf[pos + 6]; //*x = (buf[pos + 7] << 8) + buf[pos + 8]; //std::cout << "GOT MARKER" << std::endl; return AttachmentMetadata{(buf[pos + 7] << 8) + buf[pos + 8], (buf[pos + 5] << 8) + buf[pos + 6], "image/jpeg", data_size, hash, file}; } else // this was a different frame, skip it { //std::cout << "DIFFERENT MARKER, SKIP!" << std::endl; int block_length = (buf[pos + 2] << 8) + buf[pos + 3]; //std::cout << "Skipping frame (" << block_length << ")" << std::endl; //std::cout << block_length << " " << jpeg_bufsize << " " << pos << " " << block_length + 2 - (jpeg_bufsize - pos) << std::endl; if ((block_length + pos + 2) > static_cast(data_size - jpeg_bufsize)) { Logger::warning("Failed to read next jpeg_buffer from data"); return AttachmentMetadata{-1, -1, std::string(), data_size, hash, file}; } //filestream.seekg(block_length + 2 - (jpeg_bufsize - pos), std::ios_base::cur); // + 2 skip marker itself // if (!filestream.read(reinterpret_cast(buf.get()), jpeg_bufsize)) // { // Logger::warning("Failed to read next 24 bytes from file"); // return AttachmentMetadata{-1, -1, std::string(), data_size, hash, file}; // } //std::cout << "Pos is now: " << filestream.tellg() << std::endl; seekpos += block_length + pos + 2; buf = data + seekpos; //for (int i = 0; i < bufsize; ++i) // std::cout << i << " : " << std::hex << reinterpret_cast(buf[i] & 0xff) << std::dec << std::endl; pos = 0; } } } return AttachmentMetadata{-1, -1, std::string(), data_size, hash, file}; } AttachmentMetadata AttachmentMetadata::getAttachmentMetaData(std::string const &file, bool skiphash) //static { //struct AttachmentMetadata //{ // int width; // int height; // std::string filetype; // unsigned long filesize; // std::string hash; // std::string filename; // operator bool() const { return (width != -1 && height != -1 && !filetype.empty() && filesize != 0); } //}; std::ifstream filestream(std::filesystem::path(file), std::ios_base::binary | std::ios_base::in); if (!filestream.is_open()) { Logger::warning("Failed to open image for reading: ", file); return AttachmentMetadata{-1, -1, std::string(), 0, std::string(), std::string()}; } //filestream.seekg(0, std::ios_base::end); //long long int file_size = filestream.tellg(); //filestream.seekg(0, std::ios_base::beg); uint64_t file_size = bepaald::fileSize(file); if (file_size == static_cast(-1)) [[unlikely]] { Logger::warning("Failed to get filesize for attachment '", file, "'"); return AttachmentMetadata{-1, -1, std::string(), file_size, std::string(), file}; } if (file_size == 0) [[unlikely]] { Logger::warning("Attachment '", file, "' is zero bytes"); return AttachmentMetadata{-1, -1, std::string(), file_size, std::string(), file}; } std::unique_ptr file_data(new unsigned char[file_size]); if (!filestream.read(reinterpret_cast(file_data.get()), file_size) || filestream.gcount() != static_cast(file_size)) { Logger::warning("Failed to read ", file_size, " bytes from file '", file, "'"); return AttachmentMetadata{-1, -1, std::string(), file_size, std::string(), file}; } return getAttachmentMetaData(file, file_data.get(), file_size, skiphash); } signalbackup-tools-20250313-1/autoversion.h000066400000000000000000000014721476450434500204610ustar00rootroot00000000000000/* Copyright (C) 2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef VERSION_H_ #define VERSION_H_ #define VERSIONDATE "20250313.082731" #endif signalbackup-tools-20250313-1/avatarframe/000077500000000000000000000000001476450434500202175ustar00rootroot00000000000000signalbackup-tools-20250313-1/avatarframe/avatarframe.h000066400000000000000000000222121476450434500226600ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef AVATARFRAME_H_ #define AVATARFRAME_H_ #include #include #include #include "../framewithattachment/framewithattachment.h" #include "../attachmentmetadata/attachmentmetadata.h" #include "../common_be.h" #include "../common_bytes.h" class AvatarFrame : public FrameWithAttachment { enum FIELD { INVALID = 0, NAME = 1, // string LENGTH = 2, // uint32 RECIPIENT = 3, //string }; static Registrar s_registrar; std::optional d_mimetype; public: inline explicit AvatarFrame(uint64_t count = 0); inline AvatarFrame(unsigned char const *bytes, size_t length, uint64_t count = 0); inline virtual ~AvatarFrame() override = default; inline virtual AvatarFrame *clone() const override; inline virtual AvatarFrame *move_clone() override; inline static BackupFrame *create(unsigned char const *bytes, size_t length, uint64_t count); inline virtual void printInfo() const override; inline virtual FRAMETYPE frameType() const override; inline uint32_t length() const; inline virtual uint32_t attachmentSize() const override; inline std::string name() const; inline std::string recipient() const; inline void setRecipient(std::string const &r); inline std::pair getData() const override; inline virtual bool validate(uint64_t available) const override; inline std::string getHumanData() const override; inline unsigned int getField(std::string_view const &str) const; inline std::optional mimetype() const; inline unsigned char *attachmentData(bool *badmac = nullptr, bool verbose = false); private: inline uint64_t dataSize() const override; }; inline AvatarFrame::AvatarFrame(uint64_t count) : FrameWithAttachment(count) {} inline AvatarFrame::AvatarFrame(unsigned char const *bytes, size_t length, uint64_t count) : FrameWithAttachment(bytes, length, count) {} inline AvatarFrame *AvatarFrame::clone() const { return new AvatarFrame(*this); } inline AvatarFrame *AvatarFrame::move_clone() { return new AvatarFrame(std::move(*this)); } inline BackupFrame *AvatarFrame::create(unsigned char const *bytes, size_t length, uint64_t count) // static { return new AvatarFrame(bytes, length, count); } inline void AvatarFrame::printInfo() const // virtual override { Logger::message("Frame number: ", d_count); Logger::message(" Size: ", d_constructedsize); Logger::message(" Type: AVATAR"); for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::NAME) Logger::message(" - name : ", bepaald::bytesToString(std::get<1>(p), std::get<2>(p)), " (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::RECIPIENT) Logger::message(" - recipient : ", bepaald::bytesToString(std::get<1>(p), std::get<2>(p)), " (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::LENGTH) Logger::message(" - length : ", bytesToUint32(std::get<1>(p), std::get<2>(p)), " (", std::get<2>(p), " bytes)"); } if (d_attachmentdata) { uint32_t size = length(); if (size < 25) Logger::message(" - attachment: ", bepaald::bytesToHexString(d_attachmentdata, size)); else Logger::message(" - attachment: ", bepaald::bytesToHexString(d_attachmentdata, 25), " ... (", size, " bytes total)"); } } inline BackupFrame::FRAMETYPE AvatarFrame::frameType() const // virtual override { return FRAMETYPE::AVATAR; } inline uint32_t AvatarFrame::length() const { if (!d_attachmentdata_size) for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::LENGTH) return bytesToUint32(std::get<1>(p), std::get<2>(p)); return d_attachmentdata_size; } inline uint32_t AvatarFrame::attachmentSize() const // virtual override { return length(); } inline std::string AvatarFrame::name() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::NAME) return bepaald::bytesToString(std::get<1>(p), std::get<2>(p)); return std::string(); } inline std::string AvatarFrame::recipient() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::RECIPIENT) return bepaald::bytesToString(std::get<1>(p), std::get<2>(p)); return std::string(); } inline void AvatarFrame::setRecipient(std::string const &r) { unsigned char *temp = new unsigned char[r.length()]; std::memcpy(temp, r.c_str(), r.length()); for (auto &fd : d_framedata) if (std::get<0>(fd) == FIELD::RECIPIENT) { // destroy old... if (std::get<1>(fd)) delete[] std::get<1>(fd); // set new std::get<1>(fd) = temp; std::get<2>(fd) = r.length(); return; } // if we reach this, no field 'recipient' was found (should not happen) [[unlikely]] delete[] temp; } inline uint64_t AvatarFrame::dataSize() const { uint64_t size = 0; for (auto const &fd : d_framedata) { switch (std::get<0>(fd)) { case FIELD::NAME: case FIELD::RECIPIENT: { uint64_t stringsize = std::get<2>(fd); size += varIntSize(stringsize); size += stringsize + 1; // +1 for fieldtype + wiretype break; } case FIELD::LENGTH: { uint64_t value = bytesToUint64(std::get<1>(fd), std::get<2>(fd)); size += varIntSize(value); size += 1; // for fieldtype + wiretype break; } } } // for size of this entire frame. size += varIntSize(size); return ++size; // for frametype and wiretype } inline std::pair AvatarFrame::getData() const { uint64_t size = dataSize(); unsigned char *data = new unsigned char[size]; uint64_t datapos = 0; datapos += setFieldAndWire(FRAMETYPE::AVATAR, WIRETYPE::LENGTHDELIM, data + datapos); datapos += setFrameSize(size, data + datapos); for (auto const &fd : d_framedata) { switch (std::get<0>(fd)) { case FIELD::NAME: datapos += putLengthDelimType(fd, data + datapos); break; case FIELD::LENGTH: datapos += putVarIntType(fd, data + datapos); break; case FIELD::RECIPIENT: datapos += putLengthDelimType(fd, data + datapos); break; } } return {data, size}; } inline bool AvatarFrame::validate(uint64_t available) const { if (d_framedata.empty()) return false; int foundlength = 0; int length_fieldsize = 0; unsigned int length = 0; int foundname_or_recipient = 0; for (auto const &p : d_framedata) { if (std::get<0>(p) != FIELD::NAME && std::get<0>(p) != FIELD::RECIPIENT && std::get<0>(p) != FIELD::LENGTH) return false; // a valid avatar frame must contain length AND (recipient (newer backups) XOR name (older backups)) if (std::get<0>(p) == FIELD::LENGTH) { ++foundlength; length += bytesToUint32(std::get<1>(p), std::get<2>(p)); length_fieldsize += std::get<2>(p); } else if (std::get<0>(p) == FIELD::RECIPIENT || std::get<0>(p) != FIELD::NAME) ++foundname_or_recipient; } return foundlength == 1 && foundname_or_recipient == 1 && length <= available && length_fieldsize <= 8; // && length < some_max_avatar_size; } inline std::string AvatarFrame::getHumanData() const { std::string data; for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::NAME) data += "NAME:string:" + bepaald::bytesToString(std::get<1>(p), std::get<2>(p)) + "\n"; else if (std::get<0>(p) == FIELD::RECIPIENT) data += "RECIPIENT:string:" + bepaald::bytesToString(std::get<1>(p), std::get<2>(p)) + "\n"; else if (std::get<0>(p) == FIELD::LENGTH) data += "LENGTH:uint32:" + bepaald::toString(bytesToUint32(std::get<1>(p), std::get<2>(p))) + "\n"; } return data; } inline unsigned int AvatarFrame::getField(std::string_view const &str) const { if (str == "RECIPIENT") return FIELD::RECIPIENT; if (str == "NAME") return FIELD::NAME; if (str == "LENGTH") return FIELD::LENGTH; return FIELD::INVALID; } inline std::optional AvatarFrame::mimetype() const { return d_mimetype; } inline unsigned char *AvatarFrame::attachmentData(bool *badmac, bool verbose) { unsigned char *data = FrameWithAttachment::attachmentData(badmac, verbose); if (data && !d_mimetype) // try to get mimetype { AttachmentMetadata amd = AttachmentMetadata::getAttachmentMetaData(std::string(), data, d_attachmentdata_size, true/*skiphash*/); if (!amd.filetype.empty()) d_mimetype = amd.filetype; } return data; } #endif signalbackup-tools-20250313-1/avatarframe/avatarframe.ih000066400000000000000000000014031476450434500230300ustar00rootroot00000000000000/* Copyright (C) 2019-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "avatarframe.h" signalbackup-tools-20250313-1/avatarframe/statics.cc000066400000000000000000000015211476450434500221770ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "avatarframe.ih" AvatarFrame::Registrar AvatarFrame::s_registrar(FRAMETYPE::AVATAR, create); signalbackup-tools-20250313-1/backupframe/000077500000000000000000000000001476450434500202065ustar00rootroot00000000000000signalbackup-tools-20250313-1/backupframe/backupframe.h000066400000000000000000000422531476450434500226450ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef BACKUPFRAME_H_ #define BACKUPFRAME_H_ #include #include #include #include #include #include #include "../logger/logger.h" class BackupFrame { public: enum FRAMETYPE : unsigned int { HEADER = 1, SQLSTATEMENT = 2, SHAREDPREFERENCE = 3, ATTACHMENT = 4, DATABASEVERSION = 5, END = 6, // bool AVATAR = 7, STICKER = 8, KEYVALUE = 9, INVALID = std::numeric_limits::max() }; enum WIRETYPE : unsigned int { VARINT = 0, FIXED64 = 1, LENGTHDELIM = 2, STARTTYPE = 3, ENDTYPE = 4, FIXED32 = 5 }; protected: inline static std::unordered_map &s_registry(); struct Registrar { Registrar(FRAMETYPE ft, BackupFrame *(*func)(unsigned char const *, size_t, uint64_t)) { //Logger::message("Registering class type: ", ft); s_registry()[ft] = func; } }; bool d_ok; std::vector> d_framedata; // field number, field data, length uint64_t d_count; size_t d_constructedsize; public: explicit inline BackupFrame(uint64_t count); inline BackupFrame(unsigned char const *data, size_t length, uint64_t count); inline BackupFrame(BackupFrame &&other); inline BackupFrame &operator=(BackupFrame &&other); inline BackupFrame(BackupFrame const &other); inline BackupFrame &operator=(BackupFrame const &other); inline virtual ~BackupFrame(); inline virtual BackupFrame *clone() const = 0; inline virtual BackupFrame *move_clone() = 0; inline bool ok(); inline static int getFieldnumber(unsigned char head); inline static unsigned int wiretype(unsigned char head); inline static int64_t getLength(unsigned char const *data, unsigned int *offset, unsigned int totallength); inline static int64_t getVarint(unsigned char const *data, unsigned int *offset, unsigned int totallength); virtual FRAMETYPE frameType() const = 0; inline std::string frameTypeString() const; inline static BackupFrame *instantiate(FRAMETYPE, unsigned char *data, size_t length, uint64_t count = 0); virtual void printInfo() const = 0; inline uint64_t frameNumber() const; inline virtual uint32_t attachmentSize() const; inline virtual bool setAttachmentDataBacked(unsigned char *data, long long int size); inline virtual std::pair getData() const; inline virtual std::string getHumanData() const; inline bool setNewData(unsigned int field, unsigned char *data, uint64_t size); inline virtual bool validate(uint64_t available) const; inline virtual uint64_t dataSize() const; protected: inline uint32_t bytesToUint32(unsigned char const *data, size_t len) const; inline uint64_t bytesToUint64(unsigned char const *data, size_t len) const; inline int32_t bytesToInt32(unsigned char const *data, size_t len) const; inline int64_t bytesToInt64(unsigned char const *data, size_t len) const; bool init(unsigned char const *data, size_t length, std::vector> *framedata); template inline void intTypeToBytes(T val, unsigned char *b); inline uint64_t putVarInt(uint64_t val, unsigned char *mem) const; inline uint64_t varIntSize(uint64_t val) const; inline uint64_t setFieldAndWire(unsigned int field, unsigned int type, unsigned char *mem) const; inline uint64_t setFrameSize(uint64_t totalsize, unsigned char *mem) const; inline uint64_t putLengthDelimType(std::tuple const &data, unsigned char *mem) const; inline uint64_t putVarIntType(std::tuple const &data, unsigned char *mem) const; inline uint64_t putFixed32Type(std::tuple const &data, unsigned char *mem) const; inline uint64_t putFixed64Type(std::tuple const &data, unsigned char *mem) const; private: inline static int64_t getLengthOrVarint(unsigned char const *data, unsigned int *offset, unsigned int totallength); }; inline std::unordered_map &BackupFrame::s_registry() // static { static std::unordered_map impl; return impl; } inline BackupFrame::BackupFrame(uint64_t num) : d_ok(false), d_count(num), d_constructedsize(0) {} inline BackupFrame::BackupFrame(unsigned char const *data, size_t l, uint64_t num) : d_ok(false), d_count(num), d_constructedsize(l) { //std::cout << "CREATING BACKUPFRAME!" << std::endl; //Logger::message("DATA: ", bepaald::bytesToHexString(data, l), " (", l, " bytes)"); d_ok = init(data, l, &d_framedata); } inline BackupFrame::BackupFrame(BackupFrame &&other) : d_ok(std::move(other.d_ok)), d_framedata(std::move(other.d_framedata)), d_count(std::move(other.d_count)), d_constructedsize(std::move(other.d_constructedsize)) { other.d_framedata.clear(); // clear other without delete[]ing, ~this will do it } inline BackupFrame &BackupFrame::operator=(BackupFrame &&other) { if (this != &other) { // properly delete any data this is holding for (unsigned int i = 0; i < d_framedata.size(); ++i) if (std::get<1>(d_framedata[i])) delete[] std::get<1>(d_framedata[i]); d_framedata.clear(); d_ok = std::move(other.d_ok); d_framedata = std::move(other.d_framedata); other.d_framedata.clear(); d_count = std::move(other.d_count); d_constructedsize = std::move(other.d_constructedsize); } return *this; } inline BackupFrame::BackupFrame(BackupFrame const &other) { d_ok = other.d_ok; d_count = other.d_count; d_constructedsize = other.d_constructedsize; for (unsigned int i = 0; i < other.d_framedata.size(); ++i) { unsigned char *datacpy = nullptr; if (std::get<1>(other.d_framedata[i])) { datacpy = new unsigned char[std::get<2>(other.d_framedata[i])]; std::memcpy(datacpy, std::get<1>(other.d_framedata[i]), std::get<2>(other.d_framedata[i])); } d_framedata.emplace_back(std::make_tuple(std::get<0>(other.d_framedata[i]), datacpy, std::get<2>(other.d_framedata[i]))); } } inline BackupFrame &BackupFrame::operator=(BackupFrame const &other) { if (this != &other) { // properly delete any data this is holding for (unsigned int i = 0; i < d_framedata.size(); ++i) if (std::get<1>(d_framedata[i])) delete[] std::get<1>(d_framedata[i]); d_framedata.clear(); d_ok = other.d_ok; d_count = other.d_count; d_constructedsize = other.d_constructedsize; for (unsigned int i = 0; i < other.d_framedata.size(); ++i) { unsigned char *datacpy = nullptr; if (std::get<1>(other.d_framedata[i])) { datacpy = new unsigned char[std::get<2>(other.d_framedata[i])]; std::memcpy(datacpy, std::get<1>(other.d_framedata[i]), std::get<2>(other.d_framedata[i])); } d_framedata.emplace_back(std::make_tuple(std::get<0>(other.d_framedata[i]), datacpy, std::get<2>(other.d_framedata[i]))); } } return *this; } inline BackupFrame::~BackupFrame() { //std::cout << "DESTROYING BACKUPFRAME!" << std::endl; for (unsigned int i = 0; i < d_framedata.size(); ++i) if (std::get<1>(d_framedata[i])) delete[] std::get<1>(d_framedata[i]); d_framedata.clear(); } inline bool BackupFrame::ok() { return d_ok; } inline int BackupFrame::getFieldnumber(unsigned char head) // static { if (head & 0b10000000) return -1; return (head & 0b01111000) >> 3; } inline unsigned int BackupFrame::wiretype(unsigned char head) // static { return (head & 0b00000111); } inline int64_t BackupFrame::getLength(unsigned char const *data, unsigned int *offset, unsigned int totallength) // static { return getLengthOrVarint(data, offset, totallength); } inline int64_t BackupFrame::getVarint(unsigned char const *data, unsigned int *offset, unsigned int totallength) // static { return getLengthOrVarint(data, offset, totallength); } inline std::string BackupFrame::frameTypeString() const { switch (frameType()) { case FRAMETYPE::HEADER: { return "HeaderFrame"; } case FRAMETYPE::SQLSTATEMENT: { return "SqlStatementFrame"; } case FRAMETYPE::SHAREDPREFERENCE: { return "SharedPreferenceFrame"; } case FRAMETYPE::ATTACHMENT: { return "AttachmentFrame"; } case FRAMETYPE::DATABASEVERSION: { return "DatabaseVersionFrame"; } case FRAMETYPE::END: { return "EndFrame"; } case FRAMETYPE::AVATAR: { return "AvatarFrame"; } case FRAMETYPE::STICKER: { return "StickerFrame"; } case FRAMETYPE::KEYVALUE: { return "KeyValueFrame"; } //case FRAMETYPE::INVALID: default: { return "InvalidFrame"; } } return "Unknown frame type"; } inline int64_t BackupFrame::getLengthOrVarint(unsigned char const *data, unsigned int *offset, unsigned int totallength) // static { if (*offset >= totallength) return -1; uint64_t length = 0; uint64_t times = 0; while (*offset < totallength && (data[*offset]) & 0b10000000) length += ((static_cast(data[(*offset)++]) & 0b01111111) << (times++ * 7)); if (*offset >= totallength) return -1; length += ((static_cast(data[(*offset)++]) & 0b01111111) << (times * 7)); return length; } inline BackupFrame *BackupFrame::instantiate(FRAMETYPE ft, unsigned char *data, size_t length, uint64_t count) //static { auto it = s_registry().find(ft); /* if (it == s_registry().end()) { std::cout << s_registry().size() << std::endl; std::cout << ft << std::endl; std::cout << "END!" << std::endl; } */ if (it == s_registry().end()) [[unlikely]] { //std::cout << "ERROR: Incorrect or unknown frametype (" << static_cast(ft) << ")" << std::endl; return nullptr; } BackupFrame *ret = (it->second)(data, length, count); if (!ret->ok()) [[unlikely]] { //std::cout << "ERROR: BackupFrame::ok() failed" << std::endl; delete ret; return nullptr; } return ret; } // maybe check endianness? inline uint32_t BackupFrame::bytesToUint32(unsigned char const *data, size_t len) const { return static_cast(data[len - 1] & 0xFF) | static_cast(data[len - 2] & 0xFF) << 8 | static_cast(data[len - 3] & 0xFF) << 16 | static_cast(data[len - 4] & 0xFF) << 24; } inline uint64_t BackupFrame::bytesToUint64(unsigned char const *data, size_t len) const { return static_cast(data[len - 1] & 0xFF) | static_cast(data[len - 2] & 0xFF) << 8 | static_cast(data[len - 3] & 0xFF) << 16 | static_cast(data[len - 4] & 0xFF) << 24 | static_cast(data[len - 5] & 0xFF) << 32 | static_cast(data[len - 6] & 0xFF) << 40 | static_cast(data[len - 7] & 0xFF) << 48 | static_cast(data[len - 8] & 0xFF) << 56; } inline int32_t BackupFrame::bytesToInt32(unsigned char const *data, size_t len) const { return static_cast(data[len - 1] & 0xFF) | static_cast(data[len - 2] & 0xFF) << 8 | static_cast(data[len - 3] & 0xFF) << 16 | static_cast(data[len - 4] & 0xFF) << 24; } inline int64_t BackupFrame::bytesToInt64(unsigned char const *data, size_t len) const { return static_cast(data[len - 1] & 0xFF) | static_cast(data[len - 2] & 0xFF) << 8 | static_cast(data[len - 3] & 0xFF) << 16 | static_cast(data[len - 4] & 0xFF) << 24 | static_cast(data[len - 5] & 0xFF) << 32 | static_cast(data[len - 6] & 0xFF) << 40 | static_cast(data[len - 7] & 0xFF) << 48 | static_cast(data[len - 8] & 0xFF) << 56; } // inline void BackupFrame::printInfo() const // virtual // { // DEBUGOUT("I AM A GENERIC BACKUPFRAME. IT SEEMS A BAD CHILD OF MINE HAS NOT OVERRIDDEN ME!"); // std::cout << "Frame number: " << d_count << std::endl; // } inline uint64_t BackupFrame::frameNumber() const { return d_count; } template inline void BackupFrame::intTypeToBytes(T val, unsigned char *b) { for (size_t i = 0; i < sizeof(T); ++i) b[i] = (val >> ((sizeof(T) - (i + 1)) * 8)); // this may have a swap_endian builtin? } inline uint32_t BackupFrame::attachmentSize() const { return 0; } inline bool BackupFrame::setAttachmentDataBacked(unsigned char *, long long int) // virtual { return false; } inline std::pair BackupFrame::getData() const { return {nullptr, 0}; } inline std::string BackupFrame::getHumanData() const { return std::string(); } // taken from techoverflow inline uint64_t BackupFrame::putVarInt(uint64_t val, unsigned char *mem) const { uint64_t outputSize = 0; //While more than 7 bits of data are left, occupy the last output byte // and set the next byte flag while (val > 127) { //|128: Set the next byte flag mem[outputSize] = (static_cast(val & 127)) | 128; //Remove the seven bits we just wrote val >>= 7; outputSize++; } mem[outputSize++] = (static_cast(val)) & 127; return outputSize; } inline uint64_t BackupFrame::varIntSize(uint64_t value) const { if (value <= 0x7f) return 1; if (value <= 0x3fff) return 2; if (value <= 0x1fffff) return 3; if (value <= 0xfffffff) return 4; if (value <= 0x7ffffffff) return 5; if (value <= 0x3ffffffffff) return 6; if (value <= 0x1ffffffffffff) return 7; if (value <= 0xffffffffffffff) return 8; if (value <= 0x7fffffffffffffff) return 9; return 10; } inline uint64_t BackupFrame::setFieldAndWire(unsigned int field, unsigned int type, unsigned char *mem) const { mem[0] = (field << 3); mem[0] |= type; return 1; } inline uint64_t BackupFrame::setFrameSize(uint64_t totalsize, unsigned char *mem) const { // expand to varint size 10 if (varIntSize(totalsize - 2) == 1) return putVarInt(totalsize - 2, mem); if (varIntSize(totalsize - 3) == 2) return putVarInt(totalsize - 3, mem); if (varIntSize(totalsize - 4) == 3) return putVarInt(totalsize - 4, mem); if (varIntSize(totalsize - 5) == 4) return putVarInt(totalsize - 5, mem); if (varIntSize(totalsize - 6) == 5) return putVarInt(totalsize - 6, mem); if (varIntSize(totalsize - 7) == 6) return putVarInt(totalsize - 7, mem); if (varIntSize(totalsize - 8) == 7) return putVarInt(totalsize - 8, mem); if (varIntSize(totalsize - 9) == 8) return putVarInt(totalsize - 9, mem); if (varIntSize(totalsize - 10) == 9) return putVarInt(totalsize - 10, mem); return putVarInt(totalsize - 11, mem); } inline uint64_t BackupFrame::putLengthDelimType(std::tuple const &data, unsigned char *mem) const { uint64_t datapos = 0; datapos += setFieldAndWire(std::get<0>(data), WIRETYPE::LENGTHDELIM, mem + datapos); datapos += putVarInt(std::get<2>(data), mem + datapos); std::memcpy(mem + datapos, std::get<1>(data), std::get<2>(data)); datapos += std::get<2>(data); return datapos; } inline uint64_t BackupFrame::putVarIntType(std::tuple const &data, unsigned char *mem) const { uint64_t datapos = 0; uint64_t value = bytesToUint64(std::get<1>(data), std::get<2>(data)); datapos += setFieldAndWire(std::get<0>(data), WIRETYPE::VARINT, mem + datapos); datapos += putVarInt(value, mem + datapos); return datapos; } inline uint64_t BackupFrame::putFixed32Type(std::tuple const &data, unsigned char *mem) const { uint64_t datapos = 0; datapos += setFieldAndWire(std::get<0>(data), WIRETYPE::FIXED32, mem + datapos); std::memcpy(mem + datapos, std::get<1>(data), std::get<2>(data)); datapos += std::get<2>(data); return datapos; } inline uint64_t BackupFrame::putFixed64Type(std::tuple const &data, unsigned char *mem) const { uint64_t datapos = 0; datapos += setFieldAndWire(std::get<0>(data), WIRETYPE::FIXED64, mem + datapos); std::memcpy(mem + datapos, std::get<1>(data), std::get<2>(data)); datapos += std::get<2>(data); return datapos; } inline bool BackupFrame::setNewData(unsigned int field, unsigned char *data, uint64_t size) { d_framedata.emplace_back(std::make_tuple(field, data, size)); return true; } inline bool BackupFrame::validate(uint64_t) const { return true; } inline uint64_t BackupFrame::dataSize() const { return 0; } #endif signalbackup-tools-20250313-1/backupframe/backupframe.ih000066400000000000000000000014271476450434500230140ustar00rootroot00000000000000/* Copyright (C) 2019-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "backupframe.h" #include signalbackup-tools-20250313-1/backupframe/init.cc000066400000000000000000000105101476450434500214550ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "backupframe.ih" #include "../common_bytes.h" bool BackupFrame::init(unsigned char const *data, size_t l, std::vector> *framedata) { //std::cout << "INITIALIZING FRAME OF " << l << " BYTES" << std::endl; unsigned int processed = 0; while (processed < l) { //DEBUGOUT("PROCESSED ", processed, " OUT OF ", l, " BYTES!"); int fieldnumber = getFieldnumber(data[processed]); if (fieldnumber < 0) return false; unsigned int type = wiretype(data[processed]); ++processed; // first byte was eaten //DEBUGOUT("FIELDNUMBER: ", fieldnumber); //DEBUGOUT("WIRETYPE : ", type); switch (type) { case LENGTHDELIM: // need to read next bytes for length { int64_t length = getLength(data, &processed, l); if (length < 0 || processed + static_cast(length) > l) [[unlikely]] // more then we have return false; unsigned char *fielddata = new unsigned char[length]; std::memcpy(fielddata, data + processed, length); //DEBUGOUT("FIELDDATA: ", bepaald::bytesToHexString(fielddata, length)); framedata->push_back(std::make_tuple(fieldnumber, fielddata, length)); processed += length; // up to length was eaten break; } case VARINT: { int64_t val = getVarint(data, &processed, l); // for UNSIGNED varints if (val == -1 && // (possible?) invalid value processed == l && // last byte was processed data[l - 1] & 0b10000000) [[unlikely]] // but last byte was not end of varint return false; //DEBUGOUT("Got varint: ", val); val = bepaald::swap_endian(val); // because java writes integers in big endian? //DEBUGOUT("Got varint: ", val); unsigned char *fielddata = new unsigned char[sizeof(decltype(val))]; std::memcpy(fielddata, reinterpret_cast(&val), sizeof(decltype(val))); //DEBUGOUT("FIELDDATA: ", bepaald::bytesToHexString(fielddata, sizeof(decltype(val)))); // this used to say sizeof(sizeof(decltype(val))), I assumed it was a mistake framedata->push_back(std::make_tuple(fieldnumber, fielddata, sizeof(decltype(val)))); // processed is set in getVarInt break; } case FIXED32: { //std::cout << "BIT32 TYPE" << std::endl; unsigned int length = 4; if (processed + length > l) // more then we have return false; processed += length; // ???? break; } case FIXED64: { unsigned int length = 8; if (processed + length > l) // more then we have return false; unsigned char *fielddata = new unsigned char[length]; std::memcpy(fielddata, data + processed, length); //DEBUGOUT("FIELDDATA: ", bepaald::bytesToHexString(fielddata, length)); framedata->push_back(std::make_tuple(fieldnumber, fielddata, length)); processed += length; break; } case STARTTYPE: { //std::cout << "GOT STARTTYPE" << std::endl; unsigned int length = 0; processed += length; // ???? break; } case ENDTYPE: { //std::cout << "GOT ENDTYPE" << std::endl; unsigned int length = 0; processed += length; // ???? break; } [[unlikely]] default: Logger::error("Unknown wiretype (", type, ")."); } //DEBUGOUT("Offset: ", processed); } //DEBUGOUT("PROCESSED ", processed, " OUT OF ", l, " BYTES!"); return (processed == l); } signalbackup-tools-20250313-1/base64/000077500000000000000000000000001476450434500170125ustar00rootroot00000000000000signalbackup-tools-20250313-1/base64/base64.h000066400000000000000000000060061476450434500202510ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef BASE64_H_ #define BASE64_H_ #include #include #include #include #include "../logger/logger.h" #include "../common_bytes.h" struct Base64 { public: inline static std::string bytesToBase64String(unsigned char const *data, size_t size); inline static std::string bytesToBase64String(std::pair const &data); template inline static std::pair base64StringToBytes(T const &str, typename std::enable_if || std::is_same_v>::type *dummy = nullptr); }; inline std::string Base64::bytesToBase64String(unsigned char const *data, size_t size) { int base64length = ((4 * size / 3) + 3) & ~3; std::unique_ptr output(new unsigned char[base64length + 1]); // +1 for terminating null if (EVP_EncodeBlock(output.get(), data, size) != base64length) { Logger::error("Failed to base64 encode data"); return std::string(); } return std::string(reinterpret_cast(output.get()), base64length); } inline std::string Base64::bytesToBase64String(std::pair const &data) { return bytesToBase64String(data.first, data.second); } template inline std::pair Base64::base64StringToBytes(T const &str, typename std::enable_if || std::is_same_v>::type *) { int binarylength = str.size() / 4 * 3; std::unique_ptr output(new unsigned char[binarylength]); if (EVP_DecodeBlock(output.get(), reinterpret_cast(str.data()), str.size()) == -1) { Logger::error("Failed to base64 decode data (size: ", str.size(), "): ", str.data()); return {nullptr, 0}; } if (str.empty() || str.back() != '=') return {output.release(), binarylength}; int realsize = binarylength - 1; if (str.size() >= 2 && str[str.size() - 2] == '=') realsize = binarylength - 2; unsigned char *unpaddedoutput = new unsigned char[realsize]; std::memcpy(unpaddedoutput, output.get(), realsize); return {unpaddedoutput, realsize}; } #endif signalbackup-tools-20250313-1/baseattachmentreader/000077500000000000000000000000001476450434500220745ustar00rootroot00000000000000signalbackup-tools-20250313-1/baseattachmentreader/baseattachmentreader.h000066400000000000000000000032451476450434500264170ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef BASEATTACHMENTREADER_H_ #define BASEATTACHMENTREADER_H_ class FrameWithAttachment; class BaseAttachmentReader { public: BaseAttachmentReader() = default; BaseAttachmentReader(BaseAttachmentReader const &other) = default; BaseAttachmentReader(BaseAttachmentReader &&other) = default; BaseAttachmentReader &operator=(BaseAttachmentReader const &other) = default; BaseAttachmentReader &operator=(BaseAttachmentReader &&other) = default; virtual ~BaseAttachmentReader() = default; virtual BaseAttachmentReader *clone() const = 0; inline virtual int getAttachment(FrameWithAttachment *frame, bool verbose) = 0; // this can be overridden in attachment readers to do more cleanup if needed inline virtual void clearData() {}; }; template class AttachmentReader : public BaseAttachmentReader { public: virtual BaseAttachmentReader *clone() const { return new T(static_cast(*this)); } }; #endif signalbackup-tools-20250313-1/common_be.h000066400000000000000000000236211476450434500200410ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef COMMON_BE_H_ #define COMMON_BE_H_ #include #include #include #include #include #include #include #include #include #if __cpp_lib_format >= 201907L #include #endif #ifdef DEBUGMSG #define DEBUGOUT(...) bepaald::log("[DEBUG] : ", __PRETTY_FUNCTION__," : ", __VA_ARGS__); #else #define DEBUGOUT(...) #endif #ifdef DEBUGISSUE #define DEBUGOUT2(...) bepaald::log("[DEBUG] : ", __VA_ARGS__); #else #define DEBUGOUT2(...) #endif #define STRLEN( STR ) (bepaald::strlitLength(STR)) #if __cpp_lib_starts_ends_with >= 201711L #define STRING_STARTS_WITH( STR, SUB ) ( STR.starts_with(SUB) ) #else #define STRING_STARTS_WITH( STR, SUB ) ( STR.substr(0, STRLEN(SUB)) == SUB ) #endif using std::literals::string_literals::operator""s; namespace bepaald { #if defined DEBUGMSG || DEBUGISSUE template inline void log(Args && ...args); #endif template T toNumber(S const &str, T def = 0, typename std::enable_if::value && (std::is_same_v || std::is_same_v)>::type *dummy = nullptr); template T toNumber(std::string const &str, T def = 0, typename std::enable_if::value>::type *dummy = nullptr); template T toNumberFromHex(std::string const &str, T def = 0); template void destroyPtr(P **p, T *psize); template inline std::string toString(T const &num, typename std::enable_if::value>::type *dummy = nullptr); template inline std::string toHexString(T const &num, typename std::enable_if::value>::type *dummy = nullptr); inline std::string toString(double num); inline constexpr int strlitLength(char const *str, int pos = 0); //inline int strlitLength(std::string const &str); inline int numDigits(long long int num); inline std::string toDateString(std::time_t epoch, std::string const &format); inline std::string toLower(std::string s); inline std::string toUpper(std::string s); inline void replaceAll(std::string *in, char from, std::string const &to); inline void replaceAll(std::string *in, std::string const &from, std::string const &to); template class container_has_contains { template static std::false_type test(...); template static constexpr auto test(int) -> decltype(std::declval().contains(std::declval()), std::true_type()); public: static constexpr bool value = std::is_same(0)), std::true_type>::value; }; template class container_has_find { template static std::false_type test(...); template static constexpr auto test(int) -> decltype(std::declval().find(std::declval()), std::true_type()); public: static constexpr bool value = std::is_same(0)), std::true_type>::value; }; template inline constexpr bool contains(T const &container, I const &item, typename std::enable_if::value>::type *dummy [[maybe_unused]] = nullptr) { if constexpr (container_has_contains::value) return container.contains(item); else if constexpr (container_has_find::value) return container.find(item) != container.end(); else return std::find(container.begin(), container.end(), item) != container.end(); } template inline constexpr bool contains(T const *const container, I const &item) { if constexpr (container_has_contains::value) return container->contains(item); else if constexpr (container_has_find::value) return container->find(item) != container->end(); else return std::find(container->begin(), container->end(), item) != container->end(); } template inline int findIdxOf(T const &container, U const &value); } #if defined DEBUGMSG || DEBUGISSUE template inline void bepaald::log(Args && ...args) { std::cout.copyfmt(std::ios(nullptr)); (std::cout << ... << args) << std::endl; } #endif //#include template T bepaald::toNumber(S const &str, T def, typename std::enable_if::value && (std::is_same_v || std::is_same_v)>::type *) { if (str.empty()) [[unlikely]] return def; int sign = 1; int lowestpos = 0; if (str[0] == '-') [[unlikely]] { ++lowestpos; sign = -1; } T value = 0; T multi = 1; for (int i = str.size() - 1; i >= lowestpos; --i) { value += static_cast((str[i] - '0')) * multi; multi *= 10; if (str[i] > '9' || value < 0) [[unlikely]] return def; } return value * sign; // std::istringstream s(str); // T i = def; // if (!(s >> i)) [[unlikely]] // return def; // return i; } // non-integral to number, not ever called I dont think template T bepaald::toNumber(std::string const &str, T def, typename std::enable_if::value>::type *) { std::istringstream s(str); T i = def; if (!(s >> i)) [[unlikely]] return def; return i; } template T bepaald::toNumberFromHex(std::string const &str, T def) { std::istringstream s(str); T i = def; if (!(s >> std::hex >> i >> std::dec)) [[unlikely]] return def; return i; } template inline void bepaald::destroyPtr(P **p, T *psize) { if (*p) { delete[] *p; *p = nullptr; *psize = 0; } } template inline std::string bepaald::toString(T const &num, typename std::enable_if::value>::type *) { return std::to_string(num); //std::ostringstream oss; //oss << std::dec << num; //return oss.str(); } template inline std::string bepaald::toHexString(T const &num, typename std::enable_if::value>::type *) { #if __cpp_lib_format >= 201907L return std::format("{:x}", num); #else std::ostringstream oss; oss << std::hex << num << std::dec; return oss.str(); #endif } inline std::string bepaald::toString(double num) { std::ostringstream oss; oss << std::defaultfloat << std::setprecision(17) << num; return oss.str(); } inline constexpr int bepaald::strlitLength(char const *str, int pos) { return str[pos] == '\0' ? 0 : 1 + strlitLength(str, pos + 1); } // inline int bepaald::strlitLength(std::string const &str) // { // return str.size(); // } inline int bepaald::numDigits(long long int num) { int count = 0; while (num) { num /= 10; ++count; } return count; } inline std::string bepaald::toDateString(std::time_t epoch, std::string const &format) { std::ostringstream tmp; tmp << std::put_time(std::localtime(&epoch), format.c_str()); return tmp.str(); } inline std::string bepaald::toLower(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); }); return s; } inline std::string bepaald::toUpper(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); }); return s; } inline void bepaald::replaceAll(std::string *in, char from, std::string const &to) { replaceAll(in, std::string(1, from), to); } inline void bepaald::replaceAll(std::string *in, std::string const &from, std::string const &to) { size_t start_pos = 0; while ((start_pos = in->find(from, start_pos)) != std::string::npos) { in->replace(start_pos, from.length(), to); start_pos += to.length(); } } template inline int bepaald::findIdxOf(T const &container, U const &value) { auto it = std::find(container.begin(), container.end(), value); if (it == container.end()) return -1; return std::distance(container.begin(), it); } #ifdef SIGNALBACKUP_TOOLS_REPORT_MEM #define MEMINFO(...) process_mem_usage(__VA_ARGS__) #include #include #include #include // code adapted from https://stackoverflow.com/questions/669438 by Don Wakefield template void process_mem_usage(Args && ...args) { std::cout.copyfmt(std::ios(nullptr)); (std::cout << ... << args) << std::endl; // the two fields we want unsigned long vsize = 0; long rss = 0; { // dummy vars for leading entries in stat that we don't care about std::string dummy; // 'file' stat seems to give the most reliable results std::ifstream stat_stream("/proc/self/stat", std::ios_base::in); stat_stream >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> dummy >> vsize >> rss; // don't care about the rest } long page_size_kb = sysconf(_SC_PAGE_SIZE) / 1024; // in case x86-64 is configured to use 2MB pages double vm_usage = vsize / 1024.0; double resident_set = rss * page_size_kb; std::cout << " *** VM: " << std::fixed << std::setprecision(2) << (vm_usage / 1024) << "MB ; RSS: " << (resident_set / 1024) << "MB" << std::endl; } #else #define MEMINFO(...) #endif #endif signalbackup-tools-20250313-1/common_bytes.h000066400000000000000000000117241476450434500206020ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef COMMON_BYTES_H_ #define COMMON_BYTES_H_ #include #include #include #include #include #include #include #if __cpp_lib_byteswap >= 202110L #include #endif #include "logger/logger.h" using std::literals::string_literals::operator""s; namespace bepaald { template inline T swap_endian(T u); std::string bytesToHexString(std::pair, unsigned int> const &data, bool unformatted = false); std::string bytesToHexString(std::pair const &data, bool unformatted = false); std::string bytesToHexString(unsigned char const *data, unsigned int length, bool unformatted = false); std::string bytesToString(unsigned char const *data, unsigned int length); std::string bytesToPrintableString(unsigned char const *data, unsigned int length); inline bool hexStringToBytes(unsigned char const *in, uint64_t insize, unsigned char *out, uint64_t outsize); inline bool hexStringToBytes(std::string const &in, unsigned char *out, uint64_t outsize); } template inline T bepaald::swap_endian(T u) { #if __cpp_lib_byteswap >= 202110L return std::byteswap(u); #else static_assert(CHAR_BIT == 8, "CHAR_BIT != 8"); union { T u; unsigned char u8[sizeof(T)]; } source, dest; source.u = u; for (size_t k = 0; k < sizeof(T); ++k) dest.u8[k] = source.u8[sizeof(T) - k - 1]; return dest.u; #endif } inline std::string bepaald::bytesToHexString(std::pair, unsigned int> const &data, bool unformatted) { return bytesToHexString(data.first.get(), data.second, unformatted); } inline std::string bepaald::bytesToHexString(std::pair const &data, bool unformatted/* = false*/) { return bytesToHexString(data.first, data.second, unformatted); } inline std::string bepaald::bytesToHexString(unsigned char const *data, unsigned int length, bool unformatted/* = false*/) { std::ostringstream oss; if (!unformatted) oss << "(hex:) "; for (unsigned int i = 0; i < length; ++i) oss << std::hex << std::setfill('0') << std::setw(2) << (static_cast(data[i]) & 0xFF) << ((i == length - 1 || unformatted) ? "" : " "); return oss.str(); } inline std::string bepaald::bytesToString(unsigned char const *data, unsigned int length) { std::ostringstream oss; for (unsigned int i = 0; i < length; ++i) oss << static_cast(data[i]); return oss.str(); } inline std::string bepaald::bytesToPrintableString(unsigned char const *data, unsigned int length) { bool prevwashex = false; std::ostringstream oss; for (unsigned int i = 0; i < length; ++i) { bool curishex = !std::isprint(static_cast(data[i])); if (curishex != prevwashex && i > 0) oss << " "; if (curishex) oss << "0x" << std::hex << std::setfill('0') << std::setw(2) << (static_cast(data[i]) & 0xFF) << (i == length - 1 ? "" : " "); else oss << static_cast(data[i]); prevwashex = curishex; } return oss.str(); } inline bool bepaald::hexStringToBytes(unsigned char const *in, uint64_t insize, unsigned char *out, uint64_t outsize) { if (insize % 2 || outsize != insize / 2) [[unlikely]] { Logger::error("Invalid size for hex string or output array too small"); out = nullptr; return false; } auto charToInt = [] (char c) { if (c <= '9' && c >= '0') return c - '0'; if (c <= 'F' && c >= 'A') return c - 'A' + 10; // if (c <= 'f' && c >= 'a') // lets assume input is valid... return c - 'a' + 10; }; uint64_t outpos = 0; for (unsigned int i = 0; i < insize - 1; i += 2) out[outpos++] = charToInt(in[i]) * 16 + charToInt(in[i + 1]); return true; } inline bool bepaald::hexStringToBytes(std::string const &in, unsigned char *out, uint64_t outsize) { // sanitize input; std::string input = in; auto newend = std::remove_if(input.begin(), input.end(), [](char c) { return (c > '9' || c < '0') && (c > 'F' || c < 'A') && (c > 'f' || c < 'a'); }); input.erase(newend, input.end()); return hexStringToBytes(reinterpret_cast(input.c_str()), input.size(), out, outsize); } #endif signalbackup-tools-20250313-1/common_filesystem.h000066400000000000000000000075351476450434500216450ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef COMMON_FILESYSTEM_H_ #define COMMON_FILESYSTEM_H_ #include #include "logger/logger.h" using std::literals::string_literals::operator""s; #if defined(_WIN32) || defined(__MINGW64__) //#define WIN_LONGPATH(...) bepaald::windows_long_file( __VA_ARGS__ ) #define WIN_LONGPATH(...) __VA_ARGS__ #else #define WIN_LONGPATH(...) __VA_ARGS__ #endif namespace bepaald { inline bool fileOrDirExists(std::string const &path); inline bool fileOrDirExists(std::filesystem::path const &path); inline bool isDir(std::string const &path); inline bool createDir(std::string const &path); inline bool isEmpty(std::string const &path); inline bool clearDirectory(std::string const &path); inline uint64_t fileSize(std::string const &path); #if defined(_WIN32) || defined(__MINGW64__) inline std::string windows_long_file(std::string const &path); inline long long int abs_path_length(std::string const &path); #endif } inline bool bepaald::fileOrDirExists(std::string const &path) { std::error_code ec; return std::filesystem::exists(path, ec); } inline bool bepaald::fileOrDirExists(std::filesystem::path const &path) { std::error_code ec; return std::filesystem::exists(path, ec); } inline bool bepaald::isDir(std::string const &path) { std::error_code ec; return std::filesystem::is_directory(path, ec); } inline bool bepaald::createDir(std::string const &path) { std::error_code ec; return std::filesystem::create_directory(path, ec); } inline bool bepaald::isEmpty(std::string const &path) { std::error_code ec; for (auto const &p: std::filesystem::directory_iterator(path)) if (p.exists(ec)) return false; return true; } inline bool bepaald::clearDirectory(std::string const &path) { std::error_code ec; for (auto const &p: std::filesystem::directory_iterator(path)) if (std::filesystem::remove_all(p.path(), ec) == static_cast(-1)) return false; return true; } inline uint64_t bepaald::fileSize(std::string const &path) { std::error_code ec; return std::filesystem::file_size(std::filesystem::path(path), ec); } #if defined(_WIN32) || defined(__MINGW64__) inline std::string bepaald::windows_long_file(std::string const &path) { //Logger::message("WINDOWS_LONG_PATH: input \"", path, "\""); std::error_code ec; auto abs_path = std::filesystem::absolute(path, ec); if (ec) { Logger::error("Failed to get an absolute path for '", path, "'"); return path; } //Logger::message("WINDOWS_LONG_PATH: output \"", "\\\\?\\" + abs_path.string(), "\""); // prepend windows magic long path prefix: return R"(\\?\)" + abs_path.string(); } #endif #if defined(_WIN32) || defined(__MINGW64__) inline long long int bepaald::abs_path_length(std::string const &path) { //Logger::message("WINDOWS_LONG_PATH: input \"", path, "\""); std::error_code ec; auto abs_path = std::filesystem::absolute(path, ec); if (ec) { Logger::error("Failed to get an absolute path for '", path, "'"); return -1; } //Logger::message("WINDOWS_PATH_LENGTH: \"", abs_path.string(), "\", ", abs_path.string().size()); return abs_path.string().size(); } #endif #endif signalbackup-tools-20250313-1/cryptbase/000077500000000000000000000000001476450434500177225ustar00rootroot00000000000000signalbackup-tools-20250313-1/cryptbase/cryptbase.h000066400000000000000000000170701476450434500220740ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef CRYPTBASE_H_ #define CRYPTBASE_H_ #include #include #include "../common_be.h" #include "../common_bytes.h" class CryptBase { public: unsigned int static constexpr MACSIZE = 10; protected: bool d_ok; bool d_verbose; unsigned char *d_backupkey; uint64_t d_backupkey_size; unsigned char *d_cipherkey; uint64_t d_cipherkey_size; unsigned char *d_mackey; uint64_t d_mackey_size; unsigned char *d_iv; uint64_t d_iv_size; unsigned char *d_salt; uint64_t d_salt_size; uint64_t d_counter; public: inline explicit CryptBase(bool verbose); inline CryptBase(CryptBase const &other); inline CryptBase &operator=(CryptBase const &other); inline CryptBase(CryptBase &&other); inline CryptBase &operator=(CryptBase &&other); inline ~CryptBase(); inline bool ok() const; protected: bool getCipherAndMac(unsigned int hashoutputsize, size_t outputsize); bool getBackupKey(std::string const &passphrase); inline void uintToFourBytes(unsigned char *bytes, uint32_t val) const; inline uint32_t fourBytesToUint(unsigned char const *b) const; }; inline CryptBase::CryptBase(bool verbose) : d_ok(false), d_verbose(verbose), d_backupkey(nullptr), d_backupkey_size(0), d_cipherkey(nullptr), d_cipherkey_size(0), d_mackey(nullptr), d_mackey_size(0), d_iv(nullptr), d_iv_size(0), d_salt(nullptr), d_salt_size(0), d_counter(0) {} inline CryptBase::CryptBase(CryptBase const &other) : d_ok(false), d_verbose(other.d_verbose), d_backupkey(nullptr), d_backupkey_size(other.d_backupkey_size), d_cipherkey(nullptr), d_cipherkey_size(other.d_cipherkey_size), d_mackey(nullptr), d_mackey_size(other.d_mackey_size), d_iv(nullptr), d_iv_size(other.d_iv_size), d_salt(nullptr), d_salt_size(other.d_salt_size), d_counter(other.d_counter) { if (other.d_backupkey) { d_backupkey = new unsigned char[d_backupkey_size]; std::memcpy(d_backupkey, other.d_backupkey, d_backupkey_size); } if (other.d_cipherkey) { d_cipherkey = new unsigned char[d_cipherkey_size]; std::memcpy(d_cipherkey, other.d_cipherkey, d_cipherkey_size); } if (other.d_mackey) { d_mackey = new unsigned char[d_mackey_size]; std::memcpy(d_mackey, other.d_mackey, d_mackey_size); } if (other.d_iv) { d_iv = new unsigned char[d_iv_size]; std::memcpy(d_iv, other.d_iv, d_iv_size); } if (other.d_salt) { d_salt = new unsigned char[d_salt_size]; std::memcpy(d_salt, other.d_salt, d_salt_size); } d_ok = true; } inline CryptBase &CryptBase::operator=(CryptBase const &other) { if (this != &other) { bepaald::destroyPtr(&d_iv, &d_iv_size); bepaald::destroyPtr(&d_salt, &d_salt_size); bepaald::destroyPtr(&d_backupkey, &d_backupkey_size); bepaald::destroyPtr(&d_mackey, &d_mackey_size); bepaald::destroyPtr(&d_cipherkey, &d_cipherkey_size); d_backupkey_size = other.d_backupkey_size; d_cipherkey_size = other.d_cipherkey_size; d_mackey_size = other.d_mackey_size; d_iv_size = other.d_iv_size; d_salt_size = other.d_salt_size; if (other.d_backupkey) { d_backupkey = new unsigned char[d_backupkey_size]; std::memcpy(d_backupkey, other.d_backupkey, d_backupkey_size); } if (other.d_cipherkey) { d_cipherkey = new unsigned char[d_cipherkey_size]; std::memcpy(d_cipherkey, other.d_cipherkey, d_cipherkey_size); } if (other.d_mackey) { d_mackey = new unsigned char[d_mackey_size]; std::memcpy(d_mackey, other.d_mackey, d_mackey_size); } if (other.d_iv) { d_iv = new unsigned char[d_iv_size]; std::memcpy(d_iv, other.d_iv, d_iv_size); } if (other.d_salt) { d_salt = new unsigned char[d_salt_size]; std::memcpy(d_salt, other.d_salt, d_salt_size); } d_counter = other.d_counter; d_verbose = other.d_verbose; d_ok = other.d_ok; } return *this; } inline CryptBase::CryptBase(CryptBase &&other) : d_ok(std::move(other.d_ok)), d_verbose(std::move(other.d_verbose)), d_backupkey(std::move(other.d_backupkey)), d_backupkey_size(std::move(other.d_backupkey_size)), d_cipherkey(std::move(other.d_cipherkey)), d_cipherkey_size(std::move(other.d_cipherkey_size)), d_mackey(std::move(other.d_mackey)), d_mackey_size(std::move(other.d_mackey_size)), d_iv(std::move(other.d_iv)), d_iv_size(std::move(other.d_iv_size)), d_salt(std::move(other.d_salt)), d_salt_size(std::move(other.d_salt_size)), d_counter(std::move(other.d_counter)) { other.d_backupkey = nullptr; other.d_backupkey_size = 0; other.d_cipherkey = nullptr; other.d_cipherkey_size = 0; other.d_mackey = nullptr; other.d_mackey_size = 0; other.d_iv = nullptr; other.d_iv_size = 0; other.d_salt = nullptr; other.d_salt_size = 0; } inline CryptBase &CryptBase::operator=(CryptBase &&other) { if (this != &other) { // destroy any data this already owns bepaald::destroyPtr(&d_iv, &d_iv_size); bepaald::destroyPtr(&d_salt, &d_salt_size); bepaald::destroyPtr(&d_backupkey, &d_backupkey_size); bepaald::destroyPtr(&d_mackey, &d_mackey_size); bepaald::destroyPtr(&d_cipherkey, &d_cipherkey_size); // take over other's data d_ok = std::move(other.d_ok); d_verbose = std::move(other.d_verbose); d_backupkey = std::move(other.d_backupkey); d_backupkey_size = std::move(other.d_backupkey_size); d_cipherkey = std::move(other.d_cipherkey); d_cipherkey_size = std::move(other.d_cipherkey_size); d_mackey = std::move(other.d_mackey); d_mackey_size = std::move(other.d_mackey_size); d_iv = std::move(other.d_iv); d_iv_size = std::move(other.d_iv_size); d_salt = std::move(other.d_salt); d_salt_size = std::move(other.d_salt_size); d_counter = std::move(other.d_counter); // invalidate other other.d_backupkey = nullptr; other.d_backupkey_size = 0; other.d_cipherkey = nullptr; other.d_cipherkey_size = 0; other.d_mackey = nullptr; other.d_mackey_size = 0; other.d_iv = nullptr; other.d_iv_size = 0; other.d_salt = nullptr; other.d_salt_size = 0; } return *this; } inline CryptBase::~CryptBase() { bepaald::destroyPtr(&d_iv, &d_iv_size); bepaald::destroyPtr(&d_salt, &d_salt_size); bepaald::destroyPtr(&d_backupkey, &d_backupkey_size); bepaald::destroyPtr(&d_mackey, &d_mackey_size); bepaald::destroyPtr(&d_cipherkey, &d_cipherkey_size); } inline bool CryptBase::ok() const { return d_ok; } inline void CryptBase::uintToFourBytes(unsigned char *bytes, uint32_t val) const { val = bepaald::swap_endian(val); std::memcpy(bytes, reinterpret_cast(&val), 4); } inline uint32_t CryptBase::fourBytesToUint(unsigned char const *b) const { return static_cast(b[3] & 0xFF) | static_cast(b[2] & 0xFF) << 8 | static_cast(b[1] & 0xFF) << 16 | static_cast(b[0] & 0xFF) << 24; } #endif signalbackup-tools-20250313-1/cryptbase/cryptbase.ih000066400000000000000000000016001476450434500222350ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "cryptbase.h" #include #include "../logger/logger.h" #include #include #include signalbackup-tools-20250313-1/cryptbase/getbackupkey.cc000066400000000000000000000060011476450434500227040ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "cryptbase.ih" bool CryptBase::getBackupKey(std::string const &passphrase) { // convert passwords digits to unsigned char * size_t const passlength = 30; unsigned char pass[passlength]; unsigned int i = 0; unsigned int j = 0; while (i < passlength && j < passphrase.size()) { if (!std::isdigit(passphrase[j])) // skip non digits ++j; else pass[i++] = passphrase[j++]; } while (j < passphrase.size() && !std::isdigit(passphrase[j])) // also eat any trailing non-digits ++j; if (i != passlength) { Logger::error("Failed to parse passphrase from string '", passphrase, "' : passphrase too short! " "Need ", passlength, " digits, ", i, " provided"); return false; } if (j != passphrase.size()) // passlength == 30 && all chars in passphrase were processed { Logger::error("Failed to parse passphrase from string '", passphrase, "' : passphrase too long! " "Need ", passlength, " digits, ", passphrase.size(), " provided"); return false; } //std::cout << "Passphrase: " << bepaald::bytesToHexString(pass, passlength) << std::endl; std::unique_ptr mdctx(EVP_MD_CTX_create(), &::EVP_MD_CTX_free); if (mdctx.get() == nullptr || EVP_DigestInit_ex(mdctx.get(), EVP_sha512(), nullptr) != 1) { Logger::error("Failed to create message digest context"); return false; } EVP_DigestUpdate(mdctx.get(), d_salt, d_salt_size); unsigned long digest_size = EVP_MD_size(EVP_sha512()); std::unique_ptr digest(new unsigned char[digest_size]); for (i = 0; i < 250000; ++i) { if (i > 0) EVP_DigestUpdate(mdctx.get(), digest.get(), digest_size); else EVP_DigestUpdate(mdctx.get(), pass, passlength); EVP_DigestUpdate(mdctx.get(), pass, passlength); EVP_DigestFinal(mdctx.get(), digest.get(), nullptr); if (EVP_MD_CTX_reset(mdctx.get()) != 1 || EVP_DigestInit_ex(mdctx.get(), EVP_sha512(), nullptr) != 1) { Logger::error("Failed to reset digest context"); return false; } } d_backupkey_size = 32; // backupkey is digest trimmed to 32 bytes d_backupkey = new unsigned char[d_backupkey_size]; std::memcpy(d_backupkey, digest.get(), d_backupkey_size); return true; } signalbackup-tools-20250313-1/cryptbase/getcipherandmac.cc000066400000000000000000000042631476450434500233540ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "cryptbase.ih" bool CryptBase::getCipherAndMac(unsigned int hashoutputsize, size_t outputsize) { std::unique_ptr pctx(EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr), &::EVP_PKEY_CTX_free); if (EVP_PKEY_derive_init(pctx.get()) != 1 || EVP_PKEY_CTX_set_hkdf_md(pctx.get(), EVP_sha256()) != 1) { Logger::error("Failed to init HKDF"); return false; } unsigned int const info_size = 13; unsigned char info[info_size] = {'B','a','c','k','u','p',' ','E','x','p','o','r','t'}; //std::unique_ptr localsalt(new unsigned char[hashoutputsize]); if (EVP_PKEY_CTX_set1_hkdf_key(pctx.get(), d_backupkey, d_backupkey_size) != 1 || // EVP_PKEY_CTX_set1_hkdf_salt(pctx.get(), localsalt, hashoutputsize) != 1 || EVP_PKEY_CTX_add1_hkdf_info(pctx.get(), info, info_size) != 1) { Logger::error("Failed to set data for HKDF"); return false; } std::unique_ptr derived(new unsigned char[outputsize]); if (EVP_PKEY_derive(pctx.get(), derived.get(), &outputsize) != 1) { Logger::error("Error deriving HKDF"); return false; } d_cipherkey_size = hashoutputsize; d_cipherkey = new unsigned char[d_cipherkey_size]; std::memcpy(d_cipherkey, derived.get(), hashoutputsize); d_mackey_size = hashoutputsize; d_mackey = new unsigned char[d_mackey_size]; std::memcpy(d_mackey, derived.get() + hashoutputsize, hashoutputsize); return true; } signalbackup-tools-20250313-1/csvreader/000077500000000000000000000000001476450434500177045ustar00rootroot00000000000000signalbackup-tools-20250313-1/csvreader/csvreader.h000066400000000000000000000041651476450434500220410ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef CSVREADER_H_ #define CSVREADER_H_ #include #include #include #include "../logger/logger.h" class CSVReader { private: enum class CSVState { UNQUOTEDFIELD, QUOTEDFIELD, QUOTEDQUOTE }; std::ifstream d_csvfile; std::vector> d_results; bool d_ok; unsigned int d_fields; // an extra check public: inline explicit CSVReader(std::string const &filename); inline bool ok() const; inline size_t fields() const; inline size_t rows() const; inline std::string const &get(int field, int row) const; inline std::string const &getFieldName(int row) const; private: bool read(); CSVState readRow(std::string const &row, CSVState laststate); }; inline CSVReader::CSVReader(std::string const &filename) : d_csvfile(filename), d_ok(false), d_fields(0) { if (d_csvfile.is_open()) d_ok = read(); else Logger::error("Opening file '", filename, "' for reading."); } inline bool CSVReader::ok() const { return d_ok; } inline size_t CSVReader::fields() const { return d_results.size() ? d_results[0].size() : 0; } inline size_t CSVReader::rows() const { return d_results.size() - 1; } inline std::string const &CSVReader::get(int field, int row) const { return d_results[row + 1][field]; } inline std::string const &CSVReader::getFieldName(int field) const { return d_results[0][field]; } #endif signalbackup-tools-20250313-1/csvreader/csvreader.ih000066400000000000000000000014011476450434500222000ustar00rootroot00000000000000/* Copyright (C) 2021-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "csvreader.h" signalbackup-tools-20250313-1/csvreader/read.cc000066400000000000000000000032501476450434500211260ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "csvreader.ih" bool CSVReader::read() { CSVState initialstate = CSVState::UNQUOTEDFIELD; std::string row; while (!d_csvfile.eof()) { std::getline(d_csvfile, row); if (row.empty()) // skip empty rows continue; if (d_csvfile.eof()) return true; if (d_csvfile.bad() || d_csvfile.fail()) { Logger::error("Reading CSV file"); return false; } Logger::message("Got row: ", row); if ((initialstate = readRow(row, initialstate)) == CSVState::UNQUOTEDFIELD) { Logger::message("READ :", d_results.back()); // extra check: all rows must have same number of fields if (d_results.size() == 1) [[unlikely]] d_fields = d_results.back().size(); else // if (d_results.back().size() != d_fields) [[unlikely]] { Logger::error("invalid csv"); return false; } } } return initialstate == CSVState::UNQUOTEDFIELD; } signalbackup-tools-20250313-1/csvreader/readrow.cc000066400000000000000000000047151476450434500216650ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "csvreader.ih" CSVReader::CSVState CSVReader::readRow(std::string const &row, CSVReader::CSVState laststate) { CSVState state = laststate; size_t i = 0; if (laststate == CSVState::UNQUOTEDFIELD) { d_results.resize(d_results.size() + 1); // got next row, make room d_results.back().emplace_back(""); } else i = d_results.back().size() - 1; for (unsigned int c = 0; c < row.size(); ++c) { switch (state) { case CSVState::UNQUOTEDFIELD: switch (row[c]) { case ',': // end of field d_results.back().push_back(""); ++i; break; case '"': state = CSVState::QUOTEDFIELD; break; default: d_results.back()[i].push_back(row[c]); break; } break; case CSVState::QUOTEDFIELD: switch (row[c]) { case '"': state = (c == row.size() - 1) ? CSVState::UNQUOTEDFIELD : CSVState::QUOTEDQUOTE; break; default: d_results.back()[i].push_back(row[c]); break; } break; case CSVState::QUOTEDQUOTE: switch (row[c]) { case ',': // , after closing quote d_results.back().push_back(""); ++i; state = CSVState::UNQUOTEDFIELD; break; case '"': // "" -> " d_results.back()[i].push_back('"'); state = CSVState::QUOTEDFIELD; break; default: // end of quote state = CSVState::UNQUOTEDFIELD; break; } break; } } if (state != CSVState::UNQUOTEDFIELD) d_results.back()[i].push_back('\n'); return state; } signalbackup-tools-20250313-1/databaseversionframe/000077500000000000000000000000001476450434500221135ustar00rootroot00000000000000signalbackup-tools-20250313-1/databaseversionframe/databaseversionframe.h000066400000000000000000000134071476450434500264560ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef DATABASEVERSIONFRAME_H_ #define DATABASEVERSIONFRAME_H_ #include #include "../common_be.h" #include "../backupframe/backupframe.h" class DatabaseVersionFrame : public BackupFrame { enum FIELD: unsigned int { INVALID = 0, VERSION = 1 // uint32 }; static Registrar s_registrar; public: inline explicit DatabaseVersionFrame(uint64_t count = 0); inline DatabaseVersionFrame(unsigned char const *bytes, size_t length, uint64_t count = 0); inline virtual ~DatabaseVersionFrame() override = default; inline virtual DatabaseVersionFrame *clone() const override; inline virtual DatabaseVersionFrame *move_clone() override; inline static BackupFrame *create(unsigned char const *bytes, size_t length, uint64_t count = 0); inline virtual FRAMETYPE frameType() const override; inline uint32_t version() const; inline virtual void printInfo() const override; inline virtual std::pair getData() const override; inline virtual bool validate(uint64_t) const override; inline std::string getHumanData() const override; //inline virtual bool setNewData(std::string const &field, std::string const &data) override; inline unsigned int getField(std::string_view const &str) const; private: inline uint64_t dataSize() const override; }; inline DatabaseVersionFrame::DatabaseVersionFrame(uint64_t count) : BackupFrame(count) {} inline DatabaseVersionFrame::DatabaseVersionFrame(unsigned char const *bytes, size_t length, uint64_t count) : BackupFrame(bytes, length, count) { //std::cout << "CREATING DATABASEVERSIONFRAME" << std::endl; } inline DatabaseVersionFrame *DatabaseVersionFrame::clone() const { return new DatabaseVersionFrame(*this); } inline DatabaseVersionFrame *DatabaseVersionFrame::move_clone() { return new DatabaseVersionFrame(std::move(*this)); } inline BackupFrame *DatabaseVersionFrame::create(unsigned char const *bytes, size_t length, uint64_t count) // static { return new DatabaseVersionFrame(bytes, length, count); } inline BackupFrame::FRAMETYPE DatabaseVersionFrame::frameType() const // virtual { return FRAMETYPE::DATABASEVERSION; } inline uint32_t DatabaseVersionFrame::version() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::VERSION) return bytesToUint32(std::get<1>(p), std::get<2>(p)); return 0; } inline void DatabaseVersionFrame::printInfo() const { Logger::message("Frame number: ", d_count); Logger::message(" Size: ", d_constructedsize); Logger::message(" Type: DATABASEVERSION"); Logger::message(" - Version: ", version()); } inline uint64_t DatabaseVersionFrame::dataSize() const { uint64_t size = 0; for (auto const &p : d_framedata) { switch (std::get<0>(p)) { case FIELD::VERSION: { uint32_t value = bytesToUint32(std::get<1>(p), std::get<2>(p)); size += varIntSize(value); size += 1; // for fieldtype + wiretype } } } // for size of this frame. size += varIntSize(size); return ++size; // for frametype and wiretype } inline std::pair DatabaseVersionFrame::getData() const { // first write the frametype as and the wiretype // 0b01111000 == fieldnumber (== 5 for databaseversionframe) // 0b00000111 == wiretype (== 2, lengthdelim, for all frames except endframe which is bool?) uint64_t size = dataSize(); unsigned char *data = new unsigned char[size]; uint64_t datapos = 0; datapos += setFieldAndWire(FRAMETYPE::DATABASEVERSION, WIRETYPE::LENGTHDELIM, data + datapos); datapos += setFrameSize(size, data + datapos); for (auto const &fd : d_framedata) { //switch (std::get<0>(p)) //{ //case FIELD::VERSION: //uint32_t value = bytesToUint32(std::get<1>(p), std::get<2>(p)); //datapos += setFieldAndWire(FIELD::VERSION, WIRETYPE::VARINT, data + datapos); //datapos += putVarInt(value, data + datapos); datapos += putVarIntType(fd, data + datapos); //} } //std::cout << bepaald::bytesToHexString(data, size) << std::endl; return {data, size}; } inline bool DatabaseVersionFrame::validate(uint64_t) const { if (d_framedata.empty()) return false; for (auto const &p : d_framedata) { if (std::get<0>(p) != FIELD::VERSION) return false; } return true; } inline std::string DatabaseVersionFrame::getHumanData() const { std::string data; for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::VERSION) data += "VERSION:uint32:" + bepaald::toString(version()) + "\n"; return data; } /* inline bool DatabaseVersionFrame::setNewData(std::string const &field, std::string const &data) { if (field != "VERSION") return false; std::pair decdata = numToData(bepaald::swap_endian(std::stoul(data))); d_framedata.emplace_back(std::make_tuple(FIELD::VERSION, decdata.first, decdata.second)); return true; } */ inline unsigned int DatabaseVersionFrame::getField(std::string_view const &str) const { if (str == "VERSION") return FIELD::VERSION; return FIELD::INVALID; } #endif signalbackup-tools-20250313-1/databaseversionframe/databaseversionframe.ih000066400000000000000000000014141476450434500266220ustar00rootroot00000000000000/* Copyright (C) 2019-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "databaseversionframe.h" signalbackup-tools-20250313-1/databaseversionframe/statics.cc000066400000000000000000000016131476450434500240750ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "databaseversionframe.ih" DatabaseVersionFrame::Registrar DatabaseVersionFrame::s_registrar(FRAMETYPE::DATABASEVERSION, DatabaseVersionFrame::create); signalbackup-tools-20250313-1/dbuscon/000077500000000000000000000000001476450434500173635ustar00rootroot00000000000000signalbackup-tools-20250313-1/dbuscon/dbuscon.h000066400000000000000000000650411476450434500211770ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ /* * !! NOTE !! * * This class is by no means a complete dbus service interface * it (hopefully) works for the current purpose of this program. * * There are certainly limitations in sending/receiving * complex nested types (DICTS of ARRAYS of DICTS...) * */ #ifndef DBUSCONNECTION_H_ #define DBUSCONNECTION_H_ #if !defined(_WIN32) && !defined(__MINGW64__) && (!defined(__APPLE__) || !defined(__MACH__)) #if !defined WITHOUT_DBUS #include #include #include #include #include #include #include #include #include "../logger/logger.h" template struct is_std_map : std::false_type {}; template struct is_std_map> : std::true_type {}; template class recursive_wrapper { // Wrapper over unique_ptr. std::unique_ptr d_impl; public: // Automatic construction from a `T`, not a `T*`. recursive_wrapper(T &&obj) : d_impl(new T(std::move(obj))) {} recursive_wrapper(T const &obj) : d_impl(new T(obj)) {} // Copy constructor copies `T`. recursive_wrapper(const recursive_wrapper &other) : recursive_wrapper(*other.d_impl) {} recursive_wrapper &operator=(const recursive_wrapper &other) { *d_impl = *other.d_impl; return *this; } // unique_ptr destroys `T` for us. ~recursive_wrapper() = default; // Access propagates constness. T &operator*() { return *d_impl; } T const &operator*() const { return *d_impl; } T *operator->() { return d_impl.get(); } const T *operator->() const { return d_impl.get(); } }; using DBusArg = std::variant, struct DBusObjectPath, struct DBusArray, recursive_wrapper>; struct DBusObjectPath { std::string d_value; }; struct DBusArray : public std::vector { using std::vector::vector; }; struct DBusVariant { DBusArg d_value; }; struct DBusDictElement { DBusArg d_key; // NOTE: only basic types are allowed as dict key by dbus spec DBusArg d_value; }; struct DBusDict : public std::vector { using std::vector::vector; }; class DBusCon { DBusError d_error; DBusConnection *d_connection; std::unique_ptr d_reply; bool d_dbus_verbose; bool d_ok; public: inline DBusCon(bool dbus_verbose); inline DBusCon(DBusCon const &other) = delete; inline DBusCon &operator=(DBusCon const &other) = delete; inline ~DBusCon(); inline bool ok() const; inline void callMethod(std::string const &destination, std::string const &path, std::string const &interface, std::string const &method, std::vector const &args); inline void callMethod(std::string const &destination, std::string const &path, std::string const &interface, std::string const &method); inline void showResponse(DBusMessage *reply); inline bool matchSignal(std::string const &matchingrule); inline bool waitSignal(int attempts, int timeoutms_per_attempt, std::string const &interface, std::string const &name); template inline T get(std::string const &sig, std::vector const &idx, T def = T{}); template inline T get(std::string const &sig, int idx, T def = T{}); private: template inline void addBasic(T const &t, DBusMessageIter *dbus_iter, bool isvar = false, bool isarray = false); inline void passArg(DBusDictElement const &arg, DBusMessageIter *dbus_iter); inline void passArg(DBusArg const &arg, DBusMessageIter *dbus_iter, bool isvar = false, bool isarray = false); inline void showresponse2(DBusMessageIter *iter, int indent); template inline bool setBasicTypeReturn(T *target, int current_type, DBusMessageIter *iter); template inline void set(T *ret, DBusMessageIter *iter, int current_type); template inline T get2(DBusMessageIter *iter, std::vector const &idx, T def = T{}); }; inline DBusCon::DBusCon(bool dbus_verbose) : d_connection(nullptr), d_reply(nullptr, &::dbus_message_unref), d_dbus_verbose(dbus_verbose), d_ok(false) { dbus_error_init(&d_error); d_connection = dbus_bus_get_private(DBUS_BUS_SESSION, &d_error); if (d_connection) d_ok = true; } inline DBusCon::~DBusCon() { if (d_connection) { dbus_connection_close(d_connection); dbus_connection_unref(d_connection); } if (dbus_error_is_set(&d_error)) dbus_error_free(&d_error); } inline bool DBusCon::ok() const { return d_ok; } inline bool DBusCon::matchSignal(std::string const &matchingrule) { //Rules are specified as a string of comma separated key/value pairs. An example is "type='signal',sender='org.freedesktop.DBus', interface='org.freedesktop.DBus',member='Foo', path='/bar/foo',destination=':452345.34'" // Possible keys you can match on are type, sender, interface, member, path, destination and numbered keys to match message args (keys are 'arg0', 'arg1', etc.). dbus_bus_add_match(d_connection, matchingrule.c_str(), &d_error); if (dbus_error_is_set(&d_error)) { Logger::error("::dbus_message_new_method_call - Unable to allocate memory for the message!"); return false; } dbus_connection_flush(d_connection); return true; } inline bool DBusCon::waitSignal(int attempts, int timeoutms_per_attempt, std::string const &interface, std::string const &name) { if (d_dbus_verbose) Logger::message_start("(waitSignal)"); std::unique_ptr dbus_signal_msg(nullptr, &::dbus_message_unref); for (int i = 0; i < attempts; ++i) { if (d_dbus_verbose) Logger::message_continue("."); dbus_connection_read_write(d_connection, timeoutms_per_attempt); while (true) { dbus_signal_msg.reset(dbus_connection_pop_message(d_connection)); if (!dbus_signal_msg) break; // showResponse(dbus_signal_msg.get()); // std::cout << "sender " << dbus_message_get_sender(dbus_signal_msg.get())); // std::cout << "path " << dbus_message_get_path(dbus_signal_msg.get())); // std::cout << "iface " << dbus_message_get_interface(dbus_signal_msg.get())); // std::cout << "dest " << dbus_message_get_destination(dbus_signal_msg.get())); // std::cout << "member " << dbus_message_get_member(dbus_signal_msg.get())); // std::cout << "type " << dbus_message_get_type(dbus_signal_msg.get())); // check if the message is a signal from the correct interface and with the correct name if (dbus_message_is_signal(dbus_signal_msg.get(), interface.c_str(), name.c_str())) { if (d_dbus_verbose) { Logger::message(" *** RECEIVED SIGNAL WE WERE WATING FOR..."); showResponse(dbus_signal_msg.get()); } return true; } // else // { // std::cout << "(different message received)"); // } } } if (d_dbus_verbose) Logger::message_end(); return false; } template inline void DBusCon::addBasic(T const &t, DBusMessageIter *dbus_iter, bool isvar, bool isarray [[maybe_unused]]) { if constexpr (std::is_same_v) { if (isvar) { DBusMessageIter dbus_iter_sub; dbus_message_iter_open_container(dbus_iter, DBUS_TYPE_VARIANT, DBUS_TYPE_OBJECT_PATH_AS_STRING, &dbus_iter_sub); dbus_message_iter_append_basic(&dbus_iter_sub, DBUS_TYPE_OBJECT_PATH, &t.d_value); dbus_message_iter_close_container(dbus_iter, &dbus_iter_sub); } else dbus_message_iter_append_basic(dbus_iter, DBUS_TYPE_OBJECT_PATH, &t.d_value); } if constexpr (std::is_same_v) { if (isvar) { DBusMessageIter dbus_iter_sub; dbus_message_iter_open_container(dbus_iter, DBUS_TYPE_VARIANT, DBUS_TYPE_STRING_AS_STRING, &dbus_iter_sub); dbus_message_iter_append_basic(&dbus_iter_sub, DBUS_TYPE_STRING, &t); dbus_message_iter_close_container(dbus_iter, &dbus_iter_sub); } else dbus_message_iter_append_basic(dbus_iter, DBUS_TYPE_STRING, &t); } else if constexpr (std::is_same_v) { if (isvar) { DBusMessageIter dbus_iter_sub; dbus_message_iter_open_container(dbus_iter, DBUS_TYPE_VARIANT, DBUS_TYPE_INT32_AS_STRING, &dbus_iter_sub); dbus_message_iter_append_basic(&dbus_iter_sub, DBUS_TYPE_INT32, &t); dbus_message_iter_close_container(dbus_iter, &dbus_iter_sub); } dbus_message_iter_append_basic(dbus_iter, DBUS_TYPE_INT32, &t); } else if constexpr (std::is_same_v) { if (isvar) { DBusMessageIter dbus_iter_sub; dbus_message_iter_open_container(dbus_iter, DBUS_TYPE_VARIANT, DBUS_TYPE_INT64_AS_STRING, &dbus_iter_sub); dbus_message_iter_append_basic(&dbus_iter_sub, DBUS_TYPE_INT64, &t); dbus_message_iter_close_container(dbus_iter, &dbus_iter_sub); } dbus_message_iter_append_basic(dbus_iter, DBUS_TYPE_INT64, &t); } else if constexpr (std::is_same_v) { if (isvar) { DBusMessageIter dbus_iter_sub; dbus_message_iter_open_container(dbus_iter, DBUS_TYPE_VARIANT, DBUS_TYPE_BOOLEAN_AS_STRING, &dbus_iter_sub); dbus_message_iter_append_basic(&dbus_iter_sub, DBUS_TYPE_BOOLEAN, &t); dbus_message_iter_close_container(dbus_iter, &dbus_iter_sub); } int32_t b = t ? 1 : 0; dbus_message_iter_append_basic(dbus_iter, DBUS_TYPE_BOOLEAN, &b); } } inline void DBusCon::passArg(DBusDictElement const &arg, DBusMessageIter *dbus_iter) { if (d_dbus_verbose) Logger::message("Got arg : DICTELEM"); DBusMessageIter dbus_iter_dict; dbus_message_iter_open_container(dbus_iter, DBUS_TYPE_DICT_ENTRY, NULL, &dbus_iter_dict); passArg(arg.d_key, &dbus_iter_dict, false, false); passArg(arg.d_value, &dbus_iter_dict, false, false); dbus_message_iter_close_container(dbus_iter, &dbus_iter_dict); } inline void DBusCon::passArg(DBusArg const &arg, DBusMessageIter *dbus_iter, bool isvar, bool isarray) { if (std::holds_alternative(arg)) { if (d_dbus_verbose) Logger::message("Got arg : ", std::get(arg)); addBasic(std::get(arg), dbus_iter, isvar, isarray); } else if (std::holds_alternative(arg)) { if (d_dbus_verbose) Logger::message("Got arg : ", std::get(arg)); addBasic(std::get(arg), dbus_iter, isvar, isarray); } else if (std::holds_alternative(arg)) { if (d_dbus_verbose) Logger::message("Got arg : '", std::get(arg), "'"); addBasic(std::get(arg), dbus_iter, isvar, isarray); } else if (std::holds_alternative(arg)) { if (d_dbus_verbose) Logger::message("Got arg : ", std::boolalpha, std::get(arg), std::noboolalpha); addBasic(std::get(arg), dbus_iter, isvar, isarray); } else if (std::holds_alternative(arg)) { if (d_dbus_verbose) Logger::message("Got arg : ", "ARRAY"); DBusMessageIter dbus_array_iter; char const *arraytype = DBUS_TYPE_INVALID_AS_STRING; DBusArray const *array = &std::get(arg); if (array->empty()) // something ; else { if (std::holds_alternative(array->at(0))) arraytype = DBUS_TYPE_INT64_AS_STRING; else if (std::holds_alternative(array->at(0))) arraytype = DBUS_TYPE_INT32_AS_STRING; else if (std::holds_alternative(array->at(0))) arraytype = DBUS_TYPE_STRING_AS_STRING; else if (std::holds_alternative(array->at(0))) arraytype = DBUS_TYPE_BOOLEAN_AS_STRING; else if (std::holds_alternative(array->at(0))) arraytype = DBUS_TYPE_ARRAY_AS_STRING; else if (std::holds_alternative(array->at(0))) arraytype = DBUS_TYPE_OBJECT_PATH_AS_STRING; else if (std::holds_alternative>(array->at(0))) arraytype = DBUS_TYPE_VARIANT_AS_STRING; // to do? //else if (std::holds_alternative>(array->at(0))) // arraytype = DBUS_TYPE_VARIANT_AS_STRING; } if (std::strcmp(arraytype, DBUS_TYPE_INVALID_AS_STRING) == 0) return; dbus_message_iter_open_container(dbus_iter, DBUS_TYPE_ARRAY, arraytype, &dbus_array_iter); for (unsigned int i = 0; i < array->size(); ++i) passArg(array->at(i), &dbus_array_iter, isvar, isarray); dbus_message_iter_close_container(dbus_iter, &dbus_array_iter); } else if (std::holds_alternative(arg)) { if (d_dbus_verbose) Logger::message("Got arg : (o)'", std::get(arg).d_value, "'"); addBasic(std::get(arg), dbus_iter, isvar, isarray); } else if (std::holds_alternative>(arg)) { if (d_dbus_verbose) Logger::message("Got arg : VARIANT"); passArg(std::get>(arg)->d_value, dbus_iter, true, isarray); } else if (std::holds_alternative>(arg)) { if (d_dbus_verbose) Logger::message("Got arg : DICT"); DBusMessageIter dbus_array_iter; std::string dictspec = DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING; if (std::get>(arg)->empty()) ; // handle this somehow? else { DBusDictElement elem = std::get>(arg)->at(0); // key if (std::holds_alternative(elem.d_key)) dictspec += DBUS_TYPE_INT64_AS_STRING; else if (std::holds_alternative(elem.d_key)) dictspec += DBUS_TYPE_INT32_AS_STRING; else if (std::holds_alternative(elem.d_key)) dictspec += DBUS_TYPE_STRING_AS_STRING; else if (std::holds_alternative(elem.d_key)) dictspec += DBUS_TYPE_BOOLEAN_AS_STRING; // value if (std::holds_alternative(elem.d_value)) dictspec += DBUS_TYPE_INT64_AS_STRING; else if (std::holds_alternative(elem.d_value)) dictspec += DBUS_TYPE_INT32_AS_STRING; else if (std::holds_alternative(elem.d_value)) dictspec += DBUS_TYPE_STRING_AS_STRING; else if (std::holds_alternative(elem.d_value)) dictspec += DBUS_TYPE_BOOLEAN_AS_STRING; else if (std::holds_alternative(elem.d_value)) dictspec += DBUS_TYPE_OBJECT_PATH_AS_STRING; else if (std::holds_alternative>(elem.d_value)) // RECURSE! (?) dictspec += DBUS_TYPE_VARIANT_AS_STRING; else if (std::holds_alternative(elem.d_value)) // RECURSE! (?) dictspec += DBUS_TYPE_ARRAY_AS_STRING; } dictspec += DBUS_DICT_ENTRY_END_CHAR_AS_STRING; dbus_message_iter_open_container(dbus_iter, DBUS_TYPE_ARRAY, dictspec.c_str(), &dbus_array_iter); for (unsigned int i = 0; i < std::get>(arg)->size(); ++i) passArg(std::get>(arg)->at(i), &dbus_array_iter); dbus_message_iter_close_container(dbus_iter, &dbus_array_iter); } } inline void DBusCon::callMethod(std::string const &destination, std::string const &path, std::string const &interface, std::string const &method, std::vector const &args) { std::unique_ptr dbus_message(dbus_message_new_method_call(destination.c_str(), path.c_str(), interface.c_str(), method.c_str()), &::dbus_message_unref); if (!dbus_message) { Logger::error("::dbus_message_new_method_call - Unable to allocate memory for the message!"); return; } // set args if (!args.empty()) { DBusMessageIter dbus_iter; dbus_message_iter_init_append(dbus_message.get(), &dbus_iter); for (auto const &a : args) passArg(a, &dbus_iter); } // get reply d_reply.reset(dbus_connection_send_with_reply_and_block(d_connection, dbus_message.get(), DBUS_TIMEOUT_USE_DEFAULT, &d_error)); if (!d_reply) { Logger::error(d_error.name, " : ", d_error.message); return; } // show output if (d_dbus_verbose) showResponse(d_reply.get()); } inline void DBusCon::callMethod(std::string const &destination, std::string const &path, std::string const &interface, std::string const &method) { return callMethod(destination, path, interface, method, {}); } inline void DBusCon::showresponse2(DBusMessageIter *iter, int indent) { // auto charsinnumber = [](int num) // { // int i = 0; // while (num /= 10) // ++i; // return i + 1; // }; int current_type; int idx = 0; while ((current_type = dbus_message_iter_get_arg_type(iter)) != DBUS_TYPE_INVALID) { char *cursig = dbus_message_iter_get_signature(iter); Logger::message_start(std::string(indent, ' '), idx++, ". Got reply (", (char)current_type, ") (sig: \"", cursig, "\") : "); dbus_free(cursig); if (current_type == DBUS_TYPE_VARIANT || current_type == DBUS_TYPE_ARRAY || current_type == DBUS_TYPE_DICT_ENTRY || current_type == DBUS_TYPE_STRUCT) { Logger::message_continue(" -> recursing... "); if (current_type == DBUS_TYPE_ARRAY) Logger::message_continue("(", dbus_message_iter_get_element_count(iter), ")"); Logger::message_end(); DBusMessageIter iter_sub; dbus_message_iter_recurse(iter, &iter_sub); showresponse2(&iter_sub, indent + 4); } else if (current_type == DBUS_TYPE_OBJECT_PATH) { char *path; dbus_message_iter_get_basic(iter, &path); Logger::message_continue("VALUE (o): '", path, "'"); } else if (current_type == DBUS_TYPE_STRING) { char *str; dbus_message_iter_get_basic(iter, &str); Logger::message_continue("VALUE (s): '", str, "'"); } else if (current_type == DBUS_TYPE_INT32) { int32_t i = 0; dbus_message_iter_get_basic(iter, &i); Logger::message_continue("VALUE (i32): ", i); } else if (current_type == DBUS_TYPE_INT64) { int64_t i = 0; dbus_message_iter_get_basic(iter, &i); Logger::message_continue("VALUE (i64): ", i); } else if (current_type == DBUS_TYPE_BOOLEAN) { bool i = 0; dbus_message_iter_get_basic(iter, &i); Logger::message_continue("VALUE (b): ", std::boolalpha, i, std::noboolalpha); } else if (current_type == DBUS_TYPE_BYTE) { unsigned char b = '\0'; dbus_message_iter_get_basic(iter, &b); std::isprint(b) ? Logger::message_continue("VALUE (byte): '", b, "' (0x", std::hex, std::setfill('0'), std::setw(2), (static_cast(b) & 0xFF), std::dec, ")") : Logger::message_continue("VALUE (byte): 0x", std::hex, std::setfill('0'), std::setw(2), (static_cast(b) & 0xFF), std::dec); } else { Logger::message_continue("[?]"); } dbus_message_iter_next(iter); } } inline void DBusCon::showResponse(DBusMessage *reply) { Logger::message(" -> Reply signature: ", dbus_message_get_signature(reply)); DBusMessageIter dbus_iter_reply; dbus_message_iter_init(reply, &dbus_iter_reply); showresponse2(&dbus_iter_reply, 4); } template inline bool DBusCon::setBasicTypeReturn(T *target [[maybe_unused]], int current_type, DBusMessageIter *iter [[maybe_unused]]) { if constexpr (std::is_same_v) { if (current_type == DBUS_TYPE_BOOLEAN) { int32_t b = 0; dbus_message_iter_get_basic(iter, &b); *target = b; return true; } } else if constexpr (std::is_same_v) { if ((current_type == DBUS_TYPE_STRING || current_type == DBUS_TYPE_OBJECT_PATH) && std::is_same_v) { char *str; dbus_message_iter_get_basic(iter, &str); *target = str; return true; } } else if constexpr (std::is_same_v || std::is_same_v) { if (current_type == DBUS_TYPE_INT32 || current_type == DBUS_TYPE_INT64) { dbus_message_iter_get_basic(iter, target); return true; } } return false; } template inline void DBusCon::set(T *ret, DBusMessageIter *iter, int current_type1) { if constexpr (std::is_same_v>) { if (current_type1 == DBUS_TYPE_ARRAY) { DBusMessageIter iter_sub; dbus_message_iter_recurse(iter, &iter_sub); int current_type2; while ((current_type2 = dbus_message_iter_get_arg_type(&iter_sub)) != DBUS_TYPE_INVALID) { if (current_type2 == DBUS_TYPE_STRING || current_type2 == DBUS_TYPE_OBJECT_PATH) { char *val; dbus_message_iter_get_basic(&iter_sub, &val); ret->push_back(val); } else { ret->clear(); return; } dbus_message_iter_next(&iter_sub); } } } if constexpr (std::is_same_v>) { if (current_type1 == DBUS_TYPE_ARRAY) { DBusMessageIter iter_sub; dbus_message_iter_recurse(iter, &iter_sub); int current_type2; while ((current_type2 = dbus_message_iter_get_arg_type(&iter_sub)) != DBUS_TYPE_INVALID) { if (current_type2 == DBUS_TYPE_BYTE) { unsigned char val; dbus_message_iter_get_basic(&iter_sub, &val); ret->push_back(val); } else { ret->clear(); return; } dbus_message_iter_next(&iter_sub); } } } if constexpr (is_std_map::value) { if (current_type1 == DBUS_TYPE_ARRAY) { DBusMessageIter iter_sub; dbus_message_iter_recurse(iter, &iter_sub); int current_type2; while ((current_type2 = dbus_message_iter_get_arg_type(&iter_sub)) != DBUS_TYPE_INVALID) // iterate the array { if (current_type2 != DBUS_TYPE_DICT_ENTRY) { ret->clear(); return; } DBusMessageIter iter_sub2; dbus_message_iter_recurse(&iter_sub, &iter_sub2); typename T::key_type newkey; typename T::mapped_type newvalue; if ((current_type2 = dbus_message_iter_get_arg_type(&iter_sub2)) == DBUS_TYPE_INVALID) { ret->clear(); return; } //std::cout << (char) current_type); // set key (must be basic type as per spec if (!setBasicTypeReturn(&newkey, current_type2, &iter_sub2)) { ret->clear(); return; } // next, get the mapped value. each DICT_ENTRY _must_ contain exactly 2 elements, as per spec dbus_message_iter_next(&iter_sub2); if ((current_type2 = dbus_message_iter_get_arg_type(&iter_sub2)) == DBUS_TYPE_INVALID) { ret->clear(); return; } //std::cout << (char) current_type); if (setBasicTypeReturn(&newvalue, current_type2, &iter_sub2)) ; else if (current_type2 == DBUS_TYPE_VARIANT) // recurse for this one { DBusMessageIter iter_sub3; dbus_message_iter_recurse(&iter_sub2, &iter_sub3); if ((current_type2 = dbus_message_iter_get_arg_type(&iter_sub3)) == DBUS_TYPE_INVALID) { ret->clear(); return; } // std::cout << (char) current_type); if (setBasicTypeReturn(&newvalue, current_type2, &iter_sub3)) ; else //(current_type == ...) { // only basic types inside variant for now... ret->clear(); return; } } else // ALL OTHER TYPES NOT YET SUPPORTED FOR MAPPED VALUE { ret->clear(); return; } // add pair... ret->insert({newkey, newvalue}); dbus_message_iter_next(&iter_sub); } } return; } setBasicTypeReturn(ret, current_type1, iter); /* if constexpr (std::is_same_v) { if (current_type == DBUS_TYPE_STRING || current_type == DBUS_TYPE_OBJECT_PATH) { char *val; dbus_message_iter_get_basic(iter, &val); *ret = val; } } if constexpr (std::is_same_v || std::is_same_v) { if (current_type == DBUS_TYPE_INT32) dbus_message_iter_get_basic(iter, ret); else if (current_type == DBUS_TYPE_INT64) dbus_message_iter_get_basic(iter, ret); } if constexpr (std::is_same_v) { if (current_type == DBUS_TYPE_BOOLEAN) dbus_message_iter_get_basic(iter, ret); } */ } template inline T DBusCon::get2(DBusMessageIter *iter, std::vector const &idx, T def) { T ret{def}; int i = 0; int current_type = dbus_message_iter_get_arg_type(iter); while (i < idx.front()) { //Logger::message("YO " << i << " TYPE: " << (char)current_type); dbus_message_iter_next(iter); current_type = dbus_message_iter_get_arg_type(iter); if (current_type == DBUS_TYPE_INVALID) return ret; ++i; } if (current_type == DBUS_TYPE_STRUCT) { DBusMessageIter iter_sub; dbus_message_iter_recurse(iter, &iter_sub); if (idx.size() < 2) { Logger::error("Missing next index"); return ret; } std::vector idx2 = idx; idx2.erase(idx2.begin()); ret = get2(&iter_sub, idx2, def); return ret; } if (current_type == DBUS_TYPE_VARIANT) { DBusMessageIter iter_sub; dbus_message_iter_recurse(iter, &iter_sub); current_type = dbus_message_iter_get_arg_type(&iter_sub); set(&ret, &iter_sub, current_type); return ret; } set(&ret, iter, current_type); return ret; } template inline T DBusCon::get(std::string const &sig, std::vector const &idx, T def) { if (!d_reply || dbus_message_get_signature(d_reply.get()) != sig) { if (/*verbose && */d_reply) Logger::warning("Unexpected reply signature (got '", dbus_message_get_signature(d_reply.get()), "', expected '", sig, "')"); return T{def}; } // get iterator DBusMessageIter iter; dbus_message_iter_init(d_reply.get(), &iter); return get2(&iter, idx, def); } template inline T DBusCon::get(std::string const &sig, int idx, T def) { return get(sig, std::vector{idx}, def); } #endif #endif #endif signalbackup-tools-20250313-1/deepcopyinguniqueptr/000077500000000000000000000000001476450434500222115ustar00rootroot00000000000000signalbackup-tools-20250313-1/deepcopyinguniqueptr/deepcopyinguniqueptr.h000066400000000000000000000057211476450434500266520ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef DEEPCOPYINGUNIQUEPTR_H_ #define DEEPCOPYINGUNIQUEPTR_H_ #include template > class DeepCopyingUniquePtr : public std::unique_ptr { // determine at compiletime whether T::clone() exists... struct HasCloneMethod { template struct SFINAE {}; template static char Test(SFINAE *); template static int Test(...); static bool const value = sizeof(Test(0)) == sizeof(char); }; struct HasMoveCloneMethod { template struct SFINAE {}; template static char Test(SFINAE *); template static int Test(...); static bool const value = sizeof(Test(0)) == sizeof(char); }; static_assert(std::is_copy_constructible::value, "CopyUniquePtr's contents must have copy constructor"); // inherit constructors using std::unique_ptr::unique_ptr; public: inline DeepCopyingUniquePtr(DeepCopyingUniquePtr const &other) : std::unique_ptr() { if constexpr (HasCloneMethod::value) this->reset(other ? other->clone() : nullptr); else this->reset(other ? new T(*(other.get())) : nullptr); } inline DeepCopyingUniquePtr &operator=(DeepCopyingUniquePtr const &other) { if (this != &other) [[likely]] { if constexpr (HasCloneMethod::value) this->reset(other ? other->clone() : nullptr); else this->reset(other ? new T(std::move(*(other.get()))) : nullptr); } return *this; } inline DeepCopyingUniquePtr(DeepCopyingUniquePtr &&other) : std::unique_ptr() { if constexpr (HasMoveCloneMethod::value) this->reset(other ? other->move_clone() : nullptr); else this->reset(other ? new T(std::move(*(other.get()))) : nullptr); } inline DeepCopyingUniquePtr &operator=(DeepCopyingUniquePtr &&other) { if (this != &other) [[likely]] { if constexpr (HasMoveCloneMethod::value) this->reset(other ? other->move_clone() : nullptr); else this->reset(other ? new T(std::move(*(other.get()))) : nullptr); } return *this; } }; #endif signalbackup-tools-20250313-1/desktopattachmentreader/000077500000000000000000000000001476450434500226335ustar00rootroot00000000000000signalbackup-tools-20250313-1/desktopattachmentreader/desktopattachmentreader.h000066400000000000000000000064031476450434500277140ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef DESKTOPATTACHMENTREADER_H_ #define DESKTOPATTACHMENTREADER_H_ #include "../baseattachmentreader/baseattachmentreader.h" #include "../framewithattachment/framewithattachment.h" #include "../base64/base64.h" #include "../rawfileattachmentreader/rawfileattachmentreader.h" #include #include #include /* version < 2 (or unset): attachment data is just in the file version 2: File is encrypted: [=16 bytes iv=][=AES-256-CBC encrypted file, padded with zeros to some binned size, padded with AES-CBC padding to next nearest multiple of 16=][=32 bytes HMAC=] the passed in key ('localKey') is base64 encoded concatenation of 32 bytes AES key + 32 bytes MAC key */ class DesktopAttachmentReader : public AttachmentReader { int d_version; std::string d_path; std::string d_key; uint64_t d_size; public: inline explicit DesktopAttachmentReader(std::string const &path); inline DesktopAttachmentReader(int version, std::string const &path, std::string const &key, uint64_t size); inline DesktopAttachmentReader(DesktopAttachmentReader const &other) = default; inline DesktopAttachmentReader(DesktopAttachmentReader &&other) = default; inline DesktopAttachmentReader &operator=(DesktopAttachmentReader const &other) = default; inline DesktopAttachmentReader &operator=(DesktopAttachmentReader &&other) = default; inline virtual ~DesktopAttachmentReader() override = default; inline virtual int getAttachment(FrameWithAttachment *frame, bool verbose) override; int getAttachmentData(unsigned char **data, bool verbose); //decryptdata private: int getEncryptedAttachment(FrameWithAttachment *frame, bool verbose); inline int getRawAttachment(FrameWithAttachment *frame, bool verbose); }; inline DesktopAttachmentReader::DesktopAttachmentReader(std::string const &path) : DesktopAttachmentReader(1, path, std::string(), 0) {} inline DesktopAttachmentReader::DesktopAttachmentReader(int version, std::string const &path, std::string const &key, uint64_t size) : d_version(version), d_path(path), d_key(key), d_size(size) {} inline int DesktopAttachmentReader::getAttachment(FrameWithAttachment *frame, bool verbose) { if (d_version >= 2) [[likely]] return getEncryptedAttachment(frame, verbose); else return getRawAttachment(frame, verbose); } inline int DesktopAttachmentReader::getRawAttachment(FrameWithAttachment *frame, bool verbose) { RawFileAttachmentReader raw(d_path); return raw.getAttachment(frame, verbose); } #endif signalbackup-tools-20250313-1/desktopattachmentreader/getattachmentdata.cc000066400000000000000000000136511476450434500266320ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "desktopattachmentreader.h" #include "../common_filesystem.h" int DesktopAttachmentReader::getAttachmentData(unsigned char **rawdata, bool verbose [[maybe_unused]]) { // set AES+MAC key auto [tmpdata, key_data_length] = Base64::base64StringToBytes(d_key); std::unique_ptr key_data(tmpdata); //uint64_t aeskey_length = 32; unsigned char *aeskey = key_data.get(); uint64_t mackey_length = 32; unsigned char *mackey = key_data.get() + 32; // open file std::ifstream file(std::filesystem::path(d_path), std::ios_base::in | std::ios_base::binary); if (!file.is_open()) { Logger::error("Failed to open file '", d_path, "'"); return 1; } // set iv/data length. int64_t iv_length = 16; //file.seekg(0, std::ios_base::end); //int64_t data_length = file.tellg() - static_cast(iv_length + mackey_length); //file.seekg(0, std::ios_base::beg); int64_t data_length = bepaald::fileSize(d_path) - static_cast(iv_length + mackey_length); // set iv std::unique_ptr iv(new unsigned char[iv_length]); if (!file.read(reinterpret_cast(iv.get()), iv_length) || file.gcount() != iv_length) { Logger::error("Failed to read iv"); return 1; } // set data to decrypt std::unique_ptr data(new unsigned char[data_length]); if (!file.read(reinterpret_cast(data.get()), data_length) || file.gcount() != data_length) { Logger::error("Failed to read in file data"); return 1; } // set theirMAC int64_t theirmac_length = 32; std::unique_ptr theirmac(new unsigned char[32]); if (!file.read(reinterpret_cast(theirmac.get()), theirmac_length) || file.gcount() != theirmac_length) { Logger::error("Failed to read theirmac"); return 1; } // calculate MAC evp_md_st const *digest = EVP_sha256(); #if OPENSSL_VERSION_NUMBER >= 0x30000000L char digestname[] = "SHA256"; std::unique_ptr mac(EVP_MAC_fetch(nullptr, "hmac", nullptr), &::EVP_MAC_free); std::unique_ptr hctx(EVP_MAC_CTX_new(mac.get()), &::EVP_MAC_CTX_free); OSSL_PARAM params[] = {OSSL_PARAM_construct_utf8_string("digest", digestname, 0), OSSL_PARAM_construct_end()}; if (EVP_MAC_init(hctx.get(), mackey, mackey_length, params) != 1) { Logger::error("Failed to initialize HMAC context"); return false; } std::unique_ptr calculatedmac(new unsigned char[EVP_MD_size(digest)]); if (EVP_MAC_update(hctx.get(), iv.get(), iv_length) != 1 || EVP_MAC_update(hctx.get(), data.get(), data_length) != 1 || EVP_MAC_final(hctx.get(), calculatedmac.get(), nullptr, EVP_MD_size(digest)) != 1) { Logger::error("Failed to update/finalize hmac"); return false; } #else std::unique_ptr hctx(HMAC_CTX_new(), &::HMAC_CTX_free); if (HMAC_Init_ex(hctx.get(), mackey, mackey_length, digest, nullptr) != 1) { Logger::error("Failed to initialize HMAC context"); return false; } std::unique_ptr calculatedmac(new unsigned char[32]); unsigned int finalsize = EVP_MD_size(digest); if (HMAC_Update(hctx.get(), iv.get(), iv_length) != 1 || HMAC_Update(hctx.get(), data.get(), data_length) != 1 || HMAC_Final(hctx.get(), calculatedmac.get(), &finalsize) != 1) { Logger::error("Failed to update/finalize hmac"); return false; } #endif if (std::memcmp(calculatedmac.get(), theirmac.get(), theirmac_length) != 0) { Logger::error("MAC failed! (theirMAC: ", bepaald::bytesToString(theirmac.get(), theirmac_length), " ourMAC: ", bepaald::bytesToString(calculatedmac.get(), theirmac_length)); return -1; } // DECRYPT DATA: // init decryption context std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); if (!ctx) [[unlikely]] { Logger::error("Failed to create decryption context"); return 1; } // init decrypt if (!EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_cbc(), nullptr, aeskey, iv.get())) [[unlikely]] { Logger::error("Failed to initialize decryption operation"); return 1; } // decrypt update int out_len = 0; int output_length = data_length; std::unique_ptr output(new unsigned char[output_length]); if (EVP_DecryptUpdate(ctx.get(), output.get(), &out_len, data.get(), output_length) != 1) [[unlikely]] { Logger::error("Failed to update decryption"); return 1; } // decrypt final int tail_len = 0; if (EVP_DecryptFinal_ex(ctx.get(), output.get() + out_len, &tail_len) != 1) [[unlikely]] { Logger::error("Failed to finalize decryption"); return 1; } out_len += tail_len; //std::cout << out_len << std::endl; //std::cout << "Start of decrypted data: " << bepaald::bytesToHexString(output.get(), 64) << std::endl; *rawdata = output.release(); return 0; } int DesktopAttachmentReader::getEncryptedAttachment(FrameWithAttachment *frame, bool verbose) { unsigned char *data = nullptr; int ret = getAttachmentData(&data, verbose); if (ret == 0) [[likely]] frame->setAttachmentDataBacked(data, d_size); return ret; } signalbackup-tools-20250313-1/desktopdatabase/000077500000000000000000000000001476450434500210645ustar00rootroot00000000000000signalbackup-tools-20250313-1/desktopdatabase/decryptkey_mac_linux.cc000066400000000000000000000176161476450434500256300ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #if !defined (_WIN32) && !defined(__MINGW64__) #include "desktopdatabase.ih" #include "../common_bytes.h" #include #include #include #include std::string DesktopDatabase::decryptKey_linux_mac(std::string const &secret, std::string const &encryptedkeystr, bool last) const { std::string decryptedkey; //// 1. derive decryption key from secret: // set the salt uint64_t const salt_length = 9; unsigned char salt[salt_length] = {'s', 'a', 'l', 't', 'y', 's', 'a', 'l', 't'}; // perform the KDF uint64_t key_length = 16; std::unique_ptr key(new unsigned char[key_length]); #if defined (__APPLE__) && defined (__MACH__) int iterations = 1003; #else // linux int iterations = 1; #endif if (PKCS5_PBKDF2_HMAC_SHA1(reinterpret_cast(secret.data()), secret.size(), salt, salt_length, iterations, key_length, key.get()) != 1) { Logger::error("Error deriving key from password"); return decryptedkey; } //// 2. decrypt keydata using key(1) // set encrypted key data uint64_t data_length = encryptedkeystr.size() / 2; std::unique_ptr data(new unsigned char[data_length]); bepaald::hexStringToBytes(encryptedkeystr, data.get(), data_length); // check header int const version_header_length = 3; #if defined (__APPLE__) && defined (__MACH__) unsigned char version_header[version_header_length] = {'v', '1', '0'}; #else // linux unsigned char version_header[version_header_length] = {'v', '1', '1'}; #endif if (std::memcmp(data.get(), version_header, 3) != 0) [[unlikely]] Logger::warning("Unexpected header value: ", bepaald::bytesToHexString(data.get(), 3)); // set iv uint64_t const iv_length = 16; unsigned char iv[iv_length] = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '}; // 16 spaces // init cipher and context std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); if (!ctx) { Logger::error("Failed to create decryption context"); return decryptedkey; } // init decrypt if (!EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_cbc(), nullptr, key.get(), iv)) [[unlikely]] { Logger::error("Failed to initialize decryption operation"); return decryptedkey; } // disable padding EVP_CIPHER_CTX_set_padding(ctx.get(), 0); // decrypt update int out_len = 0; int output_length = data_length - version_header_length; std::unique_ptr output(new unsigned char[output_length]); if (EVP_DecryptUpdate(ctx.get(), output.get(), &out_len, data.get() + version_header_length, output_length) != 1) { Logger::error("Decrypt update"); return decryptedkey; } // decrypt final int tail_len = 0; int err = 0; if ((err = EVP_DecryptFinal_ex(ctx.get(), output.get() + out_len, &tail_len)) != 1) { Logger::error("Finalizing decryption: ", err); return decryptedkey; } out_len += tail_len; // all input is always padded to the _next_ multiple of 16 (64 in this case to 80) // the padding bytes are always the size of the padding (see below) int padding = output_length % 16; int realsize = output_length - (padding ? padding : 16); for (int i = 0; i < (padding ? padding : 16); ++i) if (static_cast(output[realsize + i]) != (padding ? padding : 16)) { if (last) Logger::error("Decryption appears to have failed (padding bytes have unexpected value). No more secrets to try."); else Logger::warning("Decryption appears to have failed (padding bytes have unexpected value), attempting next secret..."); return decryptedkey; } decryptedkey = bepaald::bytesToPrintableString(output.get(), realsize); if (decryptedkey.find_first_not_of("abcdefghijklmnopqrstuvwxyz0123456789") != std::string::npos) { if (last) Logger::error("Failed to decrypt key correctly. No more secrets to try."); else Logger::warning("Failed to decrypt key correctly, attempting next secret..."); decryptedkey.clear(); //return empty string... } return decryptedkey; } #endif /* (spaces added in output before the padding) [~] $ echo -ne "exactly 32 bytes exactly 32 byte" > input.txt ; openssl enc -aes-128-cbc -nosalt -e -in input.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' > output.txt ; openssl enc -nopad -aes-128-cbc -nosalt -d -in output.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' | xxd -ps -g 1 -c 64 65786163746c792033322062797465732065786163746c792033322062797465 10101010101010101010101010101010 [~] $ echo -ne "exactly 33 bytes exactly 33 bytes" > input.txt ; openssl enc -aes-128-cbc -nosalt -e -in input.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' > output.txt ; openssl enc -nopad -aes-128-cbc -nosalt -d -in output.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' | xxd -ps -g 1 -c 64 65786163746c792033332062797465732065786163746c79203333206279746573 0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f [~] $ echo -ne "exactly 34 bytes exactly 34 bytes " > input.txt ; openssl enc -aes-128-cbc -nosalt -e -in input.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' > output.txt ; openssl enc -nopad -aes-128-cbc -nosalt -d -in output.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' | xxd -ps -g 1 -c 64 65786163746c792033342062797465732065786163746c7920333420627974657320 0e0e0e0e0e0e0e0e0e0e0e0e0e0e [~] $ echo -ne "exactly 35 bytes exactly 35 bytes e" > input.txt ; openssl enc -aes-128-cbc -nosalt -e -in input.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' > output.txt ; openssl enc -nopad -aes-128-cbc -nosalt -d -in output.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' | xxd -ps -g 1 -c 64 65786163746c792033352062797465732065786163746c792033352062797465732065 0d0d0d0d0d0d0d0d0d0d0d0d0d [...] [~] $ echo -ne "exactly 46 bytes exactly 46 bytes exactly 46 b" > input.txt ; openssl enc -aes-128-cbc -nosalt -e -in input.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' > output.txt ; openssl enc -nopad -aes-128-cbc -nosalt -d -in output.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' | xxd -ps -g 1 -c 64 65786163746c792034362062797465732065786163746c792034362062797465732065786163746c792034362062 0202 [~] $ echo -ne "exactly 47 bytes exactly 47 bytes exactly 47 by" > input.txt ; openssl enc -aes-128-cbc -nosalt -e -in input.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' > output.txt ; openssl enc -nopad -aes-128-cbc -nosalt -d -in output.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' | xxd -ps -g 1 -c 64 65786163746c792034372062797465732065786163746c792034372062797465732065786163746c79203437206279 01 [~] $ echo -ne "exactly 48 bytes exactly 48 bytes exactly 48 byt" > input.txt ; openssl enc -aes-128-cbc -nosalt -e -in input.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' > output.txt ; openssl enc -nopad -aes-128-cbc -nosalt -d -in output.txt -K '2222233333232323' -iv '5a04ec902686fb05a6b7a338b6e07760' | xxd -ps -g 1 -c 64 65786163746c792034382062797465732065786163746c792034382062797465732065786163746c7920343820627974 10101010101010101010101010101010 */ signalbackup-tools-20250313-1/desktopdatabase/desktopdatabase.h000066400000000000000000000171411476450434500243770ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef DESKTOPDATABASE_H_ #define DESKTOPDATABASE_H_ #include #include #include #include "../logger/logger.h" #include "../common_filesystem.h" #include "../memsqlitedb/memsqlitedb.h" #include "../sqlcipherdecryptor/sqlcipherdecryptor.h" class DesktopDatabase { std::unique_ptr d_cipherdb; std::unique_ptr d_rawdb; MemSqliteDB d_database; std::string d_configdir; std::string d_databasedir; std::string d_hexkey; bool d_ok; bool d_verbose; bool d_dbus_verbose; bool d_ignorewal; long long int d_cipherversion; bool d_truncate; bool d_showkey; public: inline DesktopDatabase(std::string const &hexkey, bool verbose, bool ignorewal, long long int cipherversion, bool truncate, bool showkey, bool dbus_verbose); inline DesktopDatabase(std::string const &configdir, std::string const &databasedir, std::string const &rawdb, std::string const &hexkey, bool verbose, bool ignorewal, long long int cipherversion, bool truncate, bool showkey, bool dbus_verbose); DesktopDatabase(DesktopDatabase const &other) = delete; DesktopDatabase(DesktopDatabase &&other) = delete; DesktopDatabase &operator=(DesktopDatabase const &other) = delete; DesktopDatabase &operator=(DesktopDatabase &&other) = delete; inline bool ok() const; inline bool dumpDb(std::string const &file, bool overwrite) const; inline std::string const &getConfigDir() const; inline std::string const &getDatabaseDir() const; inline void runQuery(std::string const &q, std::string const &mode = std::string()) const; private: bool init(std::string const &rawdb = std::string()); inline std::pair getDesktopDir() const; std::string readEncryptedKey() const; bool getKey(); bool getKeyFromEncrypted(); #if defined(_WIN32) || defined(__MINGW64__) bool getKeyFromEncrypted_win(); #else bool getKeyFromEncrypted_mac_linux(); std::string decryptKey_linux_mac(std::string const &secret, std::string const &encryptedkeystr, bool last = true) const; #endif #if defined(__APPLE__) && defined(__MACH__) // if apple... void getSecrets_mac(std::set *secrets) const; #elif !defined(_WIN32) && !defined(__MINGW64__) // not apple, but also not windows void getSecrets_linux_SecretService(std::set *secrets) const; void getSecrets_linux_Kwallet(int version, std::set *secrets) const; #endif friend class SignalBackup; friend class DummyBackup; }; inline DesktopDatabase::DesktopDatabase(std::string const &hexkey, bool verbose, bool ignorewal, long long int cipherversion, bool truncate, bool showkey, bool dbus_verbose) : DesktopDatabase(std::string(), std::string(), std::string(), hexkey, verbose, ignorewal, cipherversion, truncate, showkey, dbus_verbose) {} inline DesktopDatabase::DesktopDatabase(std::string const &configdir, std::string const &databasedir, std::string const &rawdb, std::string const &hexkey, bool verbose, bool ignorewal, long long int cipherversion, bool truncate, bool showkey, bool dbus_verbose) : d_configdir(configdir), d_databasedir(databasedir), d_hexkey(hexkey), d_ok(false), d_verbose(verbose), d_dbus_verbose(dbus_verbose), d_ignorewal(ignorewal), d_cipherversion(cipherversion), d_truncate(truncate), d_showkey(showkey) { d_ok = init(rawdb); } inline bool DesktopDatabase::ok() const { return d_ok; } inline std::pair DesktopDatabase::getDesktopDir() const { #if defined(_WIN32) || defined(__MINGW64__) // Windows: concatenate HOMEDRIVE+HOMEPATH // probably only works on windows 7 and newer? (if at all) const char *homedrive_cs = std::getenv("HOMEDRIVE"); const char *homepath_cs = std::getenv("HOMEPATH"); if (homedrive_cs == nullptr || homepath_cs == nullptr) return {std::string(), std::string()}; std::string home = std::string(homedrive_cs) + std::string(homepath_cs); if (home.empty()) return {std::string(), std::string()}; if (bepaald::isDir(home + "/AppData/Roaming/Signal")) return {home + "/AppData/Roaming/Signal", home + "/AppData/Roaming/Signal"}; else if (bepaald::isDir(home + "/AppData/Roaming/Signal Beta")) return {home + "/AppData/Roaming/Signal Beta", home + "/AppData/Roaming/Signal Beta"}; else return {std::string(), std::string()}; #else char const *homedir_cs = std::getenv("HOME"); if (homedir_cs == nullptr) return {std::string(), std::string()}; std::string homedir(homedir_cs); if (homedir.empty()) return {std::string(), std::string()}; #if defined(__APPLE__) && defined(__MACH__) if (bepaald::isDir(homedir + "/Library/Application Support/Signal")) return {homedir + "/Library/Application Support/Signal", homedir + "/Library/Application Support/Signal"}; if (bepaald::isDir(homedir + "/Library/Application Support/Signal Beta")) return {homedir + "/Library/Application Support/Signal Beta", homedir + "/Library/Application Support/Signal Beta"}; else return {std::string(), std::string()}; #else // !windows && !mac if (bepaald::isDir(homedir + "/.config/Signal")) return {homedir + "/.config/Signal", homedir + "/.config/Signal"}; if (bepaald::isDir(homedir + "/.config/Signal Beta")) return {homedir + "/.config/Signal Beta", homedir + "/.config/Signal Beta"}; else return {std::string(), std::string()}; #endif #endif } inline bool DesktopDatabase::dumpDb(std::string const &file, bool overwrite) const { if (bepaald::fileOrDirExists(file) && !overwrite) { Logger::message("File '", file, "' exists, use `--overwrite` to overwrite."); return false; } if (!d_database.saveToFile(file)) { Logger::error("Failed to save Signal Desktop sql database to file '", file, "'"); return false; } return true; } inline std::string const &DesktopDatabase::getConfigDir() const { return d_configdir; } inline std::string const &DesktopDatabase::getDatabaseDir() const { return d_databasedir; } inline void DesktopDatabase::runQuery(std::string const &q, std::string const &mode) const { Logger::message(" * Executing query: ", q); SqliteDB::QueryResults res; if (!d_database.exec(q, &res)) return; std::string q_comm(q, 0, STRLEN("DELETE")); // delete, insert and update are same length... std::for_each(q_comm.begin(), q_comm.end(), [] (char &ch) { ch = std::toupper(ch); }); if (q_comm == "DELETE" || q_comm == "INSERT" || q_comm == "UPDATE") { Logger::message("Modified ", d_database.changed(), " rows"); if (res.rows() == 0 && res.columns() == 0) return; } if (mode == "pretty") res.prettyPrint(d_truncate); else if (mode == "line") res.printLineMode(); else if (mode == "single") res.printSingleLine(); else res.print(); } #endif signalbackup-tools-20250313-1/desktopdatabase/desktopdatabase.ih000066400000000000000000000014701476450434500245460ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "desktopdatabase.h" #include #include #include signalbackup-tools-20250313-1/desktopdatabase/getkey.cc000066400000000000000000000040351476450434500226650ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "desktopdatabase.ih" bool DesktopDatabase::getKey() { if (getKeyFromEncrypted()) { if (d_verbose) [[unlikely]] Logger::message("Initialized from encryptedkey"); return true; } // read key from config.json std::fstream config(d_configdir + "/config.json", std::ios_base::in | std::ios_base::binary); if (!config.is_open()) { Logger::error("Failed to open input: ", d_configdir, "/config.json"); return false; } std::string line; //std::regex keyregex("PRAGMA KEY = \"x\\\\'([a-zA-Z0-9]{64})\\\\'\";"); /* $ cat ~/.config/Signal/config.json | pcregrep -o1 "^\s*\"key\":\s*\"([a-z0-9]{64})\"$" aac2f422c149db6180b1a76df1ee462101c11d2d2347044ef055a956dfcbfa98 */ std::regex keyregex("^\\s*\"key\":\\s*\"([a-zA-Z0-9]{64})\",?$"); std::smatch m; bool found = false; while (std::getline(config, line)) { //std::cout << "Line: " << line << std::endl; if (std::regex_match(line, m, keyregex)) if (m.size() == 2) // m[0] is full match, m[1] is first submatch (which we want) { found = true; break; } } if (!found) { Logger::error("Failed to read key from config.json"); return false; } d_hexkey = m[1].str(); //std::cout << bepaald::bytesToHexString(d_key, d_keysize) << std::endl; return true; } signalbackup-tools-20250313-1/desktopdatabase/getkeyfromencrypted.cc000066400000000000000000000016731476450434500254740ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "desktopdatabase.ih" bool DesktopDatabase::getKeyFromEncrypted() { #if defined(_WIN32) || defined(__MINGW64__) return getKeyFromEncrypted_win(); #else return getKeyFromEncrypted_mac_linux(); #endif } signalbackup-tools-20250313-1/desktopdatabase/getkeyfromencrypted_mac_linux.cc000066400000000000000000000037121476450434500275270ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #if !defined(__WIN32) && !defined(__MINGW64__) #include "desktopdatabase.ih" bool DesktopDatabase::getKeyFromEncrypted_mac_linux() { // 1. get the encrypted key from config.json std::string keystr = readEncryptedKey(); if (keystr.empty()) return false; // 2. get the secrets std::set secrets; auto tryDecrypt = [&]() { for (auto s = secrets.begin(); s != secrets.end(); ++s) { d_hexkey = decryptKey_linux_mac(*s, keystr, s == std::prev(secrets.end())); if (!d_hexkey.empty()) return true; } return false; }; #if defined(__APPLE__) && defined(__MACH__) getSecrets_mac(&secrets); if (tryDecrypt()) return true; #else getSecrets_linux_SecretService(&secrets); if (tryDecrypt()) return true; getSecrets_linux_Kwallet(6, &secrets); // nothing from libsecret, try kwallet6... if (tryDecrypt()) return true; getSecrets_linux_Kwallet(5, &secrets); // nothing from kwallet6, try kwallet5... if (tryDecrypt()) return true; #endif if (secrets.empty()) { Logger::error("Failed to get any secrets"); return false; } if (d_hexkey.empty()) { Logger::error("Failed to decrypt valid key. :("); return false; } return false; } #endif signalbackup-tools-20250313-1/desktopdatabase/getkeyfromencrypted_win.cc000066400000000000000000000152201476450434500263420ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #if defined(_WIN32) || defined(__MINGW64__) #include "desktopdatabase.ih" #include "../base64/base64.h" #include #include bool DesktopDatabase::getKeyFromEncrypted_win() { // 1. get the encrypted key from config.json std::string keystr = readEncryptedKey(); if (keystr.empty()) return false; unsigned long encryptedkey_data_length = keystr.size() / 2; std::unique_ptr encryptedkey_data(new unsigned char[encryptedkey_data_length]); bepaald::hexStringToBytes(keystr, encryptedkey_data.get(), encryptedkey_data_length); // 2. get the key to decrypt the encrypted key //***** 2a. get the base64 encoded encrypted key to decrypt the encrypted key ******// std::fstream localstate(d_configdir + "/Local State", std::ios_base::in | std::ios_base::binary); if (!localstate.is_open()) { Logger::error("Failed to open input: ", d_configdir, "/Local State"); return false; } std::string line; std::regex keyregex(".*\"encrypted_key\":\\s*\"([^\"]*)\".*"); std::smatch m; bool found = false; while (std::getline(localstate, line)) { //std::cout << "Line: " << line << std::endl; if (std::regex_match(line, m, keyregex)) if (m.size() == 2) // m[0] is full match, m[1] is first submatch (which we want) { found = true; break; } } if (!found) { Logger::error("Failed to read key from Local State"); return false; } std::string encrypted_encryptedkey_keyb64 = m[1].str(); //std::cout << encrypted_encryptedkey_keyb64 << std::endl; //***** 2b. decrypt it ******// std::pair encrypted_encryptedkey_key = Base64::base64StringToBytes(encrypted_encryptedkey_keyb64); //std::cout << "enc Key size: " << encrypted_encryptedkey_key.second << std::endl; //std::cout << "enc Key: " << bepaald::bytesToHexString(encrypted_encryptedkey_key.first, encrypted_encryptedkey_key.second) << std::endl; // the encrypted key starts with 'D' 'P' 'A' 'P' 'I' {0x44, 0x50, 0x41, 0x50, 0x41}, skip this... DATA_BLOB encrypted_encryptedkey_key_blob{static_cast(encrypted_encryptedkey_key.second - STRLEN("DPAPI")), encrypted_encryptedkey_key.first + STRLEN("DPAPI")}; DATA_BLOB encryptedkey_key_blob; if (!CryptUnprotectData(&encrypted_encryptedkey_key_blob, nullptr, nullptr, nullptr, nullptr, 0, &encryptedkey_key_blob)) [[unlikely]] { Logger::error("Failed to decrypt key (1)"); bepaald::destroyPtr(&encrypted_encryptedkey_key.first, &encrypted_encryptedkey_key.second); return false; } bepaald::destroyPtr(&encrypted_encryptedkey_key.first, &encrypted_encryptedkey_key.second); uint64_t encryptedkey_key_length = encryptedkey_key_blob.cbData; std::unique_ptr encryptedkey_key(new unsigned char[encryptedkey_key_length]); std::memcpy(encryptedkey_key.get(), encryptedkey_key_blob.pbData, encryptedkey_key_length); LocalFree(encryptedkey_key_blob.pbData); //std::cout << "Decrypted key to decrypt encrypted key: " << bepaald::bytesToHexString(encryptedkey_key.get(), encryptedkey_key_length) << std::endl << std::endl; // 3. Now decrypt the encrypted_key using the decrypted key from local state // the encrypted key (from step 1) is made up of // - a 3 byte header ('v', '1', '0') // - a 12 byte nonce // - 64 bytes of encrypted data // - 16 bytes mac uint64_t constexpr header_length = 3; unsigned char *header = encryptedkey_data.get(); uint64_t constexpr nonce_length = 12; unsigned char *nonce = encryptedkey_data.get() + header_length; uint64_t constexpr mac_length = 16; unsigned char *mac = encryptedkey_data.get() + (encryptedkey_data_length - mac_length); uint64_t encdata_length = encryptedkey_data_length - mac_length - header_length - nonce_length; unsigned char *encdata = nonce + nonce_length; //std::cout << bepaald::bytesToHexString(header, header_length) << std::endl; //std::cout << bepaald::bytesToHexString(nonce, nonce_length) << std::endl; //std::cout << bepaald::bytesToHexString(encdata, encdata_length) << std::endl; //std::cout << bepaald::bytesToHexString(mac, mac_length) << std::endl; unsigned char const v10header[3] = {'v', '1', '0'}; if (std::memcmp(header, v10header, header_length) != 0) [[unlikely]] Logger::warning("Unexpected header value: ", bepaald::bytesToHexString(header, header_length)); // Create and initialize the decryption context & cipher std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); std::unique_ptr cipher(EVP_CIPHER_fetch(NULL, "AES-256-GCM", NULL), &::EVP_CIPHER_free); if (!ctx || !cipher) { Logger::error("Failed to create decryption context or cipher"); return false; } // set parameters (to set and check MAC) OSSL_PARAM params[2]; params[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG, mac, mac_length); params[1] = OSSL_PARAM_construct_end(); if (!EVP_DecryptInit_ex2(ctx.get(), cipher.get(), encryptedkey_key.get(), nonce, params)) [[unlikely]] { Logger::error("Failed to initialize decryption operation"); return false; } int len = 0; uint64_t key_hexstr_length = 64; std::unique_ptr key_hexstr(new unsigned char[key_hexstr_length]); if (!EVP_DecryptUpdate(ctx.get(), key_hexstr.get(), &len, encdata, encdata_length)) [[unlikely]] { Logger::error("Failed to decrypt key (2)"); return false; } if (EVP_DecryptFinal_ex(ctx.get(), key_hexstr.get() + len, &len) > 0) [[likely]] { // the decrypted data is not the actual key, but the key as hex in ascii for some reason... d_hexkey = std::string(reinterpret_cast(key_hexstr.get()), key_hexstr_length); //std::cout << "KEY !! : " << d_hexkey << std::endl; return true; } else [[unlikely]] { Logger::error("Failed to finalize decryption (possibly MAC failed)"); return false; } } #endif signalbackup-tools-20250313-1/desktopdatabase/getsecrets_linux_kwallet.cc000066400000000000000000000132061476450434500265070ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #if !defined(_WIN32) && !defined(__MINGW64__) && (!defined(__APPLE__) || !defined(__MACH__)) #if defined WITHOUT_DBUS #include "desktopdatabase.ih" void DesktopDatabase::getSecrets_linux_Kwallet(int /*version*/, std::set */*secrets*/) const { Logger::error ("Found encrypted sqlcipher key in config file. To decrypt this key"); Logger::error_indent("a secret must be retrieved from your keyring through dbus, but"); Logger::error_indent("this program was explicitly compiled without dbus."); return; } #else #include "desktopdatabase.ih" #include "../dbuscon/dbuscon.h" void DesktopDatabase::getSecrets_linux_Kwallet(int version, std::set *secrets) const { if (d_dbus_verbose) [[unlikely]] Logger::message("Getting secret from Kwallet through DBUS (version ", version, ")"); if (!secrets) return; DBusCon dbuscon(d_dbus_verbose); if (!dbuscon.ok()) { Logger::error("Failed to connect to dbus session"); return; } std::string destination("org.kde.kwalletd" + std::to_string(version)); std::string path("/modules/kwalletd" + std::to_string(version)); std::string interface("org.kde.KWallet"); /* GET WALLET */ if (d_dbus_verbose) Logger::message("[networkWallet]"); dbuscon.callMethod(destination.c_str(), path.c_str(), interface.c_str(), "networkWallet"); std::string walletname = dbuscon.get("s", 0); if (walletname.empty()) { Logger::error("Failed to get wallet name"); return; } if (d_dbus_verbose) Logger::message(" *** Wallet name: ", walletname); // ON KDE THE 'open' METHOD SEEMS TO BLOCK FOR PASSWORD PROMPT BY ITSELF... // /* Register to wait for opening wallet */ // if (!matchSignal("member='walletOpened'")) // Logger::message("WARN: Failed to register for signal"); /* OPEN WALLET */ if (d_dbus_verbose) Logger::message("[open]"); dbuscon.callMethod(destination.c_str(), path.c_str(), interface.c_str(), "open", {walletname, int64_t{0}, "signalbackup-tools"}); int32_t handle = dbuscon.get("i", 0 - 1); if (handle < 0) { Logger::error("Failed to open wallet"); return; } if (d_dbus_verbose) Logger::message(" *** Handle: ", handle); /* GET FOLDERS */ if (d_dbus_verbose) Logger::message("[folderList]"); dbuscon.callMethod(destination.c_str(), path.c_str(), interface.c_str(), "folderList", {handle, "signalbackup-tools"}); std::vector folders = dbuscon.get>("as", 0); if (folders.empty()) { Logger::error("Failed to get any folders from wallet"); return; } for (auto const &folder : folders) { #if __cpp_lib_string_contains >= 202011L if ((folder.contains("Chrome") || folder.contains("Chromium")) && (folder.contains("Safe Storage") || folder.contains("Keys"))) #else if ((folder.find("Chrome") != std::string::npos || folder.find("Chromium") != std::string::npos) && (folder.find("Safe Storage") != std::string::npos || folder.find("Keys") != std::string::npos)) #endif { /* GET PASSWORD */ if (d_dbus_verbose) Logger::message("[passwordList]"); dbuscon.callMethod(destination.c_str(), path.c_str(), interface.c_str(), "passwordList", {handle, folder, "signalbackup-tools"}); /* The password list returns a dict (dicts are always (in) an array as per dbus spec) the signature is a{sv} -> the v in our case is a string again, pretty much a map, The value we want seems to have the key "Chrom[e|ium] Safe Storage"... */ std::map passwordmap = dbuscon.get>("a{sv}", 0); if (passwordmap.empty()) { Logger::error("Failed to get password map"); return; } for (auto const &e : passwordmap) if (e.first == "Chromium Safe Storage" || e.first == "Chrome Safe Storage") { if (d_dbus_verbose) [[unlikely]] Logger::message(" *** SECRET: ", e.second); secrets->insert(e.second); } } } /* CLOSE WALLET */ if (d_dbus_verbose) Logger::message("[close (wallet)]"); dbuscon.callMethod(destination.c_str(), path.c_str(), interface.c_str(), "close", {walletname, false}); /* CLOSE SESSION */ if (d_dbus_verbose) Logger::message("[close (session)]"); dbuscon.callMethod(destination.c_str(), path.c_str(), interface.c_str(), "close", {handle, false, "signalbackup-tools"}); return; } #endif #endif signalbackup-tools-20250313-1/desktopdatabase/getsecrets_linux_secretservice.cc000066400000000000000000000220631476450434500277130ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #if !defined(_WIN32) && !defined(__MINGW64__) && (!defined(__APPLE__) || !defined(__MACH__)) #if defined WITHOUT_DBUS #include "desktopdatabase.ih" void DesktopDatabase::getSecrets_linux_SecretService(std::set */*secrets*/) const { Logger::error ("Found encrypted sqlcipher key in config file. To decrypt this key"); Logger::error_indent("a secret must be retrieved from your keyring through dbus, but"); Logger::error_indent("this program was explicitly compiled without dbus."); return; } #else #include "desktopdatabase.ih" #include "../dbuscon/dbuscon.h" void DesktopDatabase::getSecrets_linux_SecretService(std::set *secrets) const { if (d_dbus_verbose) [[unlikely]] Logger::message("Getting secret from SecretService through DBUS"); if (!secrets) return; DBusCon dbuscon(d_dbus_verbose); if (!dbuscon.ok()) { Logger::error("Failed to connect to dbus session"); return; } /* OPEN SESSION */ if (d_dbus_verbose) Logger::message("[OpenSession]"); dbuscon.callMethod("org.freedesktop.secrets", "/org/freedesktop/secrets", "org.freedesktop.Secret.Service", "OpenSession", {"plain", DBusVariant{""}}); std::string session_objectpath = dbuscon.get("vo", 1); if (session_objectpath.empty()) { Logger::error("Failed to get session"); return; } if (d_dbus_verbose) Logger::message(" *** Session: ", session_objectpath); // if constexpr (false) // { // /* SEARCHITEMS */ // // note searching is of no use on KDE, the secret does not seem to have any attributes set. // // so lets just get all items and inspect them // Logger::message("[SearchItems(label:Chromium Keys/Chromium Safe Storage)]"); // dbuscon.callMethod("org.freedesktop.secrets", // "/org/freedesktop/secrets", // "org.freedesktop.Secret.Service", // "SearchItems", // {DBusDict{{"org.freedesktop.Secret.Collection.Label", "Chromium Keys/Chromium Safe Storage"}, // {"Label", "Chromium Keys/Chromium Safe Storage"}}}); // } // if constexpr (false) // { // /* GET DEFAULT COLLECTION */ // // not necessary we can address the default directly (without knowing what it points to), through // // the aliases/default path... // Logger::message("[ReadAlias(default)]"); // dbuscon.callMethod("org.freedesktop.secrets", // "/org/freedesktop/secrets", // "org.freedesktop.Secret.Service", // "ReadAlias", // {"default"}); // } /* UNLOCK THE DEFAULT COLLECTION */ if (d_dbus_verbose) Logger::message("[Unlock]"); dbuscon.callMethod("org.freedesktop.secrets", "/org/freedesktop/secrets", "org.freedesktop.Secret.Service", "Unlock", std::vector{DBusArray{DBusObjectPath{"/org/freedesktop/secrets/aliases/default"}}}); // This returns an array of already unlocked object paths (out of the input ones) and a prompt to unlock any locked ones. // if no collections need unlocking, the prompt is '/'; std::string prompt = dbuscon.get("aoo", 1); if (prompt.empty()) { Logger::error("Error getting prompt"); return; } if (d_dbus_verbose) Logger::message(" *** Prompt: ", prompt); bool unlocked_by_us = false; if (prompt != "/") { /* REGISTER FOR SIGNAL */ if (!dbuscon.matchSignal("member='Completed'")) Logger::message("WARN: Failed to register for prompt signal"); /* PROMPT FOR UNLOCK */ if (d_dbus_verbose) Logger::message("[Prompt]"); dbuscon.callMethod("org.freedesktop.secrets", prompt.c_str(), "org.freedesktop.Secret.Prompt", "Prompt", {""}); // 'Platform specific window handle to use for showing the prompt.' /* WAIT FOR PROMPT COMPLETED SIGNAL */ // note, we will not even check the signal contents (dismissed/result), since we check if we're // unlocked next anyway... if (!dbuscon.waitSignal(20, 2500, "org.freedesktop.Secret.Prompt", "Completed")) if (d_dbus_verbose) Logger::error("Failed to wait for unlock prompt..."); unlocked_by_us = true; } /* CHECK COLLECTION IS UNLOCKED NOW */ dbuscon.callMethod("org.freedesktop.secrets", "/org/freedesktop/secrets/aliases/default", "org.freedesktop.DBus.Properties", "Get", {"org.freedesktop.Secret.Collection", "Locked"}); bool islocked = dbuscon.get("v", 0, true); if (islocked) { Logger::error("Failed to unlock collection"); return; } /* GET ITEMS */ if (d_dbus_verbose) Logger::message("[GetItems]"); dbuscon.callMethod("org.freedesktop.secrets", "/org/freedesktop/secrets/aliases/default", "org.freedesktop.DBus.Properties", "Get", {"org.freedesktop.Secret.Collection", "Items"}); std::vector items = dbuscon.get>("v", 0); if (items.empty()) { Logger::error("Failed to get any items"); return; } else if (d_dbus_verbose) Logger::message("Got ", items.size(), " items to check"); for (auto const &item : items) { // check label dbuscon.callMethod("org.freedesktop.secrets", item.c_str(), "org.freedesktop.DBus.Properties", "Get", {"org.freedesktop.Secret.Item", "Label"}); std::string label = dbuscon.get("v", 0); if (d_dbus_verbose) Logger::message(" *** Label: ", label); #if __cpp_lib_string_contains >= 202011L if ((label.contains("Chrome") || label.contains("Chromium")) && (label.contains("Safe Storage") || label.contains("Keys")) && (!label.contains("Control"))) #else if ((label.find("Chrome") != std::string::npos || label.find("Chromium") != std::string::npos) && (label.find("Safe Storage") != std::string::npos || label.find("Keys") != std::string::npos) && (label.find("Control") == std::string::npos)) #endif { /* GET SECRETS */ if (d_dbus_verbose) Logger::message("[GetSecret]"); dbuscon.callMethod("org.freedesktop.secrets", item, "org.freedesktop.Secret.Item", "GetSecret", {DBusObjectPath{session_objectpath}}); /* The secret returned by SecretService is a struct: struct Secret { ObjectPath session ; Array parameters ; Array value ; String content_type ; }; A struct has signature (oayays), the brackets meaning 'struct'. we want the 'value' (the second ay); */ std::vector secret_bytes = dbuscon.get>("(oayays)", {0, 2}); // Since the secret is always 16 bytes, in base64 encoding, // its length must be [16/3]*4 + two '=' padding. if (secret_bytes.size() != 24 || secret_bytes[23] != '=' || secret_bytes[22] != '=') { if (d_dbus_verbose) [[unlikely]] Logger::message("Retrieved data is not a valid secret"); continue; } if (d_dbus_verbose) [[unlikely]] Logger::message(" *** SECRET: ", Logger::VECTOR(secret_bytes)); secrets->emplace(std::string{secret_bytes.begin(), secret_bytes.end()}); } } /* LOCK COLLECTION */ if (unlocked_by_us) { if (d_dbus_verbose) Logger::message("[Lock]"); dbuscon.callMethod("org.freedesktop.secrets", "/org/freedesktop/secrets", "org.freedesktop.Secret.Service", "Lock", std::vector{DBusArray{DBusObjectPath{"/org/freedesktop/secrets/aliases/default"}}}); } /* CLOSE SESSION */ if (d_dbus_verbose) Logger::message("[Close]"); dbuscon.callMethod("org.freedesktop.secrets", session_objectpath.c_str(), //"/org/freedesktop/secrets", "org.freedesktop.Secret.Session", "Close"); } #endif #endif signalbackup-tools-20250313-1/desktopdatabase/getsecrets_mac.cc000066400000000000000000000070731476450434500243720ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #if defined(__APPLE__) && defined(__MACH__) /* $ security dump-keychain: keychain: "/Users/username/Library/Keychains/login.keychain-db" version: 512 class: "genp" <--- class: general password attributes: 0x00000007 ="Signal Safe Storage" 0x00000008 = "acct"="Signal Key" <--- account: Signal Key "cdat"=0x32303234303831353134333230395A00 "20240815143209Z\000" "crtr"="aapl" "cusi"= "desc"= "gena"= "icmt"= "invi"= "mdat"=0x32303234303831353134333230395A00 "20240815143209Z\000" "nega"= "prot"= "scrp"= "svce"="Signal Safe Storage" <--- service: Signal Safe Storage "type"= */ #include "desktopdatabase.ih" #include void DesktopDatabase::getSecrets_mac(std::set *secrets) const { // create query to search the keychain: int const dict_size = 4; void const *keys[dict_size] = {kSecClass, kSecAttrAccount, kSecAttrService, kSecReturnData}; CFStringRef account = CFStringCreateWithCString(nullptr, "Signal Key", kCFStringEncodingUTF8); CFStringRef service = CFStringCreateWithCString(nullptr, "Signal Safe Storage", kCFStringEncodingUTF8); void const *values[dict_size] = {kSecClassGenericPassword, account, service, kCFBooleanTrue}; CFDictionaryRef query = CFDictionaryCreate(nullptr, keys, values, dict_size, nullptr, nullptr); // do the search CFDataRef item_data = nullptr; OSStatus ret = SecItemCopyMatching(query, reinterpret_cast(&item_data)); // clean up CFRelease(query); CFRelease(account); CFRelease(service); if (ret != 0) // error { CFStringRef errmsg_ref = SecCopyErrorMessageString(ret, nullptr); CFIndex length = CFStringGetLength(errmsg_ref); CFIndex max_length = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; std::unique_ptr error_string(new char[max_length]); if (CFStringGetCString(errmsg_ref, error_string.get(), max_length, kCFStringEncodingUTF8) != 0) Logger::error("Unknown error searching keychain"); else Logger::error(error_string.get()); return; } // parse returned items... int secret_length = CFDataGetLength(item_data); if (secret_length != 24) { Logger::warning("Unexpected secret_length (was ", secret_length, ", expected 24)"); return; } //std::string secret(reinterpret_cast(CFDataGetBytePtr(item_data)), secret_length); //secrets->emplace(secret); secrets->emplace(std::string(reinterpret_cast(CFDataGetBytePtr(item_data)), secret_length)); } #endif signalbackup-tools-20250313-1/desktopdatabase/init.cc000066400000000000000000000072371476450434500223470ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "desktopdatabase.ih" bool DesktopDatabase::init(std::string const &rawdb) { // get directories if (d_configdir.empty() || d_databasedir.empty()) { std::tie(d_configdir, d_databasedir) = getDesktopDir(); if (d_configdir.empty() || d_databasedir.empty()) [[unlikely]] { Logger::warning("Failed to set default location of Signal Desktop data."); Logger::warning_indent("Consider using `--desktopdir ' to specify manually."); Logger::warning_indent("Attempting to continue, but this will likely cause errors."); } } // open pre-decrypted desktop database if (!rawdb.empty()) [[unlikely]] { std::ifstream database(rawdb, std::ios_base::in | std::ios_base::binary); if (!database.is_open()) { Logger::error("failed to open database file '", rawdb, "'"); return false; } uint64_t size = bepaald::fileSize(rawdb); d_rawdb.reset(new unsigned char[size]); if (!(database.read(reinterpret_cast(d_rawdb.get()), size))) { Logger::error("Failed to read database data from raw file"); return false; } std::pair desktopdata = {d_rawdb.get(), size}; d_database = MemSqliteDB(&desktopdata); if (!d_database.ok()) { Logger::error("Failed to open database"); return false; } d_database.checkDatabaseWriteVersion(); return true; } // check if a wal (Write-Ahead Logging) file is present in path, and warn user to (cleanly) shut Signal Desktop down if (!d_ignorewal && bepaald::fileOrDirExists(d_databasedir + "/sql/db.sqlite-wal")) { // warn Logger::warning("Found Sqlite-WAL file (write-ahead logging)."); Logger::warning_indent("Desktop data may not be fully up-to-date."); Logger::warning_indent("Maybe Signal Desktop has not cleanly shut down?"); Logger::warning_indent("(pass `--ignorewal' to disable this warning)"); return false; } // get key if (d_hexkey.empty()) if (!getKey()) { Logger::error("Failed to get sqlcipher key to decrypt Signal Desktop database"); return false; } if (d_showkey) Logger::message("Signal Desktop key (hex): ", d_hexkey); // decrypt the database d_cipherdb.reset(new SqlCipherDecryptor(d_databasedir + "/sql/db.sqlite", d_hexkey, d_cipherversion, d_verbose)); if (!d_cipherdb->ok()) return false; // get the decrypted data auto [data, size] = d_cipherdb->data(); // unsigned char *, uint64_t // disable WAL (Write-Ahead Logging) on database, reading from memory // otherwise will not work see https://www.sqlite.org/fileformat.html if (data[0x12] == 2) data[0x12] = 1; if (data[0x13] == 2) data[0x13] = 1; std::pair desktopdata = {data, size}; d_database = MemSqliteDB(&desktopdata); if (!d_database.ok()) { Logger::error("Failed to open database"); return false; } d_database.checkDatabaseWriteVersion(); return true; } signalbackup-tools-20250313-1/desktopdatabase/readencryptedkey.cc000066400000000000000000000032621476450434500247400ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "desktopdatabase.ih" std::string DesktopDatabase::readEncryptedKey() const { std::string encrypted_key; std::fstream config(d_configdir + "/config.json", std::ios_base::in | std::ios_base::binary); if (!config.is_open()) [[unlikely]] { Logger::error("Failed to open input: ", d_configdir, "/config.json"); return encrypted_key; } std::string line; std::regex keyregex("^\\s*\"encryptedKey\":\\s*\"([a-zA-Z0-9]+)\",?$"); std::smatch m; bool found = false; while (std::getline(config, line)) { //std::cout << "Line: " << line << std::endl; if (std::regex_match(line, m, keyregex)) if (m.size() == 2) // m[0] is full match, m[1] is first submatch (which we want) { found = true; break; } } if (!found) { Logger::warning("Failed to read encrypted key from config.json, trying plaintext key..."); return encrypted_key; } encrypted_key = m[1].str(); return encrypted_key; } signalbackup-tools-20250313-1/dummybackup/000077500000000000000000000000001476450434500202475ustar00rootroot00000000000000signalbackup-tools-20250313-1/dummybackup/dummybackup.h000066400000000000000000000422221476450434500227430ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef DUMMYBACKUP_H_ #define DUMMYBACKUP_H_ #include "../signalbackup/signalbackup.h" #include "../desktopdatabase/desktopdatabase.h" #include "../signalplaintextbackupdatabase/signalplaintextbackupdatabase.h" class DummyBackup : public SignalBackup { public: inline DummyBackup(bool verbose, bool truncate, bool showprogress); inline DummyBackup(std::unique_ptr const &ptdb, std::string const &selfid, bool verbose, bool truncate, bool showprogress); // for importing from plaintext inline DummyBackup(std::unique_ptr const &ddb, bool verbose, bool truncate, bool showprogress); // for importing from desktop DummyBackup(DummyBackup const &other) = delete; DummyBackup &operator=(DummyBackup const &other) = delete; DummyBackup(DummyBackup &&other) = delete; DummyBackup &operator=(DummyBackup &&other) = delete; }; inline DummyBackup::DummyBackup(bool verbose, bool truncate, bool showprogress) : SignalBackup(verbose, truncate, showprogress) { // set up required tables (database version 223) // MESSAGE if (!d_database.exec("CREATE TABLE IF NOT EXISTS message (_id INTEGER PRIMARY KEY AUTOINCREMENT, date_sent INTEGER NOT NULL, date_received INTEGER NOT NULL, date_server INTEGER DEFAULT -1, thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE, from_recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, from_device_id INTEGER, to_recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, type INTEGER NOT NULL, body TEXT, read INTEGER DEFAULT 0, ct_l TEXT, exp INTEGER, m_type INTEGER, m_size INTEGER, st INTEGER, tr_id TEXT, subscription_id INTEGER DEFAULT -1, receipt_timestamp INTEGER DEFAULT -1, has_delivery_receipt INTEGER DEFAULT 0, has_read_receipt INTEGER DEFAULT 0, viewed INTEGER DEFAULT 0, mismatched_identities TEXT DEFAULT NULL, network_failures TEXT DEFAULT NULL, expires_in INTEGER DEFAULT 0, expire_started INTEGER DEFAULT 0, notified INTEGER DEFAULT 0, quote_id INTEGER DEFAULT 0, quote_author INTEGER DEFAULT 0, quote_body TEXT DEFAULT NULL, quote_missing INTEGER DEFAULT 0, quote_mentions BLOB DEFAULT NULL, quote_type INTEGER DEFAULT 0, shared_contacts TEXT DEFAULT NULL, unidentified INTEGER DEFAULT 0, link_previews TEXT DEFAULT NULL, view_once INTEGER DEFAULT 0, reactions_unread INTEGER DEFAULT 0, reactions_last_seen INTEGER DEFAULT -1, remote_deleted INTEGER DEFAULT 0, mentions_self INTEGER DEFAULT 0, notified_timestamp INTEGER DEFAULT 0, server_guid TEXT DEFAULT NULL, message_ranges BLOB DEFAULT NULL, story_type INTEGER DEFAULT 0, parent_story_id INTEGER DEFAULT 0, export_state BLOB DEFAULT NULL, exported INTEGER DEFAULT 0, scheduled_date INTEGER DEFAULT -1, latest_revision_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE CASCADE, original_message_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE CASCADE, revision_number INTEGER DEFAULT 0, message_extras BLOB DEFAULT NULL)")) return; // THREAD if (!d_database.exec("CREATE TABLE IF NOT EXISTS thread ( _id INTEGER PRIMARY KEY AUTOINCREMENT, date INTEGER DEFAULT 0, meaningful_messages INTEGER DEFAULT 0, recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE, read INTEGER DEFAULT 1, type INTEGER DEFAULT 0, error INTEGER DEFAULT 0, snippet TEXT, snippet_type INTEGER DEFAULT 0, snippet_uri TEXT DEFAULT NULL, snippet_content_type TEXT DEFAULT NULL, snippet_extras TEXT DEFAULT NULL, unread_count INTEGER DEFAULT 0, archived INTEGER DEFAULT 0, status INTEGER DEFAULT 0, has_delivery_receipt INTEGER DEFAULT 0, has_read_receipt INTEGER DEFAULT 0, expires_in INTEGER DEFAULT 0, last_seen INTEGER DEFAULT 0, has_sent INTEGER DEFAULT 0, last_scrolled INTEGER DEFAULT 0, pinned INTEGER DEFAULT 0, unread_self_mention_count INTEGER DEFAULT 0, active INTEGER DEFAULT 0, snippet_message_extras BLOB DEFAULT NULL)")) return; // RECIPIENT // added message_expiration_time_version if (!d_database.exec("CREATE TABLE IF NOT EXISTS recipient ( _id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER DEFAULT 0, e164 TEXT UNIQUE DEFAULT NULL, aci TEXT UNIQUE DEFAULT NULL, pni TEXT UNIQUE DEFAULT NULL CHECK (pni LIKE 'PNI:%'), username TEXT UNIQUE DEFAULT NULL, email TEXT UNIQUE DEFAULT NULL, group_id TEXT UNIQUE DEFAULT NULL, distribution_list_id INTEGER DEFAULT NULL, call_link_room_id TEXT DEFAULT NULL, registered INTEGER DEFAULT 0, unregistered_timestamp INTEGER DEFAULT 0, blocked INTEGER DEFAULT 0, hidden INTEGER DEFAULT 0, profile_key TEXT DEFAULT NULL, profile_key_credential TEXT DEFAULT NULL, profile_sharing INTEGER DEFAULT 0, profile_given_name TEXT DEFAULT NULL, profile_family_name TEXT DEFAULT NULL, profile_joined_name TEXT DEFAULT NULL, profile_avatar TEXT DEFAULT NULL, last_profile_fetch INTEGER DEFAULT 0, system_given_name TEXT DEFAULT NULL, system_family_name TEXT DEFAULT NULL, system_joined_name TEXT DEFAULT NULL, system_nickname TEXT DEFAULT NULL, system_photo_uri TEXT DEFAULT NULL, system_phone_label TEXT DEFAULT NULL, system_phone_type INTEGER DEFAULT -1, system_contact_uri TEXT DEFAULT NULL, system_info_pending INTEGER DEFAULT 0, notification_channel TEXT DEFAULT NULL, message_ringtone TEXT DEFAULT NULL, message_vibrate INTEGER DEFAULT 0, call_ringtone TEXT DEFAULT NULL, call_vibrate INTEGER DEFAULT 0, mute_until INTEGER DEFAULT 0, message_expiration_time INTEGER DEFAULT 0, sealed_sender_mode INTEGER DEFAULT 0, storage_service_id TEXT UNIQUE DEFAULT NULL, storage_service_proto TEXT DEFAULT NULL, mention_setting INTEGER DEFAULT 0, capabilities INTEGER DEFAULT 0, last_session_reset BLOB DEFAULT NULL, wallpaper BLOB DEFAULT NULL, wallpaper_uri TEXT DEFAULT NULL, about TEXT DEFAULT NULL, about_emoji TEXT DEFAULT NULL, extras BLOB DEFAULT NULL, groups_in_common INTEGER DEFAULT 0, avatar_color TEXT DEFAULT NULL, chat_colors BLOB DEFAULT NULL, custom_chat_colors_id INTEGER DEFAULT 0, badges BLOB DEFAULT NULL, needs_pni_signature INTEGER DEFAULT 0, reporting_token BLOB DEFAULT NULL , phone_number_sharing INTEGER DEFAULT 0, phone_number_discoverable INTEGER DEFAULT 0, pni_signature_verified INTEGER DEFAULT 0, nickname_given_name TEXT DEFAULT NULL, nickname_family_name TEXT DEFAULT NULL, nickname_joined_name TEXT DEFAULT NULL, note TEXT DEFAULT NULL, message_expiration_time_version INTEGER DEFAULT 1 NOT NULL)")) return; // ATTACHMENT if (!d_database.exec("CREATE TABLE attachment ( _id INTEGER PRIMARY KEY AUTOINCREMENT, message_id INTEGER, content_type TEXT, remote_key TEXT, remote_location TEXT, remote_digest BLOB, remote_incremental_digest BLOB, remote_incremental_digest_chunk_size INTEGER DEFAULT 0, cdn_number INTEGER DEFAULT 0, transfer_state INTEGER, transfer_file TEXT DEFAULT NULL, data_file TEXT, data_size INTEGER, data_random BLOB, file_name TEXT, fast_preflight_id TEXT, voice_note INTEGER DEFAULT 0, borderless INTEGER DEFAULT 0, video_gif INTEGER DEFAULT 0, quote INTEGER DEFAULT 0, width INTEGER DEFAULT 0, height INTEGER DEFAULT 0, caption TEXT DEFAULT NULL, sticker_pack_id TEXT DEFAULT NULL, sticker_pack_key DEFAULT NULL, sticker_id INTEGER DEFAULT -1, sticker_emoji STRING DEFAULT NULL, blur_hash TEXT DEFAULT NULL, transform_properties TEXT DEFAULT NULL, display_order INTEGER DEFAULT 0, upload_timestamp INTEGER DEFAULT 0 , data_hash_start TEXT DEFAULT NULL, data_hash_end TEXT DEFAULT NULL)")) return; // REACTION if (!d_database.exec("CREATE TABLE IF NOT EXISTS reaction ( _id INTEGER PRIMARY KEY, message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE, author_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, emoji TEXT NOT NULL, date_sent INTEGER NOT NULL, date_received INTEGER NOT NULL, UNIQUE(message_id, author_id) ON CONFLICT REPLACE)")) return; // STICKER if (!d_database.exec("CREATE TABLE sticker (_id INTEGER PRIMARY KEY AUTOINCREMENT, pack_id TEXT NOT NULL, pack_key TEXT NOT NULL, pack_title TEXT NOT NULL, pack_author TEXT NOT NULL, sticker_id INTEGER, cover INTEGER, emoji TEXT NOT NULL, last_used INTEGER, installed INTEGER,file_path TEXT NOT NULL, file_length INTEGER, file_random BLOB, pack_order INTEGER DEFAULT 0, content_type TEXT DEFAULT NULL, UNIQUE(pack_id, sticker_id, cover) ON CONFLICT IGNORE);")) return; // MENTION if (!d_database.exec("CREATE TABLE mention (_id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER, message_id INTEGER, recipient_id INTEGER, range_start INTEGER, range_length INTEGER)")) return; // GROUPS if (!d_database.exec("CREATE TABLE IF NOT EXISTS groups ( _id INTEGER PRIMARY KEY, group_id TEXT NOT NULL UNIQUE, recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE, title TEXT DEFAULT NULL, avatar_id INTEGER DEFAULT 0, avatar_key BLOB DEFAULT NULL, avatar_content_type TEXT DEFAULT NULL, avatar_digest BLOB DEFAULT NULL, timestamp INTEGER DEFAULT 0, active INTEGER DEFAULT 1, mms INTEGER DEFAULT 0, master_key BLOB DEFAULT NULL, revision BLOB DEFAULT NULL, decrypted_group BLOB DEFAULT NULL, expected_v2_id TEXT UNIQUE DEFAULT NULL, unmigrated_v1_members TEXT DEFAULT NULL, distribution_id TEXT UNIQUE DEFAULT NULL, show_as_story_state INTEGER DEFAULT 0, last_force_update_timestamp INTEGER DEFAULT 0)")) return; // GROUP MEMBERSHIP if (!d_database.exec("CREATE TABLE IF NOT EXISTS group_membership ( _id INTEGER PRIMARY KEY, group_id TEXT NOT NULL REFERENCES groups (group_id) ON DELETE CASCADE, recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, UNIQUE(group_id, recipient_id))")) return; // GROUP RECEIPTS if (!d_database.exec("CREATE TABLE group_receipts (_id INTEGER PRIMARY KEY, mms_id INTEGER, address TEXT, status INTEGER, timestamp INTEGER, unidentified INTEGER DEFAULT 0)")) return; // IDENTITIES // not really strictly necessary, but dtInsertRecipient now tries to fill this table... if (!d_database.exec("CREATE TABLE identities (_id INTEGER PRIMARY KEY AUTOINCREMENT, address INTEGER UNIQUE, identity_key TEXT, first_use INTEGER DEFAULT 0, timestamp INTEGER DEFAULT 0, verified INTEGER DEFAULT 0, nonblocking_approval INTEGER DEFAULT 0)")) return; // set database version DeepCopyingUniquePtr d_new_dbvframe; if (!setFrameFromStrings(&d_new_dbvframe, std::vector{"VERSION:uint32:223"})) { Logger::error("Failed to create new databaseversionframe"); return; } d_databaseversionframe.reset(d_new_dbvframe.release()); d_databaseversion = 223; // set headerframe DeepCopyingUniquePtr d_new_headerframe; if (!setFrameFromStrings(&d_new_headerframe, std::vector{"IV:bytes:AAAAAAAAAAAAAAAAAAAAAA==", "SALT:bytes:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "VERSION:uint32:1"})) { Logger::error("Failed to create new databaseversionframe"); return; } d_headerframe.reset(d_new_headerframe.release()); // set endframe d_endframe.reset(new EndFrame(nullptr, 1ull)); setColumnNames(); d_ok = true; } inline DummyBackup::DummyBackup(std::unique_ptr const &ptdb, std::string const &selfid, bool verbose, bool truncate, bool showprogress) : DummyBackup(verbose, truncate, showprogress) { if (!d_ok) return; d_ok = false; // a selfid is required to be able to correctly set 'from_recipient_id' and 'to_recipient_id' when importing messages std::string selfphone(selfid); if (selfphone.empty()) { // open desktopdb, scan for self id, add to recipient and set d_selfphone/id if (!ptdb->ok()) Logger::error("SignalPlaintextBackupDatabase was not ok"); selfphone = ptdb->d_database.getSingleResultAs("SELECT DISTINCT sourceaddress FROM smses " "WHERE type = 2 AND numaddresses > 1 AND ismms = 1", std::string()); } if (selfphone.empty()) { Logger::error("Failed to determine id of 'self'. Please pass `--setselfid \"[phone]\"' to set it manually if problems occur"); return; } // it is possible the contactname is set for this contact through --mapxmlcontactnames std::string contact_name = ptdb->d_database.getSingleResultAs("SELECT MAX(contact_name) FROM smses WHERE address = ? " "AND contact_name IS NOT NULL AND contact_name IS NOT ''", selfphone, std::string()); std::any new_rid; if (!insertRow("recipient", {{d_recipient_e164, selfphone}, {(contact_name.empty() ? "" : "profile_given_name"), contact_name}, {(contact_name.empty() ? "" : "profile_joined_name"), contact_name}}, "_id", &new_rid)) return; d_selfid = std::any_cast(new_rid); d_ok = true; } inline DummyBackup::DummyBackup(std::unique_ptr const &ddb, bool verbose, bool truncate, bool showprogress) : DummyBackup(verbose, truncate, showprogress) { if (!d_ok) { Logger::error("Base not initialized ok"); return; } d_ok = false; // open desktopdb, scan for self id, add to recipient and set d_selfphone/id // DesktopDatabase ddb(configdir, databasedir, hexkey, verbose, ignorewal, cipherversion, truncate); if (!ddb->ok()) Logger::error("DesktopDatabase was not ok"); dtSetColumnNames(&ddb->d_database); // on messages sent from Desktop, sourceServiceId/sourceUuid is empty std::string uuid = ddb->d_database.getSingleResultAs("SELECT DISTINCT NULLIF(" + d_dt_m_sourceuuid + ", '') FROM messages " "WHERE type = 'outgoing' AND " + d_dt_m_sourceuuid + " IS NOT NULL", std::string()); if (uuid.empty()) // on messages sent from Desktop, sourceServiceId/sourceUuid is empty { // try from sessions: uuid = ddb->d_database.getSingleResultAs("SELECT DISTINCT " + d_dt_s_uuid + " FROM sessions WHERE SUBSTR(" + d_dt_s_uuid + ", 1, 4) != 'PNI:'", std::string()); if (uuid.empty()) { // a bit more complicated: uuid = ddb->d_database.getSingleResultAs("SELECT " + d_dt_c_uuid + " FROM conversations WHERE id IS " "(" " SELECT DISTINCT NULLIF(key, '') FROM messages, json_each(messages.json, '$.sendStateByConversationId') " " WHERE messages.type = 'outgoing' AND key IS NOT messages.conversationId AND messages.conversationId NOT IN " " (" " SELECT id FROM conversations WHERE type = 'group'" " )" ")", std::string()); if (uuid.empty()) { Logger::error("Failed to determine uuid of self"); return; } } } SqliteDB::QueryResults selfdata; if (!ddb->d_database.exec("SELECT profileName, profileFamilyName, profileFullName, e164, json_extract(json, '$.color') AS color " "FROM conversations WHERE " + d_dt_c_uuid + " = ?", uuid, &selfdata) || selfdata.rows() != 1) { Logger::error("Failed to get profile data of self from Desktop database"); return; } std::any new_rid; if (!insertRow("recipient", {{d_recipient_profile_given_name, selfdata.value(0, "profileName")}, {"profile_family_name", selfdata.value(0, "profileFamilyName")}, {"profile_joined_name", selfdata.value(0, "profileFullName")}, {d_recipient_e164, selfdata.value(0, "e164")}, {d_recipient_aci, uuid}, {d_recipient_avatar_color, selfdata.value(0, "color")}}, "_id", &new_rid)) { Logger::error("Failed to insert profile data of self into DummyBackup"); return; } /* // insert self, even if we have no profile data... std::any new_rid; if (!insertRow("recipient", {{d_recipient_aci, uuid}}, "_id", &new_rid)) { Logger::error("Failed to insert profile data of self into DummyBackup"); return; } */ d_selfid = std::any_cast(new_rid); d_selfuuid = uuid; d_ok = true; } #endif signalbackup-tools-20250313-1/endframe/000077500000000000000000000000001476450434500175075ustar00rootroot00000000000000signalbackup-tools-20250313-1/endframe/endframe.h000066400000000000000000000070431476450434500214450ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef ENDFRAME_H_ #define ENDFRAME_H_ #include "../backupframe/backupframe.h" class EndFrame: public BackupFrame { static Registrar s_registrar; public: inline EndFrame(unsigned char const *bytes, size_t length, uint64_t count = 0); inline virtual ~EndFrame() override = default; inline virtual EndFrame *clone() const override; inline virtual EndFrame *move_clone() override; inline static BackupFrame *create(unsigned char const *bytes, size_t length, uint64_t count); inline virtual void printInfo() const override; inline virtual FRAMETYPE frameType() const override; inline std::pair getData() const override; inline virtual bool validate(uint64_t available) const override; private: inline uint64_t dataSize() const override; }; inline EndFrame::EndFrame(unsigned char const *, size_t length, uint64_t count) : BackupFrame(count) // endframe is a raw bool, not a message, so no field type (it sorta IS its only field), length is value { unsigned char *valbytes = new unsigned char[sizeof(length)]; intTypeToBytes(length, valbytes); d_framedata.push_back(std::make_tuple(0, valbytes, sizeof(length))); } inline EndFrame *EndFrame::clone() const { return new EndFrame(*this); } inline EndFrame *EndFrame::move_clone() { return new EndFrame(std::move(*this)); } inline BackupFrame *EndFrame::create(unsigned char const *bytes, size_t length, uint64_t count) // static { return new EndFrame(bytes, length, count); } inline void EndFrame::printInfo() const // virtual override { Logger::message("Frame number: ", d_count); Logger::message(" Type: END"); for (auto const &p : d_framedata) Logger::message(" - (value : \"", std::boolalpha, (bytesToUint64(std::get<1>(p), std::get<2>(p)) ? true : false), "\")"); } inline BackupFrame::FRAMETYPE EndFrame::frameType() const // virtual override { return FRAMETYPE::END; } inline uint64_t EndFrame::dataSize() const { uint64_t size = 0; // for size of this entire frame. size += varIntSize(size); return ++size; } inline std::pair EndFrame::getData() const { uint64_t size = dataSize(); unsigned char *data = new unsigned char[size]; //uint64_t datapos = 0; //datapos += setFieldAndWire(FRAMETYPE::END, WIRETYPE::VARINT, data + datapos); //datapos += putVarInt(1, data + datapos); uint64_t datapos = setFieldAndWire(FRAMETYPE::END, WIRETYPE::VARINT, data); putVarInt(1, data + datapos); return {data, size}; } inline bool EndFrame::validate(uint64_t available) const { return d_framedata.size() == 1 && std::get<2>(d_framedata.front()) == 8 && available == 0 && (bytesToUint64(std::get<1>(d_framedata.front()), std::get<2>(d_framedata.front())) == 1 || bytesToUint64(std::get<1>(d_framedata.front()), std::get<2>(d_framedata.front())) == 0); } #endif signalbackup-tools-20250313-1/endframe/endframe.ih000066400000000000000000000014001476450434500216050ustar00rootroot00000000000000/* Copyright (C) 2019-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "endframe.h" signalbackup-tools-20250313-1/endframe/statics.cc000066400000000000000000000015051476450434500214710ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "endframe.ih" EndFrame::Registrar EndFrame::s_registrar(FRAMETYPE::END, create); signalbackup-tools-20250313-1/filedecryptor/000077500000000000000000000000001476450434500206015ustar00rootroot00000000000000signalbackup-tools-20250313-1/filedecryptor/customs.cc000066400000000000000000000734601476450434500226170ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ //#include "filedecryptor.ih" // #include // #include // #include // #include // void FileDecryptor::strugee(uint64_t pos) // { // unsigned int offset = 0; // d_file.seekg(pos, std::ios_base::beg); // Logger::message("Getting frame at filepos: ", d_file.tellg()); // if (static_cast(d_file.tellg()) == d_filesize) // { // Logger::message("Read entire backup file..."); // return; // } // uint32_t encryptedframelength = getNextFrameBlockSize(); // if (encryptedframelength > 3145728/*= 3MB*/ /*115343360 / * =110MB*/ || encryptedframelength < 11) // { // Logger::error("Framesize too big to be real"); // return; // } // std::unique_ptr encryptedframe(new unsigned char[encryptedframelength]); // if (!getNextFrameBlock(encryptedframe.get(), encryptedframelength)) // return; // // check hash // unsigned int digest_size = SHA256_DIGEST_LENGTH; // unsigned char hash[SHA256_DIGEST_LENGTH]; // HMAC(EVP_sha256(), d_mackey, d_mackey_size, encryptedframe.get(), encryptedframelength - MACSIZE, hash, &digest_size); // if (std::memcmp(encryptedframe.get() + (encryptedframelength - MACSIZE), hash, MACSIZE) != 0) // { // Logger::error("BAD MAC!"); // return; // } // else // { // Logger::message("\nGOT GOOD MAC AT OFFSET ", offset, " BYTES!\n" // "Now let's try and find out how many frames we skipped to get here...."); // d_badmac = false; // } // // decode // unsigned int skipped = 0; // std::unique_ptr frame(nullptr); // while (!frame) // { // if (skipped > 1000000) // a frame is at least 10 bytes? -> could probably safely set this higher. MAC alone is 10 bytes, there is also actual data // { // Logger::message("TESTED 1000000 frames"); // return; // } // if (skipped % 100 == 0) // Logger::message_overwrite("Checking if we skipped ", skipped, " frames... "); // //Logger::message("\rChecking if we skipped ", skipped, " frames... ", std::flush; // uintToFourBytes(d_iv, d_counter + skipped); // // create context // std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); // // disable padding // EVP_CIPHER_CTX_set_padding(ctx.get(), 0); // if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr, d_cipherkey, d_iv) != 1) // { // Logger::error("CTX INIT FAILED"); // return; // } // int decodedframelength = encryptedframelength - MACSIZE; // unsigned char *decodedframe = new unsigned char[decodedframelength]; // if (EVP_DecryptUpdate(ctx.get(), decodedframe, &decodedframelength, encryptedframe.get(), encryptedframelength - MACSIZE) != 1) // { // Logger::error("Failed to decrypt data"); // delete[] decodedframe; // return; // } // DEBUGOUT("Decoded hex : ", bepaald::bytesToHexString(decodedframe, decodedframelength)); // frame.reset(initBackupFrame(decodedframe, decodedframelength, d_framecount + skipped)); // delete[] decodedframe; // ++skipped; // if (!frame) // { // Logger::message_overwrite("Checking if we skipped ", skipped, " frames... nope! :("); // //Logger::message("\rChecking if we skipped ", skipped, " frames... nope! :(", std::flush; // //if (skipped > // } // else // { // if (frame->validate() && // frame->frameType() != BackupFrame::FRAMETYPE::HEADER && // it is impossible to get in this function without the headerframe, and there is only one // (frame->frameType() != BackupFrame::FRAMETYPE::END || static_cast(d_file.tellg()) == d_filesize)) // { // d_counter += skipped; // d_framecount += skipped; // Logger::message_overwrite("Checking if we skipped ", skipped, " frames... YEAH! :)"); // //Logger::message("\rChecking if we skipped ", skipped, " frames... YEAH! :)"); // Logger::message("Good frame: ", frame->frameNumber(), " (", frame->frameTypeString(), ")\nCOUNTER: ", d_counter); // frame->printInfo(); // //delete[] encryptedframe.release(); // frame.reset(); // return; // } // Logger::message_overwrite("Checking if we skipped ", skipped, " frames... nope! :("); // frame.reset(); // } // } // //frame->printInfo(); // //Logger::message("HEADERTYPE: ", frame->frameType()); // uint32_t attsize = 0; // if (!d_badmac && (attsize = frame->attachmentSize()) > 0 && // (frame->frameType() == BackupFrame::FRAMETYPE::ATTACHMENT || // frame->frameType() == BackupFrame::FRAMETYPE::AVATAR || // frame->frameType() == BackupFrame::FRAMETYPE::STICKER)) // { // if (d_verbose) [[unlikely]] // Logger::message("Trying to read attachment (bruteforce)"); // uintToFourBytes(d_iv, d_counter++); // reinterpret_cast(frame.get())->setLazyData(d_iv, d_iv_size, d_mackey, d_mackey_size, d_cipherkey, d_cipherkey_size, attsize, d_filename, d_file.tellg()); // d_file.seekg(attsize + MACSIZE, std::ios_base::cur); // /* // if (!d_lazyload) // immediately decrypt i guess... // { // if (d_verbose) [[unlikely]] // Logger::message("Getting attachment at file pos ", d_file.tellg(), " (size: ", attsize, ")"); // int getatt = getAttachment(reinterpret_cast(frame.get())); // if (getatt != 0) // { // if (getatt < 0) // d_badmac = true; // return; // } // } // */ // } // } // #include "../sqlstatementframe/sqlstatementframe.h" // std::unique_ptr FileDecryptor::getFrameStrugee2() // { // long long int filepos = d_file.tellg(); // if (d_verbose) [[unlikely]] // Logger::message("Getting frame at filepos: ", filepos, " (COUNTER: ", d_counter, ")"); // if (static_cast(filepos) == d_filesize) [[unlikely]] // { // Logger::message("Read entire backup file..."); // return std::unique_ptr(nullptr); // } // if (d_headerframe) // { // std::unique_ptr frame(d_headerframe.release()); // return frame; // } // uint32_t encryptedframelength = getNextFrameBlockSize(); // //if (encryptedframelength > 3145728/*= 3MB*/ /*115343360 / * =110MB*/ || encryptedframelength < 11) // //{ // // Logger::message("Suspicious framelength"); // // bruteForceFrom(filepos)??? // //} // if (encryptedframelength == 0 && d_file.eof()) [[unlikely]] // { // Logger::message(bepaald::bold_on, "ERROR", bepaald::bold_off, " Unexpectedly hit end of file!"); // return std::unique_ptr(nullptr); // } // DEBUGOUT("Framelength: ", encryptedframelength); // if (d_verbose) [[unlikely]] // Logger::message("Framelength: ", encryptedframelength); // std::unique_ptr encryptedframe(new unsigned char[encryptedframelength]); // if (encryptedframelength > 115343360 /*110MB*/ || encryptedframelength < 11 || !getNextFrameBlock(encryptedframe.get(), encryptedframelength)) [[unlikely]] // { // Logger::message("Failed to read next frame (", encryptedframelength, " bytes at filepos ", filepos, ")"); // return std::unique_ptr(nullptr); // } // // check hash // unsigned int digest_size = SHA256_DIGEST_LENGTH; // unsigned char hash[SHA256_DIGEST_LENGTH]; // HMAC(EVP_sha256(), d_mackey, d_mackey_size, encryptedframe.get(), encryptedframelength - MACSIZE, hash, &digest_size); // if (std::memcmp(encryptedframe.get() + (encryptedframelength - MACSIZE), hash, 10) != 0) [[unlikely]] // { // Logger::warning("Bad MAC in frame: theirMac: ", bepaald::bytesToHexString(encryptedframe.get() + (encryptedframelength - MACSIZE), MACSIZE)); // Logger::warning_indent(" ourMac: ", bepaald::bytesToHexString(hash, SHA256_DIGEST_LENGTH)); // d_badmac = true; // if (d_stoponerror) // { // Logger::message("Stop reading backup. Next frame would be read at offset ", filepos + encryptedframelength); // return std::unique_ptr(nullptr); // } // } // else // { // d_badmac = false; // if (d_verbose) [[unlikely]] // { // Logger::message("Calculated mac: ", bepaald::bytesToHexString(hash, SHA256_DIGEST_LENGTH)); // Logger::message("Mac in file : ", bepaald::bytesToHexString(encryptedframe.get() + (encryptedframelength - MACSIZE), MACSIZE)); // } // } // // decode // uintToFourBytes(d_iv, d_counter++); // // create context // std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); // // disable padding // EVP_CIPHER_CTX_set_padding(ctx.get(), 0); // if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr, d_cipherkey, d_iv) != 1) [[unlikely]] // { // Logger::message("CTX INIT FAILED"); // return std::unique_ptr(nullptr); // } // int decodedframelength = encryptedframelength - MACSIZE; // unsigned char *decodedframe = new unsigned char[decodedframelength]; // if (EVP_DecryptUpdate(ctx.get(), decodedframe, &decodedframelength, encryptedframe.get(), encryptedframelength - MACSIZE) != 1) [[unlikely]] // { // Logger::message("Failed to decrypt data"); // delete[] decodedframe; // return std::unique_ptr(nullptr); // } // delete[] encryptedframe.release(); // free up already.... // std::unique_ptr frame(initBackupFrame(decodedframe, decodedframelength, d_framecount++)); // if (!frame) [[unlikely]] // { // Logger::message("Failed to get valid frame from decoded data..."); // if (d_badmac) // { // Logger::message("Encrypted data had failed verification (Bad MAC)"); // delete[] decodedframe; // return std::unique_ptr(nullptr); // } // else // { // Logger::message("Data was verified ok, but does not represent a valid frame... Don't know what happened, but it's bad... :("); // Logger::message("Decrypted frame data: ", bepaald::bytesToHexString(decodedframe, decodedframelength)); // delete[] decodedframe; // return std::make_unique(); // } // delete[] decodedframe; // return std::unique_ptr(nullptr); // } // delete[] decodedframe; // uint32_t attsize = frame->attachmentSize(); // if (!d_badmac && attsize > 0 && // (frame->frameType() == BackupFrame::FRAMETYPE::ATTACHMENT || // frame->frameType() == BackupFrame::FRAMETYPE::AVATAR || // frame->frameType() == BackupFrame::FRAMETYPE::STICKER)) // { // if ((d_file.tellg() < 0 && d_file.eof()) || (attsize + static_cast(d_file.tellg()) > d_filesize)) [[unlikely]] // if (!d_assumebadframesize) // { // Logger::error("Unexpectedly hit end of file while reading attachment!"); // return std::unique_ptr(nullptr); // } // uintToFourBytes(d_iv, d_counter++); // reinterpret_cast(frame.get())->setLazyData(d_iv, d_iv_size, d_mackey, d_mackey_size, d_cipherkey, d_cipherkey_size, attsize, d_filename, d_file.tellg()); // d_file.seekg(attsize + MACSIZE, std::ios_base::cur); // /* // if (!d_lazyload) // immediately decrypt i guess... // { // if (d_verbose) [[unlikely]] // Logger::message("Getting attachment at file pos ", d_file.tellg(), " (size: ", attsize, ")"); // int getatt = getAttachment(reinterpret_cast(frame.get())); // 0 == good, >0 == bad, <0 == bad+badmac // if (getatt > 0) // { // Logger::message("Failed to get attachment data for FrameWithAttachment... info:"); // frame->printInfo(); // return std::unique_ptr(nullptr); // } // if (getatt < 0) // { // d_badmac = true; // if (d_stoponerror) // { // Logger::message("Stop reading backup. Next frame would be read at offset ", filepos + encryptedframelength); // return std::unique_ptr(nullptr); // } // if (d_assumebadframesize) // { // std::unique_ptr f = bruteForceFrom(filepos, encryptedframelength); // //long long int curfilepos = d_file.tellg(); // //Logger::message("curpso: ", curfilepos); // //Logger::message("ATTACHMENT LENGTH SHOULD HAVE BEEN: ", curfilepos - filepos - encryptedframelength - MACSIZE); // return f; // } // } // } // */ // } // //Logger::message("FILEPOS: ", d_file.tellg()); // //delete frame; // return frame; // } // void FileDecryptor::strugee2() // { // d_stoponerror = true; // std::vector tables; // std::string lastmsg; // bool endfound = false; // std::unique_ptr frame(nullptr); // while ((frame = getFrameStrugee2())) // { // if (frame->frameType() == BackupFrame::FRAMETYPE::SQLSTATEMENT) // { // SqlStatementFrame *s = reinterpret_cast(frame.get()); // if (s->statement().find("INSERT INTO ") == 0) // { // // parse table name // std::string::size_type pos = s->statement().find(' ', 12); // std::string tablename = s->statement().substr(12, pos - 12); // if (std::find(tables.begin(), tables.end(), tablename) == tables.end()) // tables.push_back(tablename); // if (tablename == "mms" || tablename == "message" || tablename == "sms") // lastmsg = s->statement(); // } // } // if (frame->frameType() == BackupFrame::FRAMETYPE::END) // endfound = true; // } // Logger::message("Tables present in backup:"); // for (unsigned int i = 0; i < tables.size(); ++i) // Logger::message(tables[i], ((i == tables.size() - 1) && !endfound ? " (probably incomplete)" : "")); // Logger::message("Last message: ", (lastmsg.empty() ? "(none)" : lastmsg)); // } // void FileDecryptor::strugee3Helper(std::vector, uint64_t>> *macs_and_positions) // { // while (true) // { // //d_file.seekg(0, std::ios_base::beg); // long long int filepos = d_file.tellg(); // //Logger::message("FILEPOS: ", filepos); // if (d_verbose) [[unlikely]] // Logger::message("Getting frame at filepos: ", filepos, " (COUNTER: ", d_counter, ")"); // if (static_cast(filepos) == d_filesize) [[unlikely]] // { // Logger::message("Read entire backup file..."); // return; // } // if (d_headerframe) // { // std::unique_ptr frame(d_headerframe.release()); // Logger::message("Headerframe"); // continue; // } // uint32_t encryptedframelength = getNextFrameBlockSize(); // //if (encryptedframelength > 3145728/*= 3MB*/ /*115343360 / * =110MB*/ || encryptedframelength < 11) // //{ // // Logger::message("Suspicious framelength"); // // bruteForceFrom(filepos)??? // //} // if (encryptedframelength == 0 && d_file.eof()) [[unlikely]] // { // Logger::error("Unexpectedly hit end of file!"); // return; // } // DEBUGOUT("Framelength: ", encryptedframelength); // if (d_verbose) [[unlikely]] // Logger::message("Framelength: ", encryptedframelength); // std::unique_ptr encryptedframe(new unsigned char[encryptedframelength]); // if (encryptedframelength > 115343360 /*110MB*/ || encryptedframelength < 11 || !getNextFrameBlock(encryptedframe.get(), encryptedframelength)) [[unlikely]] // { // Logger::message("Failed to read next frame (", encryptedframelength, " bytes at filepos ", filepos, ")"); // return; // } // // check hash // unsigned int digest_size = SHA256_DIGEST_LENGTH; // unsigned char hash[SHA256_DIGEST_LENGTH]; // HMAC(EVP_sha256(), d_mackey, d_mackey_size, encryptedframe.get(), encryptedframelength - MACSIZE, hash, &digest_size); // if (std::memcmp(encryptedframe.get() + (encryptedframelength - MACSIZE), hash, 10) != 0) [[unlikely]] // { // Logger::warning("Bad MAC in frame: theirMac: ", bepaald::bytesToHexString(encryptedframe.get() + (encryptedframelength - MACSIZE), MACSIZE)); // Logger::warning_indent(" ourMac: ", bepaald::bytesToHexString(hash, SHA256_DIGEST_LENGTH)); // d_badmac = true; // } // else // { // macs_and_positions->emplace_back(std::make_pair(new unsigned char[SHA256_DIGEST_LENGTH], filepos)); // std::memcpy(macs_and_positions->back().first.get(), hash, SHA256_DIGEST_LENGTH); // d_badmac = false; // if (d_verbose) [[unlikely]] // { // Logger::message("Calculated mac: ", bepaald::bytesToHexString(hash, SHA256_DIGEST_LENGTH)); // Logger::message("Mac in file : ", bepaald::bytesToHexString(encryptedframe.get() + (encryptedframelength - MACSIZE), MACSIZE)); // } // } // // decode // uintToFourBytes(d_iv, d_counter++); // // create context // std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); // // disable padding // EVP_CIPHER_CTX_set_padding(ctx.get(), 0); // if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr, d_cipherkey, d_iv) != 1) [[unlikely]] // { // Logger::message("CTX INIT FAILED"); // return; // } // int decodedframelength = encryptedframelength - MACSIZE; // unsigned char *decodedframe = new unsigned char[decodedframelength]; // if (EVP_DecryptUpdate(ctx.get(), decodedframe, &decodedframelength, encryptedframe.get(), encryptedframelength - MACSIZE) != 1) [[unlikely]] // { // Logger::message("Failed to decrypt data"); // delete[] decodedframe; // return; // } // delete[] encryptedframe.release(); // free up already.... // std::unique_ptr frame(initBackupFrame(decodedframe, decodedframelength, d_framecount++)); // if (!frame) [[unlikely]] // { // Logger::message("Failed to get valid frame from decoded data..."); // if (d_badmac) // { // Logger::message("Encrypted data had failed verification (Bad MAC)"); // delete[] decodedframe; // return; // } // else // { // Logger::message("Data was verified ok, but does not represent a valid frame... Don't know what happened, but it's bad... :("); // Logger::message("Decrypted frame data: ", bepaald::bytesToHexString(decodedframe, decodedframelength)); // delete[] decodedframe; // return; // } // delete[] decodedframe; // return; // } // delete[] decodedframe; // uint32_t attsize = frame->attachmentSize(); // if (!d_badmac && attsize > 0 && // (frame->frameType() == BackupFrame::FRAMETYPE::ATTACHMENT || // frame->frameType() == BackupFrame::FRAMETYPE::AVATAR || // frame->frameType() == BackupFrame::FRAMETYPE::STICKER)) // { // if ((d_file.tellg() < 0 && d_file.eof()) || (attsize + static_cast(d_file.tellg()) > d_filesize)) [[ unlikely ]] // if (!d_assumebadframesize) // { // Logger::error("Unexpectedly hit end of file while reading attachment!"); // return; // } // uintToFourBytes(d_iv, d_counter++); // reinterpret_cast(frame.get())->setLazyData(d_iv, d_iv_size, d_mackey, d_mackey_size, d_cipherkey, d_cipherkey_size, attsize, d_filename, d_file.tellg()); // d_file.seekg(attsize + MACSIZE, std::ios_base::cur); // /* // if (!d_lazyload) // immediately decrypt i guess... // { // if (d_verbose) [[unlikely]] // Logger::message("Getting attachment at file pos ", d_file.tellg(), " (size: ", attsize, ")"); // int getatt = getAttachment(reinterpret_cast(frame.get())); // 0 == good, >0 == bad, <0 == bad+badmac // if (getatt > 0) // { // Logger::message("Failed to get attachment data for FrameWithAttachment... info:"); // frame->printInfo(); // return; // } // if (getatt < 0) // { // d_badmac = true; // if (d_stoponerror) // { // Logger::message("Stop reading backup. Next frame would be read at offset ", filepos + encryptedframelength); // return; // } // if (d_assumebadframesize) // { // std::unique_ptr f = bruteForceFrom(filepos, encryptedframelength); // //long long int curfilepos = d_file.tellg(); // //Logger::message("curpso: ", curfilepos); // //Logger::message("ATTACHMENT LENGTH SHOULD HAVE BEEN: ", curfilepos - filepos - encryptedframelength - MACSIZE); // return; // } // } // } // */ // } // } // } // void FileDecryptor::strugee3(uint64_t pos) // { // std::vector, uint64_t>> macs_and_positions; // strugee3Helper(&macs_and_positions); // Logger::message("Got macs: "); // //for (unsigned int i = 0; i < macs_and_positions.size(); ++i) // // Logger::message(macs_and_positions[i].second, " : ", bepaald::bytesToHexString(macs_and_positions[i].first.get(), SHA256_DIGEST_LENGTH)); // unsigned int offset = 0; // d_file.seekg(pos, std::ios_base::beg); // Logger::message("Getting frame at filepos: ", d_file.tellg()); // if (static_cast(d_file.tellg()) == d_filesize) // { // Logger::message("Read entire backup file..."); // return; // } // uint32_t encryptedframelength = getNextFrameBlockSize(); // if (encryptedframelength > 3145728/*= 3MB*/ /*115343360 / * =110MB*/ || encryptedframelength < 11) // { // Logger::message("Framesize too big to be real"); // return; // } // std::unique_ptr encryptedframe(new unsigned char[encryptedframelength]); // if (!getNextFrameBlock(encryptedframe.get(), encryptedframelength)) // return; // // check hash // unsigned int digest_size = SHA256_DIGEST_LENGTH; // unsigned char hash[SHA256_DIGEST_LENGTH]; // HMAC(EVP_sha256(), d_mackey, d_mackey_size, encryptedframe.get(), encryptedframelength - MACSIZE, hash, &digest_size); // if (std::memcmp(encryptedframe.get() + (encryptedframelength - MACSIZE), hash, MACSIZE) != 0) // { // Logger::message("BAD MAC!"); // return; // } // else // { // Logger::message(""); // Logger::message("GOT GOOD MAC AT OFFSET ", offset, " BYTES!"); // Logger::message("Now let's try and find out how many frames we skipped to get here...."); // d_badmac = false; // } // Logger::message("Got GOOD MAC : ", bepaald::bytesToHexString(hash, SHA256_DIGEST_LENGTH)); // for (unsigned int i = 0; i < macs_and_positions.size(); ++i) // { // if (std::memcmp(macs_and_positions[i].first.get(), hash, SHA256_DIGEST_LENGTH) == 0) // { // Logger::message("SAME MAC AT POS: ", macs_and_positions[i].second); // int const size = 200; // unsigned char bytes[size]; // d_file.seekg(macs_and_positions[i].second); // d_file.read(reinterpret_cast(bytes), size); // Logger::message("200 bytes at file position ", macs_and_positions[i].second, ": \n", bepaald::bytesToHexString(bytes, size)); // d_file.seekg(pos); // d_file.read(reinterpret_cast(bytes), size); // Logger::message("200 bytes at file position ", pos, ": \n", bepaald::bytesToHexString(bytes, size)); // } // } // } // void FileDecryptor::ashmorgan() // { // unsigned int offset = 0; // d_file.seekg(d_filesize - 100, std::ios_base::beg); // std::unique_ptr cbytes(new unsigned char[100]); // d_file.read(reinterpret_cast(cbytes.get()), 100); // Logger::message("\nLast 100 bytes: ", bepaald::bytesToHexString(cbytes.get(), 100), "\n"); // cbytes.release(); // d_file.seekg(d_filesize - 16, std::ios_base::beg); // Logger::message("Getting frame at filepos: ", d_file.tellg()); // if (static_cast(d_file.tellg()) == d_filesize) // { // Logger::message("Read entire backup file..."); // return; // } // uint32_t encryptedframelength = getNextFrameBlockSize(); // if (encryptedframelength > 3145728/*= 3MB*/ /*115343360 / * =110MB*/ || encryptedframelength < 11) // { // Logger::message("Framesize too big to be real"); // return; // } // std::unique_ptr encryptedframe(new unsigned char[encryptedframelength]); // if (!getNextFrameBlock(encryptedframe.get(), encryptedframelength)) // return; // Logger::message("FRAME: ", bepaald::bytesToHexString(encryptedframe.get(), encryptedframelength)); // // check hash // unsigned int digest_size = SHA256_DIGEST_LENGTH; // unsigned char hash[SHA256_DIGEST_LENGTH]; // HMAC(EVP_sha256(), d_mackey, d_mackey_size, encryptedframe.get(), encryptedframelength - MACSIZE, hash, &digest_size); // if (std::memcmp(encryptedframe.get() + (encryptedframelength - MACSIZE), hash, MACSIZE) != 0) // { // Logger::message("BAD MAC!"); // return; // } // else // { // Logger::message(""); // Logger::message("GOT GOOD MAC AT OFFSET ", offset, " BYTES! ", bepaald::bytesToHexString(hash, digest_size)); // Logger::message("Now let's try and find out how many frames we skipped to get here...."); // d_badmac = false; // } // // decode // unsigned int skipped = 0; // std::unique_ptr frame(nullptr); // while (true) // { // if (frame) // frame.reset(); // if (skipped > 1000000) // a frame is at least 10 bytes? -> could probably safely set this higher. MAC alone is 10 bytes, there is also actual data // { // Logger::message("TESTED 1000000 frames"); // return; // } // if (skipped % 100 == 0) // Logger::message_overwrite("\rChecking if we skipped ", skipped, " frames... "); // uintToFourBytes(d_iv, d_counter + skipped); // // create context // std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); // // disable padding // EVP_CIPHER_CTX_set_padding(ctx.get(), 0); // if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr, d_cipherkey, d_iv) != 1) // { // Logger::message("CTX INIT FAILED"); // return; // } // int decodedframelength = encryptedframelength - MACSIZE; // unsigned char *decodedframe = new unsigned char[decodedframelength]; // if (EVP_DecryptUpdate(ctx.get(), decodedframe, &decodedframelength, encryptedframe.get(), encryptedframelength - MACSIZE) != 1) // { // Logger::message("Failed to decrypt data"); // delete[] decodedframe; // return; // } // DEBUGOUT("Decoded hex : ", bepaald::bytesToHexString(decodedframe, decodedframelength)); // frame.reset(initBackupFrame(decodedframe, decodedframelength, d_framecount + skipped)); // ++skipped; // if (!frame) // { // Logger::message_overwrite("\rChecking if we skipped ", skipped, " frames... nope! :("); // //if (skipped > // } // else // { // if (frame->validate() && // frame->frameType() != BackupFrame::FRAMETYPE::HEADER && // it is impossible to get in this function without the headerframe, and there is only one // (frame->frameType() != BackupFrame::FRAMETYPE::END || static_cast(d_file.tellg()) == d_filesize)) // { // d_counter += skipped; // d_framecount += skipped; // Logger::message_overwrite("\rChecking if we skipped ", skipped, " frames... YEAH! :)", Logger::Control::ENDOVERWRITE); // Logger::message("Good frame: ", frame->frameNumber(), " (", frame->frameTypeString(), ")"); // Logger::message("COUNTER: ", d_counter); // Logger::message("Decoded hex : ", bepaald::bytesToHexString(decodedframe, decodedframelength)); // frame->printInfo(); // //delete[] encryptedframe.release(); // frame.reset(); // delete[] decodedframe; // return; // } // Logger::message_overwrite("\rChecking if we skipped ", skipped, " frames... nope! :("); // frame.reset(); // } // delete[] decodedframe; // } // //frame->printInfo(); // //Logger::message("HEADERTYPE: ", frame->frameType()); // uint32_t attsize = 0; // if (!d_badmac && (attsize = frame->attachmentSize()) > 0 && // (frame->frameType() == BackupFrame::FRAMETYPE::ATTACHMENT || // frame->frameType() == BackupFrame::FRAMETYPE::AVATAR || // frame->frameType() == BackupFrame::FRAMETYPE::STICKER)) // { // if (d_verbose) [[unlikely]] // Logger::message("Trying to read attachment (bruteforce)"); // uintToFourBytes(d_iv, d_counter++); // reinterpret_cast(frame.get())->setLazyData(d_iv, d_iv_size, d_mackey, d_mackey_size, d_cipherkey, d_cipherkey_size, attsize, d_filename, d_file.tellg()); // d_file.seekg(attsize + MACSIZE, std::ios_base::cur); // /* // if (!d_lazyload) // immediately decrypt i guess... // { // if (d_verbose) [[unlikely]] // Logger::message("Getting attachment at file pos ", d_file.tellg(), " (size: ", attsize, ")"); // int getatt = getAttachment(reinterpret_cast(frame.get())); // if (getatt != 0) // { // if (getatt < 0) // d_badmac = true; // return; // } // } // */ // } // } signalbackup-tools-20250313-1/filedecryptor/filedecryptor.cc000066400000000000000000000135121476450434500237650ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "filedecryptor.ih" #include "../common_filesystem.h" FileDecryptor::FileDecryptor(std::string const &filename, std::string const &passphrase, bool verbose, bool stoponerror, bool assumebadframesize, std::vector const &editattachments) : CryptBase(verbose), d_headerframe(nullptr), d_filename(filename), d_framecount(0), d_filesize(0), d_badmac(false), d_assumebadframesize(assumebadframesize), d_editattachments(editattachments), d_stoponerror(stoponerror), d_backupfileversion(0) { std::ifstream file(d_filename, std::ios_base::binary | std::ios_base::in); if (!file.is_open()) { Logger::error("Failed to open file '", d_filename, "'"); return; } //file.seekg(0, std::ios_base::end); //d_filesize = file.tellg(); //file.seekg(0); d_filesize = bepaald::fileSize(d_filename); // read first four bytes, they are the header size of the file: int32_t headerlength = getNextFrameBlockSize(file); DEBUGOUT("headerlength: ", headerlength); if (headerlength == 0) { Logger::error("got got length of headerframe == 0"); return; } unsigned char *headerdata = new unsigned char[headerlength]; getNextFrameBlock(file, headerdata, headerlength); BackupFrame *headerframe = initBackupFrame(headerdata, headerlength, d_framecount++); delete[] headerdata; if (!headerframe) { //std::cout << "Error: failed to retrieve HeaderFrame, length was " << headerlength << " bytes" << std::endl; Logger::error("failed to retrieve HeaderFrame, length was ", headerlength, " bytes"); return; } if (!(headerframe->frameType() == BackupFrame::FRAMETYPE::HEADER)) { //std::cout << "Error: First frame is not a HeaderFrame" << std::endl; Logger::error("First frame is not a HeaderFrame"); delete headerframe; return; } //reinterpret_cast(headerframe); if (!dynamic_cast(headerframe)->iv()) { delete headerframe; return; } d_iv_size = reinterpret_cast(headerframe)->iv_length(); d_iv = new unsigned char[d_iv_size]; std::memcpy(d_iv, reinterpret_cast(headerframe)->iv(), d_iv_size); d_counter = fourBytesToUint(d_iv); d_salt_size = reinterpret_cast(headerframe)->salt_length(); d_salt = new unsigned char[d_salt_size]; std::memcpy(d_salt, reinterpret_cast(headerframe)->salt(), d_salt_size); if (!getBackupKey(passphrase)) { //std::cout << "Error: Failed to get backupkey from passphrase" << std::endl; Logger::error("Failed to get backupkey from passphrase"); delete headerframe; return; } if (!getCipherAndMac(32, 64)) { //std::cout << "Error: Failed to get Cipher and Mac" << std::endl; Logger::error("Failed to get Cipher and Mac"); delete headerframe; return; } d_backupfileversion = reinterpret_cast(headerframe)->version(); //headerframe->printInfo(); /* DEBUGOUT("IV: ", bepaald::bytesToHexString(d_iv, d_iv_size)); DEBUGOUT("SALT: ", bepaald::bytesToHexString(d_salt, d_salt_size)); DEBUGOUT("BACKUPKEY: ", bepaald::bytesToHexString(d_backupkey, d_backupkey_size)); DEBUGOUT("CIPHERKEY: ", bepaald::bytesToHexString(d_cipherkey, d_cipherkey_size)); DEBUGOUT("MACKEY: ", bepaald::bytesToHexString(d_mackey, d_mackey_size)); DEBUGOUT("BACKUPFILE VERSION: ", d_backupfileversion); DEBUGOUT("BACKUPFILE SIZE: ", d_filesize); DEBUGOUT("COUNTER: ", d_counter); */ if (d_verbose) [[unlikely]] { // std::cout << "IV: " << bepaald::bytesToHexString(d_iv, d_iv_size) << " (size: " << d_iv_size << ")" << std::endl; // std::cout << "SALT: " << bepaald::bytesToHexString(d_salt, d_salt_size) << " (size: " << d_salt_size << ")" << std::endl; // std::cout << "BACKUPKEY: " << bepaald::bytesToHexString(d_backupkey, d_backupkey_size) << " (size: " << d_backupkey_size << ")" << std::endl; // std::cout << "CIPHERKEY: " << bepaald::bytesToHexString(d_cipherkey, d_cipherkey_size) << " (size: " << d_cipherkey_size << ")" << std::endl; // std::cout << "MACKEY: " << bepaald::bytesToHexString(d_mackey, d_mackey_size) << " (size: " << d_mackey_size << ")" << std::endl; Logger::message("IV: ", bepaald::bytesToHexString(d_iv, d_iv_size), " (size: ", d_iv_size, ")"); Logger::message("SALT: ", bepaald::bytesToHexString(d_salt, d_salt_size), " (size: ", d_salt_size, ")"); Logger::message("BACKUPKEY: ", bepaald::bytesToHexString(d_backupkey, d_backupkey_size), " (size: ", d_backupkey_size, ")"); Logger::message("CIPHERKEY: ", bepaald::bytesToHexString(d_cipherkey, d_cipherkey_size), " (size: ", d_cipherkey_size, ")" ); Logger::message("MACKEY: ", bepaald::bytesToHexString(d_mackey, d_mackey_size), " (size: ", d_mackey_size, ")"); } // std::cout << "BACKUPFILE VERSION: " << d_backupfileversion << std::endl; // std::cout << "BACKUPFILE SIZE: " << d_filesize << std::endl; // std::cout << "COUNTER: " << d_counter << std::endl; Logger::message("BACKUPFILE VERSION: ", d_backupfileversion); Logger::message("BACKUPFILE SIZE: ", d_filesize); Logger::message("COUNTER: ", d_counter); d_headerframe.reset(headerframe); d_ok = true; } signalbackup-tools-20250313-1/filedecryptor/filedecryptor.h000066400000000000000000000144171476450434500236340ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef FILEDECRYPTOR_H_ #define FILEDECRYPTOR_H_ #include #include #include "../common_be.h" #include "../backupframe/backupframe.h" #include "../framewithattachment/framewithattachment.h" #include "../cryptbase/cryptbase.h" #include "../invalidframe/invalidframe.h" #include "../logger/logger.h" class FileDecryptor : public CryptBase { std::unique_ptr d_headerframe; std::string d_filename; uint64_t d_framecount; uint64_t d_filesize; bool d_badmac; bool d_assumebadframesize; std::vector d_editattachments; bool d_stoponerror; uint32_t d_backupfileversion; public: FileDecryptor(std::string const &filename, std::string const &passphrase, bool verbose, bool stoponerror = false, bool assumebadframesize = false, std::vector const &editattachments = std::vector()); inline FileDecryptor(FileDecryptor const &other); inline FileDecryptor &operator=(FileDecryptor const &other); inline FileDecryptor(FileDecryptor &&other); inline FileDecryptor &operator=(FileDecryptor &&other); std::unique_ptr getFrameOld(std::ifstream &file); std::unique_ptr getFrame(std::ifstream &file); inline uint64_t total() const; inline bool badMac() const; // temporary /* CUSTOMS */ // void ashmorgan(std::ifstream &file); // void strugee(std::ifstream &file, uint64_t pos); // std::unique_ptr getFrameStrugee2(std::ifstream &file); // void strugee2(std::ifstream &file); // void strugee3Helper(std::ifstream &file, // std::vector, uint64_t>> *macs_and_positions); // void strugee3(std::ifstream &file, uint64_t pos); private: inline uint32_t getNextFrameBlockSize(std::ifstream &file); inline bool getNextFrameBlock(std::ifstream &file, unsigned char *data, size_t length); BackupFrame *initBackupFrame(unsigned char *data, size_t length, uint64_t count = 0) const; //virtual int getAttachment(FrameWithAttachment *frame) override; std::unique_ptr bruteForceFrom(std::ifstream &file, uint64_t filepos, uint32_t previousframelength); std::unique_ptr getFrameBrute(std::ifstream &file, uint64_t offset, uint32_t previousframelength); }; inline FileDecryptor::FileDecryptor(FileDecryptor const &other) : CryptBase(other), d_headerframe(nullptr), d_filename(other.d_filename), d_framecount(other.d_framecount), d_filesize(other.d_filesize), d_badmac(other.d_badmac), d_assumebadframesize(other.d_assumebadframesize), d_editattachments(other.d_editattachments), d_stoponerror(other.d_stoponerror), d_backupfileversion(other.d_backupfileversion) { d_ok = false; // headerfame... if (other.d_headerframe) d_headerframe.reset(other.d_headerframe->clone()); d_ok = other.d_ok; } inline FileDecryptor &FileDecryptor::operator=(FileDecryptor const &other) { if (this != &other) { CryptBase::operator=(other); if (other.d_headerframe) d_headerframe.reset(other.d_headerframe->clone()); d_filename = other.d_filename; d_framecount = other.d_framecount; d_filesize = other.d_filesize; d_badmac = other.d_badmac; d_assumebadframesize = other.d_assumebadframesize; d_editattachments = other.d_editattachments; d_stoponerror = other.d_stoponerror; d_backupfileversion = other.d_backupfileversion; d_ok = other.d_ok; } return *this; } inline FileDecryptor::FileDecryptor(FileDecryptor &&other) : CryptBase(std::move(other)), d_headerframe(std::move(other.d_headerframe)), d_filename(std::move(other.d_filename)), d_framecount(std::move(other.d_framecount)), d_filesize(std::move(other.d_filesize)), d_badmac(std::move(other.d_badmac)), d_assumebadframesize(std::move(other.d_assumebadframesize)), d_editattachments(std::move(other.d_editattachments)), d_stoponerror(std::move(other.d_stoponerror)), d_backupfileversion(std::move(other.d_backupfileversion)) {} inline FileDecryptor &FileDecryptor::operator=(FileDecryptor &&other) { if (this != &other) { CryptBase::operator=(std::move(other)); d_headerframe = std::move(other.d_headerframe); d_filename = std::move(other.d_filename); d_framecount = std::move(other.d_framecount); d_filesize = std::move(other.d_filesize); d_badmac = std::move(other.d_badmac); d_assumebadframesize = std::move(other.d_assumebadframesize); d_editattachments = std::move(other.d_editattachments); d_stoponerror = std::move(other.d_stoponerror); d_backupfileversion = std::move(other.d_backupfileversion); } return *this; } inline uint64_t FileDecryptor::total() const { return d_filesize; } inline bool FileDecryptor::badMac() const { return d_badmac; } // only used by getFrameOld(), used in older backups where frame length was not encrypted inline uint32_t FileDecryptor::getNextFrameBlockSize(std::ifstream &file) { uint32_t headerlength = 0; if (!file.read(reinterpret_cast(&headerlength), sizeof(decltype(headerlength)))) { Logger::error("Failed to read 4 bytes from file to get next frame size... (", file.tellg(), " / ", d_filesize, ")"); return 0; } return bepaald::swap_endian(headerlength); } // only used by getFrameOld(), used in older backups where frame length was not encrypted inline bool FileDecryptor::getNextFrameBlock(std::ifstream &file, unsigned char *data, size_t length) { //std::cout << "reading " << length << " bytes" << std::endl; if (!file.read(reinterpret_cast(data), length)) return false; return true; } #endif signalbackup-tools-20250313-1/filedecryptor/filedecryptor.ih000066400000000000000000000020541476450434500237770ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "filedecryptor.h" #include #include #include #include #include #include "../headerframe/headerframe.h" #include "../attachmentframe/attachmentframe.h" #include "../endframe/endframe.h" #include "../androidattachmentreader/androidattachmentreader.h" signalbackup-tools-20250313-1/filedecryptor/getframe.cc000066400000000000000000000461411476450434500227100ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "filedecryptor.ih" std::unique_ptr FileDecryptor::getFrame(std::ifstream &file) { if (d_backupfileversion == 0) [[unlikely]] return getFrameOld(file); unsigned long long int filepos = file.tellg(); if (d_verbose) [[unlikely]] Logger::message("Getting frame at filepos: ", filepos, " (COUNTER: ", d_counter, ")"); if (static_cast(filepos) == d_filesize) [[unlikely]] { if (d_verbose) [[unlikely]] Logger::message("Read entire backup file..."); return std::unique_ptr(nullptr); } if (d_headerframe) { file.seekg(4 + d_headerframe->dataSize()); return std::unique_ptr(d_headerframe.release()); } uint32_t encrypted_encryptedframelength = 0; if (!file.read(reinterpret_cast(&encrypted_encryptedframelength), sizeof(decltype(encrypted_encryptedframelength)))) [[unlikely]] { Logger::error("Failed to read ", sizeof(decltype(encrypted_encryptedframelength)), " bytes from file to get next frame size... (", file.tellg(), " / ", d_filesize, ")"); return std::unique_ptr(nullptr); } // set up context for caclulating MAC #if OPENSSL_VERSION_NUMBER >= 0x30000000L std::unique_ptr mac(EVP_MAC_fetch(nullptr, "hmac", nullptr), &::EVP_MAC_free); std::unique_ptr hctx(EVP_MAC_CTX_new(mac.get()), &::EVP_MAC_CTX_free); char digest[] = "SHA256"; OSSL_PARAM params[] = {OSSL_PARAM_construct_utf8_string("digest", digest, 0), OSSL_PARAM_construct_end()}; #else std::unique_ptr hctx(HMAC_CTX_new(), &::HMAC_CTX_free); #endif #if OPENSSL_VERSION_NUMBER >= 0x30000000L if (EVP_MAC_init(hctx.get(), d_mackey, d_mackey_size, params) != 1) [[unlikely]] #else if (HMAC_Init_ex(hctx.get(), d_mackey, d_mackey_size, EVP_sha256(), nullptr) != 1) [[unlikely]] #endif { Logger::error("Failed to initialize HMAC context"); return std::unique_ptr(nullptr); } // update MAC with frame length #if OPENSSL_VERSION_NUMBER >= 0x30000000L if (EVP_MAC_update(hctx.get(), reinterpret_cast(&encrypted_encryptedframelength), sizeof(decltype(encrypted_encryptedframelength))) != 1) [[unlikely]] #else if (HMAC_Update(hctx.get(), reinterpret_cast(&encrypted_encryptedframelength), sizeof(decltype(encrypted_encryptedframelength))) != 1) [[unlikely]] #endif { Logger::error("Failed to update HMAC"); return std::unique_ptr(nullptr); } // decrypt encrypted_encryptedframelength uintToFourBytes(d_iv, d_counter++); // create context std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); // disable padding EVP_CIPHER_CTX_set_padding(ctx.get(), 0); if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr, d_cipherkey, d_iv) != 1) [[unlikely]] { Logger::error("CTX INIT FAILED"); return nullptr; } uint32_t encryptedframelength = 0; int encryptedframelength_size = sizeof(decltype(encryptedframelength)); if (EVP_DecryptUpdate(ctx.get(), reinterpret_cast(&encryptedframelength), &encryptedframelength_size, reinterpret_cast(&encrypted_encryptedframelength), sizeof(decltype(encrypted_encryptedframelength))) != 1) [[unlikely]] { Logger::error("Failed to decrypt data"); return nullptr; } encryptedframelength = bepaald::swap_endian(encryptedframelength); if (d_verbose) [[unlikely]] Logger::message("Framelength: ", encryptedframelength); if (encryptedframelength > 115343360 /*110MB*/ || encryptedframelength < 11) { Logger::error("Failed to read next frame (", encryptedframelength, " bytes at filepos ", filepos, ")"); if (d_framecount == 1) Logger::message(Logger::Control::BOLD, " *** NOTE : IT IS LIKELY AN INCORRECT PASSPHRASE WAS PROVIDED ***", Logger::Control::NORMAL); return std::unique_ptr(nullptr); } // read in the encrypted frame data std::unique_ptr encryptedframe(new unsigned char[encryptedframelength]); if (!getNextFrameBlock(file, encryptedframe.get(), encryptedframelength)) [[unlikely]] { Logger::error("Failed to read next frame (", encryptedframelength, " bytes at filepos ", filepos, ")"); return std::unique_ptr(nullptr); } // update MAC with read data #if OPENSSL_VERSION_NUMBER >= 0x30000000L if (EVP_MAC_update(hctx.get(), encryptedframe.get(), encryptedframelength - MACSIZE) != 1) #else if (HMAC_Update(hctx.get(), encryptedframe.get(), encryptedframelength - MACSIZE) != 1) #endif { Logger::error("Failed to update HMAC"); return std::unique_ptr(nullptr); } // finalize MAC #if OPENSSL_VERSION_NUMBER >= 0x30000000L unsigned long int digest_size = SHA256_DIGEST_LENGTH; unsigned char hash[SHA256_DIGEST_LENGTH]; if (EVP_MAC_final(hctx.get(), hash, nullptr, digest_size) != 1) #else unsigned int digest_size = SHA256_DIGEST_LENGTH; unsigned char hash[SHA256_DIGEST_LENGTH]; if (HMAC_Final(hctx.get(), hash, &digest_size) != 1) #endif { Logger::error("Failed to finalize MAC"); return std::unique_ptr(nullptr); } // check MAC if (std::memcmp(encryptedframe.get() + (encryptedframelength - MACSIZE), hash, 10) != 0) [[unlikely]] { Logger::message("\n"); Logger::warning("Bad MAC in frame: theirMac: ", bepaald::bytesToHexString(encryptedframe.get() + (encryptedframelength - MACSIZE), MACSIZE), "\n ourMac: ", bepaald::bytesToHexString(hash, SHA256_DIGEST_LENGTH)); if (d_framecount == 1) [[unlikely]] Logger::message(Logger::Control::BOLD, " *** NOTE : IT IS LIKELY AN INCORRECT PASSPHRASE WAS PROVIDED ***", Logger::Control::NORMAL); d_badmac = true; return std::unique_ptr(nullptr); } else { d_badmac = false; if (d_verbose) [[unlikely]] { Logger::message("Calculated mac: ", bepaald::bytesToHexString(hash, SHA256_DIGEST_LENGTH)); Logger::message("Mac in file : ", bepaald::bytesToHexString(encryptedframe.get() + (encryptedframelength - MACSIZE), MACSIZE)); } } // decode frame data int decodedframelength = encryptedframelength - MACSIZE; unsigned char *decodedframe = new unsigned char[decodedframelength]; if (EVP_DecryptUpdate(ctx.get(), decodedframe, &decodedframelength, encryptedframe.get(), encryptedframelength - MACSIZE) != 1) [[unlikely]] { Logger::error("Failed to decrypt data"); delete[] decodedframe; return std::unique_ptr(nullptr); } delete[] encryptedframe.release(); // free up already.... std::unique_ptr frame(initBackupFrame(decodedframe, decodedframelength, d_framecount++)); if (!d_editattachments.empty() && frame && frame->frameType() == BackupFrame::FRAMETYPE::ATTACHMENT) [[unlikely]] for (unsigned int i = 0; i < d_editattachments.size(); i += 2) if (frame->frameNumber() == static_cast(d_editattachments[i])) { auto oldlength = reinterpret_cast(frame.get())->length(); // set new length reinterpret_cast(frame.get())->setLengthField(d_editattachments[i + 1]); Logger::message("Editing attachment data size in AttachmentFrame ", oldlength, " -> ", reinterpret_cast(frame.get())->length(), "\nFrame has _id = ", reinterpret_cast(frame.get())->rowId(), ", unique_id = ", reinterpret_cast(frame.get())->attachmentId()); break; } if (!frame) [[unlikely]] { Logger::error("Failed to get valid frame from decoded data..."); if (d_badmac) { Logger::error_indent("Encrypted data had failed verification (Bad MAC)"); delete[] decodedframe; return std::make_unique(); } else { Logger::error_indent("Data was verified ok, but does not represent a valid frame... Don't know what happened, but it's bad... :("); Logger::error_indent("Decrypted frame data: ", bepaald::bytesToHexString(decodedframe, decodedframelength)); delete[] decodedframe; return std::make_unique(); } delete[] decodedframe; return std::unique_ptr(nullptr); } delete[] decodedframe; // if (!frame->validate(d_filesize - file.tellg())) // { // std::cout << std::endl << " ************** FRAME NOT VALIDATED ****************" << std::endl; // frame->printInfo(); // std::cout << "TOTAL SIZE: " << d_filesize << std::endl; // std::cout << "POSITION : " << file.tellg() << std::endl; // std::cout << "AVAILABLE : " << (d_filesize - file.tellg()) << std::endl; // } uint32_t attsize = frame->attachmentSize(); if (!d_badmac && attsize > 0 && (frame->frameType() == BackupFrame::FRAMETYPE::ATTACHMENT || frame->frameType() == BackupFrame::FRAMETYPE::AVATAR || frame->frameType() == BackupFrame::FRAMETYPE::STICKER)) { if ((file.tellg() < 0 && file.eof()) || (attsize + static_cast(file.tellg()) > d_filesize)) [[unlikely]] if (!d_assumebadframesize) { Logger::error("Unexpectedly hit end of file while reading attachment!"); return std::unique_ptr(nullptr); } uintToFourBytes(d_iv, d_counter++); reinterpret_cast(frame.get())->setReader(new AndroidAttachmentReader(d_iv, d_iv_size, d_mackey, d_mackey_size, d_cipherkey, d_cipherkey_size, attsize, d_filename, file.tellg())); file.seekg(attsize + MACSIZE, std::ios_base::cur); } //std::cout << "FILEPOS: " << file.tellg() << std::endl; return frame; } // old style, where frame length was not encrypted std::unique_ptr FileDecryptor::getFrameOld(std::ifstream &file) { unsigned long long int filepos = file.tellg(); if (d_verbose) [[unlikely]] Logger::message("Getting frame at filepos: ", filepos, " (COUNTER: ", d_counter, ")"); if (static_cast(filepos) == d_filesize) [[unlikely]] { Logger::message("Read entire backup file..."); return std::unique_ptr(nullptr); } if (d_headerframe) { file.seekg(4 + d_headerframe->dataSize()); return std::unique_ptr(d_headerframe.release()); } uint32_t encryptedframelength = getNextFrameBlockSize(file); //if (encryptedframelength > 3145728/*= 3MB*/ /*115343360 / * =110MB*/ || encryptedframelength < 11) //{ // std::cout << "Suspicious framelength" << std::endl; // bruteForceFrom(filepos)??? //} if (encryptedframelength == 0 && file.eof()) [[unlikely]] { Logger::error("Unexpectedly hit end of file!"); return std::unique_ptr(nullptr); } DEBUGOUT("Framelength: ", encryptedframelength); if (d_verbose) [[unlikely]] Logger::message("Framelength: ", encryptedframelength); if (encryptedframelength > 115343360 /*110MB*/ || encryptedframelength < 11) { Logger::error("Failed to read next frame (", encryptedframelength, " bytes at filepos ", filepos, ")"); if (d_stoponerror || d_backupfileversion > 0) return std::unique_ptr(nullptr); return bruteForceFrom(file, filepos, encryptedframelength); } std::unique_ptr encryptedframe(new unsigned char[encryptedframelength]); if (!getNextFrameBlock(file, encryptedframe.get(), encryptedframelength)) [[unlikely]] { Logger::error("Failed to read next frame (", encryptedframelength, " bytes at filepos ", filepos, ")"); if (d_stoponerror || d_backupfileversion > 0) return std::unique_ptr(nullptr); return bruteForceFrom(file, filepos, encryptedframelength); } // check hash unsigned int digest_size = SHA256_DIGEST_LENGTH; unsigned char hash[SHA256_DIGEST_LENGTH]; HMAC(EVP_sha256(), d_mackey, d_mackey_size, encryptedframe.get(), encryptedframelength - MACSIZE, hash, &digest_size); if (std::memcmp(encryptedframe.get() + (encryptedframelength - MACSIZE), hash, 10) != 0) [[unlikely]] { Logger::message("\n"); Logger::warning("Bad MAC in frame: theirMac: ", bepaald::bytesToHexString(encryptedframe.get() + (encryptedframelength - MACSIZE), MACSIZE), "\n ourMac: ", bepaald::bytesToHexString(hash, SHA256_DIGEST_LENGTH)); // std::cout << "" << std::endl; // std::cout << bepaald::bold_on << "WARNING" << bepaald::bold_off << " : Bad MAC in frame: theirMac: " // << bepaald::bytesToHexString(encryptedframe.get() + (encryptedframelength - MACSIZE), MACSIZE) << std::endl; // std::cout << " ourMac: " << bepaald::bytesToHexString(hash, SHA256_DIGEST_LENGTH) << std::endl; if (d_framecount == 1) [[unlikely]] Logger::message(Logger::Control::BOLD, " *** NOTE : IT IS LIKELY AN INCORRECT PASSPHRASE WAS PROVIDED ***", Logger::Control::NORMAL); //std::cout << bepaald::bold_on << " *** NOTE : IT IS LIKELY AN INCORRECT PASSPHRASE WAS PROVIDED ***" << bepaald::bold_off << std::endl; d_badmac = true; if (d_stoponerror) { Logger::message("Stop reading backup. Next frame would be read at offset ", filepos + encryptedframelength); return std::unique_ptr(nullptr); } } else { d_badmac = false; if (d_verbose) [[unlikely]] { Logger::message("Calculated mac: ", bepaald::bytesToHexString(hash, SHA256_DIGEST_LENGTH)); Logger::message("Mac in file : ", bepaald::bytesToHexString(encryptedframe.get() + (encryptedframelength - MACSIZE), MACSIZE)); } } uintToFourBytes(d_iv, d_counter++); // create context std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); // disable padding EVP_CIPHER_CTX_set_padding(ctx.get(), 0); if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr, d_cipherkey, d_iv) != 1) [[unlikely]] { Logger::error("CTX INIT FAILED"); return std::unique_ptr(nullptr); } int decodedframelength = encryptedframelength - MACSIZE; unsigned char *decodedframe = new unsigned char[decodedframelength]; if (EVP_DecryptUpdate(ctx.get(), decodedframe, &decodedframelength, encryptedframe.get(), encryptedframelength - MACSIZE) != 1) [[unlikely]] { Logger::error("Failed to decrypt data"); delete[] decodedframe; return std::unique_ptr(nullptr); } delete[] encryptedframe.release(); // free up already.... std::unique_ptr frame(initBackupFrame(decodedframe, decodedframelength, d_framecount++)); if (!d_editattachments.empty() && frame && frame->frameType() == BackupFrame::FRAMETYPE::ATTACHMENT) [[unlikely]] for (unsigned int i = 0; i < d_editattachments.size(); i += 2) if (frame->frameNumber() == static_cast(d_editattachments[i])) { auto oldlength = reinterpret_cast(frame.get())->length(); // set new length reinterpret_cast(frame.get())->setLengthField(d_editattachments[i + 1]); Logger::message("Editing attachment data size in AttachmentFrame ", oldlength, " -> ", reinterpret_cast(frame.get())->length(), "\nFrame has _id = ", reinterpret_cast(frame.get())->rowId(), ", unique_id = ", reinterpret_cast(frame.get())->attachmentId()); // std::cout << "Editing attachment data size in AttachmentFrame " // << reinterpret_cast(frame.get())->length() << " -> "; // reinterpret_cast(frame.get())->setLengthField(d_editattachments[i + 1]); // std::cout << reinterpret_cast(frame.get())->length() << std::endl; // std::cout << "Frame has _id = " << reinterpret_cast(frame.get())->rowId() // << ", unique_id = " << reinterpret_cast(frame.get())->attachmentId() << std::endl; break; } if (!frame) [[unlikely]] { Logger::error("Failed to get valid frame from decoded data..."); if (d_badmac) { Logger::error_indent("Encrypted data had failed verification (Bad MAC)"); delete[] decodedframe; return bruteForceFrom(file, filepos, encryptedframelength); } else { Logger::error_indent("Data was verified ok, but does not represent a valid frame... Don't know what happened, but it's bad... :("); Logger::error_indent("Decrypted frame data: ", bepaald::bytesToHexString(decodedframe, decodedframelength)); delete[] decodedframe; return std::make_unique(); } delete[] decodedframe; return std::unique_ptr(nullptr); } delete[] decodedframe; // if (!frame->validate(d_filesize - file.tellg())) // { // std::cout << std::endl << " ************** FRAME NOT VALIDATED ****************" << std::endl; // frame->printInfo(); // std::cout << "TOTAL SIZE: " << d_filesize << std::endl; // std::cout << "POSITION : " << file.tellg() << std::endl; // std::cout << "AVAILABLE : " << (d_filesize - file.tellg()) << std::endl; // } uint32_t attsize = frame->attachmentSize(); if (!d_badmac && attsize > 0 && (frame->frameType() == BackupFrame::FRAMETYPE::ATTACHMENT || frame->frameType() == BackupFrame::FRAMETYPE::AVATAR || frame->frameType() == BackupFrame::FRAMETYPE::STICKER)) { if ((file.tellg() < 0 && file.eof()) || (attsize + static_cast(file.tellg()) > d_filesize)) [[unlikely]] if (!d_assumebadframesize) { Logger::error("Unexpectedly hit end of file while reading attachment!"); return std::unique_ptr(nullptr); } uintToFourBytes(d_iv, d_counter++); //reinterpret_cast(frame.get())->setLazyData(d_iv, d_iv_size, d_mackey, d_mackey_size, d_cipherkey, d_cipherkey_size, attsize, d_filename, file.tellg()); reinterpret_cast(frame.get())->setReader(new AndroidAttachmentReader(d_iv, d_iv_size, d_mackey, d_mackey_size, d_cipherkey, d_cipherkey_size, attsize, d_filename, file.tellg())); file.seekg(attsize + MACSIZE, std::ios_base::cur); } //std::cout << "FILEPOS: " << file.tellg() << std::endl; //delete frame; return frame; } signalbackup-tools-20250313-1/filedecryptor/getframebrute.cc000066400000000000000000000156231476450434500237530ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "filedecryptor.ih" std::unique_ptr FileDecryptor::bruteForceFrom(std::ifstream &file, uint64_t filepos, uint32_t previousframelength) { Logger::message("Starting bruteforcing offset to next valid frame... starting after: ", filepos); uint64_t skip = 1; std::unique_ptr ret(nullptr); while (filepos + skip < d_filesize) { file.clear(); if (skip % 10 == 0) Logger::message_overwrite("Checking offset ", skip, " bytes"); file.seekg(filepos + skip, std::ios_base::beg); ret.reset(getFrameBrute(file, skip++, previousframelength).release()); if (ret) { Logger::message("Got frame, breaking"); break; } } return ret; } std::unique_ptr FileDecryptor::getFrameBrute(std::ifstream &file, uint64_t offset, uint32_t previousframelength) { if (static_cast(file.tellg()) == d_filesize) { Logger::message("Read entire backup file..."); return std::unique_ptr(nullptr); } if (d_headerframe) { file.seekg(4 + d_headerframe->dataSize()); std::unique_ptr frame(d_headerframe.release()); return frame; } uint32_t encryptedframelength = getNextFrameBlockSize(file); if (encryptedframelength > 3145728/*= 3MB*/ /*115343360 / * =110MB*/ || encryptedframelength < 11) { //std::cout << "Framesize too big to be real" << std::endl; return std::unique_ptr(nullptr); } std::unique_ptr encryptedframe(new unsigned char[encryptedframelength]); if (!getNextFrameBlock(file, encryptedframe.get(), encryptedframelength)) return std::unique_ptr(nullptr); // check hash unsigned int digest_size = SHA256_DIGEST_LENGTH; unsigned char hash[SHA256_DIGEST_LENGTH]; HMAC(EVP_sha256(), d_mackey, d_mackey_size, encryptedframe.get(), encryptedframelength - MACSIZE, hash, &digest_size); if (std::memcmp(encryptedframe.get() + (encryptedframelength - MACSIZE), hash, MACSIZE) != 0) return std::unique_ptr(nullptr); else { Logger::message("\nGOT GOOD MAC AT OFFSET ", offset, " BYTES!"); Logger::message("Now let's try and find out how many frames we skipped to get here...."); d_badmac = false; } // decode unsigned int skippedframes = 0; std::unique_ptr frame(nullptr); while (!frame) { if (skippedframes > offset / 10) // a frame is at least 10 bytes? -> could probably safely set this higher. MAC alone is 10 bytes, there is also actual data { Logger::message("\nNo valid frame found at maximum frameskip for this offset..."); return std::unique_ptr(nullptr); } Logger::message_overwrite("Checking if we skipped ", skippedframes, " frames... "); uintToFourBytes(d_iv, d_counter + skippedframes); // create context std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); // disable padding EVP_CIPHER_CTX_set_padding(ctx.get(), 0); if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr, d_cipherkey, d_iv) != 1) { Logger::error("CTX INIT FAILED"); return std::unique_ptr(nullptr); } int decodedframelength = encryptedframelength - MACSIZE; unsigned char *decodedframe = new unsigned char[decodedframelength]; if (EVP_DecryptUpdate(ctx.get(), decodedframe, &decodedframelength, encryptedframe.get(), encryptedframelength - MACSIZE) != 1) { Logger::error("Failed to decrypt data"); delete[] decodedframe; return std::unique_ptr(nullptr); } DEBUGOUT("Decoded hex : ", bepaald::bytesToHexString(decodedframe, decodedframelength)); frame.reset(initBackupFrame(decodedframe, decodedframelength, d_framecount + skippedframes)); delete[] decodedframe; ++skippedframes; if (!frame) { Logger::message_overwrite("Checking if we skipped ", skippedframes, " frames... nope! :("); //if (skipped > } else { if (frame->validate(d_filesize - file.tellg()) && frame->frameType() != BackupFrame::FRAMETYPE::HEADER && // it is impossible to get in this function without the headerframe, and there is only one (frame->frameType() != BackupFrame::FRAMETYPE::END || static_cast(file.tellg()) == d_filesize)) { d_counter += skippedframes; d_framecount += skippedframes; Logger::message_overwrite("Checking if we skipped ", skippedframes, " frames... YEAH! :)", Logger::Control::ENDOVERWRITE); if (d_assumebadframesize && skippedframes == 1 /*NOTE, skippedframes was already upped*/) { Logger::message("\n ! CORRECT FRAME_NUMBER:SIZE = ", frame->frameNumber() - 1, ":", offset - previousframelength - MACSIZE - 4, "\n"); } Logger::message("Good frame at offset ", offset, ". Frame number: ", frame->frameNumber(), " (Type: ", frame->frameTypeString(), ")"); frame->printInfo(); delete[] encryptedframe.release(); break; } Logger::message_overwrite("Checking if we skipped ", skippedframes, " frames... nope! :("); frame.reset(); } } //frame->printInfo(); //std::cout << "HEADERTYPE: " << frame->frameType() << std::endl; uint32_t attsize = 0; if (!d_badmac && (attsize = frame->attachmentSize()) > 0 && (frame->frameType() == BackupFrame::FRAMETYPE::ATTACHMENT || frame->frameType() == BackupFrame::FRAMETYPE::AVATAR || frame->frameType() == BackupFrame::FRAMETYPE::STICKER)) { if (d_verbose) [[unlikely]] Logger::message("Trying to read attachment (bruteforce)"); uintToFourBytes(d_iv, d_counter++); //reinterpret_cast(frame.get())->setLazyData(d_iv, d_iv_size, d_mackey, d_mackey_size, d_cipherkey, d_cipherkey_size, attsize, d_filename, file.tellg()); reinterpret_cast(frame.get())->setReader(new AndroidAttachmentReader(d_iv, d_iv_size, d_mackey, d_mackey_size, d_cipherkey, d_cipherkey_size, attsize, d_filename, file.tellg())); file.seekg(attsize + MACSIZE, std::ios_base::cur); } //std::cout << "FILEPOS: " << d_file.tellg() << std::endl; //delete frame; return frame; } signalbackup-tools-20250313-1/filedecryptor/initbackupframe.cc000066400000000000000000000045011476450434500242540ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "filedecryptor.ih" BackupFrame *FileDecryptor::initBackupFrame(unsigned char *data, size_t length, uint64_t count) const { if (length < 1) [[unlikely]] { //std::cout << "ERROR: frame length < 1" << std::endl; return nullptr; } int fieldnum = BackupFrame::getFieldnumber(data[0]); if (fieldnum < 0) [[unlikely]] { //std::cout << "ERROR: field number < 0" << std::endl; return nullptr; } unsigned int wiretype = BackupFrame::wiretype(data[0]); unsigned int offset = 1; int64_t datalength = BackupFrame::getLength(data, &offset, length); DEBUGOUT("*** FRAMEDATA: ", bepaald::bytesToHexString(data, length)); DEBUGOUT("FIELDNUMBER: ", fieldnum); DEBUGOUT("DATALENGTH: ", datalength); DEBUGOUT("OFFSET: ", offset); if (datalength < 0 || (static_cast(fieldnum) != BackupFrame::FRAMETYPE::END && static_cast(datalength) > length - offset)) [[unlikely]] { //std::cout << "ERROR: data length < 0" << std::endl; return nullptr; } if (static_cast(fieldnum) == BackupFrame::FRAMETYPE::END) // is a raw bool type, not a message { if (wiretype == BackupFrame::WIRETYPE::VARINT && datalength == 1) return new EndFrame(data, datalength, count); else return nullptr; } else { if (wiretype != BackupFrame::WIRETYPE::LENGTHDELIM) [[unlikely]] { //std::cout << "ERROR: unexpected wiretype" << std::endl; return nullptr; } return BackupFrame::instantiate(static_cast(fieldnum), data + offset, datalength, count); } } signalbackup-tools-20250313-1/fileencryptor/000077500000000000000000000000001476450434500206135ustar00rootroot00000000000000signalbackup-tools-20250313-1/fileencryptor/encryptattachment.cc000066400000000000000000000072161476450434500246650ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "fileencryptor.ih" std::pair FileEncryptor::encryptAttachment(unsigned char *data, uint64_t length) { if (!d_ok) return {nullptr, 0}; if (length == 0) [[unlikely]] { Logger::warning("Asked to encrypt a zero sized attachment."); //return {nullptr, 0}; } if (d_verbose) [[unlikely]] Logger::message_start("Encrypting attachment. Length: ", length, "..."); // update iv: uintToFourBytes(d_iv, d_counter++); // encryption context std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); // disable padding EVP_CIPHER_CTX_set_padding(ctx.get(), 0); if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr, d_cipherkey, d_iv) != 1) [[unlikely]] { Logger::error("CTX INIT FAILED"); return {nullptr, 0}; } std::unique_ptr encryptedframe(new unsigned char[length + MACSIZE]); int l = static_cast(length); if (EVP_EncryptUpdate(ctx.get(), encryptedframe.get(), &l, data, length) != 1) [[unlikely]] { Logger::error("ENCRYPT FAILED"); return {nullptr, 0}; } // calc mac #if OPENSSL_VERSION_NUMBER >= 0x30000000L unsigned long int digest_size = SHA256_DIGEST_LENGTH; unsigned char hash[SHA256_DIGEST_LENGTH]; std::unique_ptr mac(EVP_MAC_fetch(nullptr, "hmac", nullptr), &::EVP_MAC_free); std::unique_ptr hctx(EVP_MAC_CTX_new(mac.get()), &::EVP_MAC_CTX_free); char digest[] = "SHA256"; OSSL_PARAM params[] = {OSSL_PARAM_construct_utf8_string("digest", digest, 0), OSSL_PARAM_construct_end()}; if (EVP_MAC_init(hctx.get(), d_mackey, d_mackey_size, params) != 1) [[unlikely]] { Logger::error("Failed to initialize HMAC"); return {nullptr, 0}; } if (EVP_MAC_update(hctx.get(), d_iv, d_iv_size) != 1 || EVP_MAC_update(hctx.get(), encryptedframe.get(), length) != 1 || EVP_MAC_final(hctx.get(), hash, nullptr, digest_size) != 1) [[unlikely]] { Logger::error("Failed to update/finalize hmac"); return {nullptr, 0}; } #else unsigned int digest_size = SHA256_DIGEST_LENGTH; unsigned char hash[SHA256_DIGEST_LENGTH]; std::unique_ptr hctx(HMAC_CTX_new(), &::HMAC_CTX_free); if (HMAC_Init_ex(hctx.get(), d_mackey, d_mackey_size, EVP_sha256(), nullptr) != 1) [[unlikely]] { Logger::error("Failed to initialize HMAC context"); return {nullptr, 0}; } if (HMAC_Update(hctx.get(), d_iv, d_iv_size) != 1 || HMAC_Update(hctx.get(), encryptedframe.get(), length) != 1 || HMAC_Final(hctx.get(), hash, &digest_size) != 1) [[unlikely]] { Logger::error("Failed to update/finalize hmac"); return {nullptr, 0}; } #endif std::memcpy(encryptedframe.get() + length, hash, 10); if (d_verbose) [[unlikely]] Logger::message_end("done!"); return {encryptedframe.release(), length + MACSIZE}; } signalbackup-tools-20250313-1/fileencryptor/encryptframe.cc000066400000000000000000000071461476450434500236310ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "fileencryptor.ih" std::pair FileEncryptor::encryptFrame(unsigned char *data, uint64_t length) { if (!d_ok) [[unlikely]] return {nullptr, 0}; if (length == 0) [[unlikely]] { Logger::warning("Asked to encrypt a zero sized frame."); //return {nullptr, 0}; } // update iv: uintToFourBytes(d_iv, d_counter++); // encryption context std::unique_ptr ctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); // disable padding EVP_CIPHER_CTX_set_padding(ctx.get(), 0); if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_ctr(), nullptr, d_cipherkey, d_iv) != 1) { Logger::error("CTX INIT FAILED"); return {nullptr, 0}; } std::unique_ptr encryptedframe(new unsigned char[sizeof(uint32_t) + length + MACSIZE]); int encryptedframepos = 0; // in newer backup file versions, the length is encrypted if (d_backupfileversion >= 1) [[likely]] { int l = static_cast(sizeof(uint32_t) + length); uint32_t length_data = bepaald::swap_endian(length + MACSIZE); if (d_verbose) [[unlikely]] Logger::message_start("Encrypting frame. Length: ", length, ", +macsize: ", (length + MACSIZE), ", swap_endian: ", length_data, " -> "); if (EVP_EncryptUpdate(ctx.get(), encryptedframe.get(), &l, reinterpret_cast(&length_data), sizeof(uint32_t)) != 1) { Logger::error("ENCRYPT FAILED"); return {nullptr, 0}; } encryptedframepos = l; if (d_verbose) [[unlikely]] Logger::message_end(bepaald::bytesToHexString(reinterpret_cast(encryptedframe.get()), 4)); } else [[unlikely]] // old backup file format, had RAW frame length { uint32_t rawlength = bepaald::swap_endian(length + MACSIZE); if (d_verbose) [[unlikely]] Logger::message("Writing raw framelength: ", length, ", +macsize: ", (length + MACSIZE), ", swap_endian: ", rawlength); std::memcpy(encryptedframe.get(), reinterpret_cast(&rawlength), sizeof(uint32_t)); encryptedframepos = 4; } int l = static_cast(length); if (EVP_EncryptUpdate(ctx.get(), encryptedframe.get() + encryptedframepos, &l, data, length) != 1) { Logger::error("ENCRYPT FAILED"); return {nullptr, 0}; } // calc mac unsigned int digest_size = SHA256_DIGEST_LENGTH; unsigned char hash[SHA256_DIGEST_LENGTH]; HMAC(EVP_sha256(), d_mackey, d_mackey_size, encryptedframe.get() + (d_backupfileversion >= 1 ? 0 : sizeof(uint32_t)), length + (d_backupfileversion >= 1 ? sizeof(uint32_t) : 0), hash, &digest_size); std::memcpy(encryptedframe.get() + sizeof(uint32_t) + length, hash, 10); //std::cout << " : " << bepaald::bytesToHexString(hash, digest_size) << std::endl; return {encryptedframe.release(), sizeof(uint32_t) + length + MACSIZE}; } signalbackup-tools-20250313-1/fileencryptor/fileencryptor.cc000066400000000000000000000025651476450434500240170ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "fileencryptor.ih" FileEncryptor::FileEncryptor(std::string const &passphrase, unsigned char const *salt, uint64_t salt_size, unsigned char const *iv, uint64_t iv_size, uint32_t backupfileversion, bool verbose) : CryptBase(verbose), d_passphrase(passphrase), d_backupfileversion(backupfileversion) { d_ok = init(salt, salt_size, iv, iv_size); } FileEncryptor::FileEncryptor(std::string const &passphrase, uint32_t backupfileversion, bool verbose) : CryptBase(verbose), d_passphrase(passphrase), d_backupfileversion(backupfileversion) {} FileEncryptor::FileEncryptor() : CryptBase(false), d_backupfileversion(-1) {} signalbackup-tools-20250313-1/fileencryptor/fileencryptor.h000066400000000000000000000074161476450434500236610ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef FILEENCRYPTOR_H_ #define FILEENCRYPTOR_H_ #include #include #include #include "../cryptbase/cryptbase.h" class FileEncryptor : public CryptBase { std::string d_passphrase; uint32_t d_backupfileversion; public: FileEncryptor(std::string const &passphrase, unsigned char const *salt, uint64_t salt_size, unsigned char const *iv, uint64_t iv_size, uint32_t backupfileversion, bool verbose); explicit FileEncryptor(std::string const &passphrase, uint32_t backupfileversion, bool verbose); FileEncryptor(); inline FileEncryptor(FileEncryptor const &other); inline FileEncryptor &operator=(FileEncryptor const &other); inline FileEncryptor(FileEncryptor &&other); inline FileEncryptor &operator=(FileEncryptor &&other); inline bool init(std::string const &passphrase, unsigned char const *salt, uint64_t salt_size, unsigned char const *iv, uint64_t iv_size, uint32_t backupfileversion, bool verbose); bool init(unsigned char const *salt, uint64_t salt_size, unsigned char const *iv, uint64_t iv_size); inline std::pair encryptFrame(std::pair, uint64_t> const &data); inline std::pair encryptFrame(std::pair const &data); std::pair encryptFrame(unsigned char *data, uint64_t length); std::pair encryptAttachment(unsigned char *data, uint64_t length); }; inline FileEncryptor::FileEncryptor(FileEncryptor const &other) : CryptBase(other), d_passphrase(other.d_passphrase), d_backupfileversion(other.d_backupfileversion) {} inline FileEncryptor &FileEncryptor::operator=(FileEncryptor const &other) { if (this != &other) { CryptBase::operator=(other); d_passphrase = other.d_passphrase; d_backupfileversion = other.d_backupfileversion; } return *this; } inline FileEncryptor::FileEncryptor(FileEncryptor &&other) : CryptBase(std::move(other)), d_passphrase(std::move(other.d_passphrase)), d_backupfileversion(std::move(other.d_backupfileversion)) {} inline FileEncryptor &FileEncryptor::operator=(FileEncryptor &&other) { if (this != &other) { CryptBase::operator=(other); d_passphrase = std::move(other.d_passphrase); d_backupfileversion = std::move(other.d_backupfileversion); } return *this; } inline bool FileEncryptor::init(std::string const &passphrase, unsigned char const *salt, uint64_t salt_size, unsigned char const *iv, uint64_t iv_size, uint32_t backupfileversion, bool verbose) { d_passphrase = passphrase; d_backupfileversion = backupfileversion; d_verbose = verbose; return init(salt, salt_size, iv, iv_size); } inline std::pair FileEncryptor::encryptFrame(std::pair const &data) { return encryptFrame(data.first, data.second); } inline std::pair FileEncryptor::encryptFrame(std::pair, uint64_t> const &data) { return encryptFrame(data.first.get(), data.second); } #endif signalbackup-tools-20250313-1/fileencryptor/fileencryptor.ih000066400000000000000000000015611476450434500240250ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "fileencryptor.h" #include #include #include #include "../logger/logger.h" signalbackup-tools-20250313-1/fileencryptor/init.cc000066400000000000000000000042471476450434500220740ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "fileencryptor.ih" bool FileEncryptor::init(unsigned char const *salt, uint64_t salt_size, unsigned char const *iv, uint64_t iv_size) { // set salt; d_salt_size = salt_size; d_salt = new unsigned char[d_salt_size]; std::memcpy(d_salt, salt, d_salt_size); // set iv; d_iv_size = iv_size; d_iv = new unsigned char[d_iv_size]; std::memcpy(d_iv, iv, d_iv_size); d_counter = fourBytesToUint(d_iv); // std::cout << "IV : " << bepaald::bytesToHexString(d_iv, d_iv_size) << std::endl; // std::cout << "SALT: " << bepaald::bytesToHexString(d_salt, d_salt_size) << std::endl; // generate backup key from salt and passphrase if (!getBackupKey(d_passphrase)) { Logger::error("Failed to generate backup key from passphrase"); return false; } // generate mackey and cipher from backupkey if (!getCipherAndMac(32, 64)) { Logger::error("Failed to generate mackey and cipher from backupkey"); return false; } DEBUGOUT("IV: ", bepaald::bytesToHexString(d_iv, d_iv_size)); DEBUGOUT("SALT: ", bepaald::bytesToHexString(d_salt, d_salt_size)); DEBUGOUT("BACKUPKEY: ", bepaald::bytesToHexString(d_backupkey, d_backupkey_size)); DEBUGOUT("CIPHERKEY: ", bepaald::bytesToHexString(d_cipherkey, d_cipherkey_size)); DEBUGOUT("MACKEY: ", bepaald::bytesToHexString(d_mackey, d_mackey_size)); DEBUGOUT("BACKUPFILE VERSION: ", d_backupfileversion); DEBUGOUT("COUNTER: ", d_counter); d_ok = true; return d_ok; } signalbackup-tools-20250313-1/filesqlitedb/000077500000000000000000000000001476450434500203755ustar00rootroot00000000000000signalbackup-tools-20250313-1/filesqlitedb/filesqlitedb.h000066400000000000000000000022431476450434500232160ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef FILESQLITEDB_H_ #define FILESQLITEDB_H_ #include "../sqlitedb/sqlitedb.h" class FileSqliteDB : public SqliteDB { public: inline explicit FileSqliteDB(std::string const &filename); FileSqliteDB(FileSqliteDB const &other) = delete; FileSqliteDB &operator=(FileSqliteDB const &other) = delete; ~FileSqliteDB() = default; }; inline FileSqliteDB::FileSqliteDB(std::string const &filename) : SqliteDB(filename, true) {} #endif signalbackup-tools-20250313-1/framewithattachment/000077500000000000000000000000001476450434500217655ustar00rootroot00000000000000signalbackup-tools-20250313-1/framewithattachment/framewithattachment.h000066400000000000000000000145201476450434500261770ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef FRAMEWITHATTACHMENT_H_ #define FRAMEWITHATTACHMENT_H_ #include #include #include #include "../common_be.h" #include "../backupframe/backupframe.h" #include "../baseattachmentreader/baseattachmentreader.h" class FrameWithAttachment : public BackupFrame { protected: unsigned char *d_attachmentdata; uint32_t d_attachmentdata_size; bool d_noclear; BaseAttachmentReader *d_attachmentreader; public: inline explicit FrameWithAttachment(uint64_t count = 0); inline FrameWithAttachment(unsigned char const *bytes, size_t length, uint64_t count = 0); inline FrameWithAttachment(FrameWithAttachment &&other); inline FrameWithAttachment &operator=(FrameWithAttachment &&other); inline FrameWithAttachment(FrameWithAttachment const &other); inline FrameWithAttachment &operator=(FrameWithAttachment const &other); inline virtual ~FrameWithAttachment() override; bool setAttachmentDataBacked(unsigned char *data, long long int length) override; bool setAttachmentDataUnbacked(unsigned char const *data, long long int length); inline uint32_t length() const; inline void setLength(int32_t l); inline void setReader(BaseAttachmentReader *reader); inline BaseAttachmentReader *reader() const; inline unsigned char *attachmentData(bool *badmac = nullptr, bool verbose = false); inline void clearData(); }; inline FrameWithAttachment::FrameWithAttachment(uint64_t count) : BackupFrame(count), d_attachmentdata(nullptr), d_attachmentdata_size(0), d_noclear(false), d_attachmentreader(nullptr) {} inline FrameWithAttachment::FrameWithAttachment(unsigned char const *bytes, size_t length, uint64_t count) : BackupFrame(bytes, length, count), d_attachmentdata(nullptr), d_attachmentdata_size(0), d_noclear(false), d_attachmentreader(nullptr) {} inline FrameWithAttachment::FrameWithAttachment(FrameWithAttachment &&other) : BackupFrame(std::move(other)), d_attachmentdata(std::move(other.d_attachmentdata)), d_attachmentdata_size(std::move(other.d_attachmentdata_size)), d_noclear(std::move(other.d_noclear)), d_attachmentreader(std::move(other.d_attachmentreader)) { other.d_attachmentdata = nullptr; other.d_attachmentdata_size = 0; other.d_attachmentreader = nullptr; } inline FrameWithAttachment &FrameWithAttachment::operator=(FrameWithAttachment &&other) { if (this != &other) { bepaald::destroyPtr(&d_attachmentdata, &d_attachmentdata_size); if (d_attachmentreader) delete d_attachmentreader; BackupFrame::operator=(std::move(other)); d_attachmentdata = std::move(other.d_attachmentdata); d_attachmentdata_size = std::move(other.d_attachmentdata_size); d_noclear = std::move(other.d_noclear); d_attachmentreader = std::move(other.d_attachmentreader); other.d_attachmentdata = nullptr; other.d_attachmentdata_size = 0; other.d_attachmentreader = nullptr; } return *this; } inline FrameWithAttachment::FrameWithAttachment(FrameWithAttachment const &other) : BackupFrame(other), d_attachmentdata(nullptr), d_attachmentdata_size(other.d_attachmentdata_size), d_noclear(other.d_noclear), d_attachmentreader(nullptr) { if (other.d_attachmentdata) { d_attachmentdata = new unsigned char[d_attachmentdata_size]; std::memcpy(d_attachmentdata, other.d_attachmentdata, d_attachmentdata_size); } if (other.d_attachmentreader) d_attachmentreader = other.d_attachmentreader->clone(); } inline FrameWithAttachment &FrameWithAttachment::operator=(FrameWithAttachment const &other) { if (this != &other) { bepaald::destroyPtr(&d_attachmentdata, &d_attachmentdata_size); if (d_attachmentreader) delete d_attachmentreader; BackupFrame::operator=(other); d_attachmentdata_size = other.d_attachmentdata_size; d_noclear = other.d_noclear; if (other.d_attachmentreader) d_attachmentreader = other.d_attachmentreader->clone(); if (other.d_attachmentdata) { d_attachmentdata = new unsigned char[d_attachmentdata_size]; std::memcpy(d_attachmentdata, other.d_attachmentdata, d_attachmentdata_size); } } return *this; } inline FrameWithAttachment::~FrameWithAttachment() { bepaald::destroyPtr(&d_attachmentdata, &d_attachmentdata_size); if (d_attachmentreader) delete d_attachmentreader; } inline uint32_t FrameWithAttachment::length() const { return d_attachmentdata_size; } inline void FrameWithAttachment::setReader(BaseAttachmentReader *reader) { d_attachmentreader = reader; } inline BaseAttachmentReader *FrameWithAttachment::reader() const { return d_attachmentreader; } inline unsigned char *FrameWithAttachment::attachmentData(bool *badmac, bool verbose) { if (!d_attachmentdata) { if (d_attachmentreader) { int result = d_attachmentreader->getAttachment(this, verbose); if (result == -1) [[unlikely]] { if (badmac) *badmac = true; return nullptr; } if (result == 1) [[unlikely]] return nullptr; } else [[unlikely]] { Logger::error("Asked for attachment data, but no reader was set"); return nullptr; } } return d_attachmentdata; } inline void FrameWithAttachment::clearData() { if (d_noclear) [[unlikely]] { //if (d_verbose) [[unlikely]] // maybe remove this warning Logger::message("Ignoring request to clear attachmentdata as it is not backed by file"); return; } if (d_attachmentdata) // do not use bepaald::destroyPtr, it will set size to zero { delete[] d_attachmentdata; d_attachmentdata = nullptr; } // to allow the attachmentreader to do its own cleanup d_attachmentreader->clearData(); } #endif signalbackup-tools-20250313-1/framewithattachment/setattachmentdata.cc000066400000000000000000000032731476450434500257770ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "framewithattachment.h" bool FrameWithAttachment::setAttachmentDataBacked(unsigned char *data, long long int datalength) // override { if (!data) return false; d_attachmentdata = data; d_attachmentdata_size = datalength; return true; } bool FrameWithAttachment::setAttachmentDataUnbacked(unsigned char const *data, long long int datalength) { bepaald::destroyPtr(&d_attachmentdata, &d_attachmentdata_size); d_attachmentdata_size = datalength; d_attachmentdata = new unsigned char[d_attachmentdata_size]; std::memcpy(d_attachmentdata, data, datalength); /* used for importing LONGTEXT messages from desktop. While on desktop they are normal message bodies, on Android they are attachments. Since we are creating these attachments on import from bytes in memory (not file backed), these can not be clearData()'s at any point, since the data can not be read back in that case. */ d_noclear = true; return true; } signalbackup-tools-20250313-1/groupstatusmessageproto/000077500000000000000000000000001476450434500227575ustar00rootroot00000000000000signalbackup-tools-20250313-1/groupstatusmessageproto/groupstatusmessageproto.h000066400000000000000000000065371476450434500301740ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef GROUPSTATUSMESSAGEPROTO_H_ #define GROUPSTATUSMESSAGEPROTO_H_ #include "../protobufparser/protobufparser.h" /* * This uses the old(?) V1(?) group status update protobuf * * /Signal-Android/libsignal/service/src/main/proto/SignalService.proto * */ /* message AttachmentPointer { enum Flags { VOICE_MESSAGE = 1; BORDERLESS = 2; reserved 3; GIF = 4; } oneof attachment_identifier { fixed64 cdnId = 1; string cdnKey = 15; } optional string contentType = 2; optional bytes key = 3; optional uint32 size = 4; optional bytes thumbnail = 5; optional bytes digest = 6; optional bytes incrementalDigest = 16; optional string fileName = 7; optional uint32 flags = 8; optional uint32 width = 9; optional uint32 height = 10; optional string caption = 11; optional string blurHash = 12; optional uint64 uploadTimestamp = 13; optional uint32 cdnNumber = 14; // Next ID: 17 } */ // NOTE WEIRD ORDERING ABOVE typedef ProtoBufParser AttachmentPointer; /* message GroupContext { enum Type { UNKNOWN = 0; UPDATE = 1; DELIVER = 2; QUIT = 3; REQUEST_INFO = 4; } message Member2 { reserved / uuid / 1; // removed optional string e164 = 2; } optional bytes id = 1; optional Type type = 2; optional string name = 3; repeated string membersE164 = 4; repeated Member2 members = 6; optional AttachmentPointer avatar = 5; } */ typedef ProtoBufParser Member2; typedef ProtoBufParser, AttachmentPointer> GroupContext; #endif signalbackup-tools-20250313-1/groupv2statusmessageproto/000077500000000000000000000000001476450434500232275ustar00rootroot00000000000000signalbackup-tools-20250313-1/groupv2statusmessageproto/groupv2statusmessageproto.h000066400000000000000000000650441476450434500307120ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef GROUPV2STATUSMESSAGEPROTO_H_ #define GROUPV2STATUSMESSAGEPROTO_H_ #include "../protobufparser/protobufparser.h" #include "../groupstatusmessageproto/groupstatusmessageproto.h" /* * For GroupV2 status messages * * /Signal-Android/libsignal/service/src/main/proto/Groups.proto * /Signal-Android/libsignal/service/src/main/proto/DecryptedGroups.proto * /Signal-Android/libsignal/service/src/main/proto/SignalService.proto * /Signal-Android/app/src/main/proto/Database.proto */ /* message AccessControl { enum AccessRequired { UNKNOWN = 0; ANY = 1; MEMBER = 2; ADMINISTRATOR = 3; UNSATISFIABLE = 4; } AccessRequired attributes = 1; AccessRequired members = 2; AccessRequired addFromInviteLink = 3; } */ typedef ProtoBufParser AccessControl; /* message Member { enum Role { UNKNOWN = 0; DEFAULT = 1; ADMINISTRATOR = 2; } bytes userId = 1; Role role = 2; bytes profileKey = 3; bytes presentation = 4; uint32 joinedAtRevision = 5; } */ typedef ProtoBufParser Member; /* message DecryptedMember { bytes uuid = 1; Member.Role role = 2; bytes profileKey = 3; uint32 joinedAtRevision = 5; bytes pni = 6; } */ typedef ProtoBufParser DecryptedMember; /* message DecryptedPendingMember { bytes uuid = 1; Member.Role role = 2; bytes addedByUuid = 3; uint64 timestamp = 4; bytes uuidCipherText = 5; } */ typedef ProtoBufParser DecryptedPendingMember; /* message DecryptedRequestingMember { bytes uuid = 1; bytes profileKey = 2; uint64 timestamp = 4; } */ typedef ProtoBufParser DecryptedRequestingMember; /* message DecryptedBannedMember { bytes uuid = 1; uint64 timestamp = 2; } */ typedef ProtoBufParser DecryptedBannedMember; /* message DecryptedPendingMemberRemoval { bytes uuid = 1; bytes uuidCipherText = 2; } */ typedef ProtoBufParser DecryptedPendingMemberRemoval; /* message DecryptedApproveMember { bytes uuid = 1; Member.Role role = 2; } */ typedef ProtoBufParser DecryptedApproveMember; /* message DecryptedModifyMemberRole { bytes uuid = 1; Member.Role role = 2; } */ typedef ProtoBufParser DecryptedModifyMemberRole; /* message DecryptedString { string value = 1; } */ typedef ProtoBufParser DecryptedString; /* message DecryptedTimer { uint32 duration = 1; } */ typedef ProtoBufParser DecryptedTimer; /* message DecryptedGroupChange { bytes editor = 1; uint32 revision = 2; repeated DecryptedMember newMembers = 3; repeated bytes deleteMembers = 4; repeated DecryptedModifyMemberRole modifyMemberRoles = 5; repeated DecryptedMember modifiedProfileKeys = 6; repeated DecryptedPendingMember newPendingMembers = 7; repeated DecryptedPendingMemberRemoval deletePendingMembers = 8; repeated DecryptedMember promotePendingMembers = 9; DecryptedString newTitle = 10; DecryptedString newAvatar = 11; DecryptedTimer newTimer = 12; AccessControl.AccessRequired newAttributeAccess = 13; AccessControl.AccessRequired newMemberAccess = 14; AccessControl.AccessRequired newInviteLinkAccess = 15; repeated DecryptedRequestingMember newRequestingMembers = 16; repeated bytes deleteRequestingMembers = 17; repeated DecryptedApproveMember promoteRequestingMembers = 18; bytes newInviteLinkPassword = 19; DecryptedString newDescription = 20; EnabledState newIsAnnouncementGroup = 21; repeated DecryptedBannedMember newBannedMembers = 22; repeated DecryptedBannedMember deleteBannedMembers = 23; repeated DecryptedMember promotePendingPniAciMembers = 24; } enum EnabledState { UNKNOWN = 0; ENABLED = 1; DISABLED = 2; } */ typedef ProtoBufParser, protobuffer::repeated::BYTES, std::vector, std::vector, std::vector, std::vector, std::vector, DecryptedString, DecryptedString, DecryptedTimer, protobuffer::optional::ENUM, protobuffer::optional::ENUM, protobuffer::optional::ENUM, std::vector, protobuffer::repeated::BYTES, std::vector, protobuffer::optional::BYTES, DecryptedString, protobuffer::optional::ENUM, std::vector, std::vector, std::vector> DecryptedGroupChange; /* message DecryptedGroup { string title = 2; string avatar = 3; DecryptedTimer disappearingMessagesTimer = 4; AccessControl accessControl = 5; uint32 revision = 6; repeated DecryptedMember members = 7; repeated DecryptedPendingMember pendingMembers = 8; repeated DecryptedRequestingMember requestingMembers = 9; bytes inviteLinkPassword = 10; string description = 11; EnabledState isAnnouncementGroup = 12; repeated DecryptedBannedMember bannedMembers = 13; } enum EnabledState { UNKNOWN = 0; ENABLED = 1; DISABLED = 2; } */ typedef ProtoBufParser, std::vector, std::vector, protobuffer::optional::BYTES, protobuffer::optional::STRING, protobuffer::optional::ENUM, std::vector> DecryptedGroup; /* message Member { enum Role { UNKNOWN = 0; DEFAULT = 1; ADMINISTRATOR = 2; } bytes userId = 1; Role role = 2; bytes profileKey = 3; bytes presentation = 4; // Only set when sending to server uint32 joinedAtRevision = 5; } */ typedef ProtoBufParser Member; /* message PendingMember { Member member = 1; bytes addedByUserId = 2; uint64 timestamp = 3; } */ typedef ProtoBufParser PendingMember; /* message RequestingMember { bytes userId = 1; bytes profileKey = 2; bytes presentation = 3; // Only set when sending to server uint64 timestamp = 4; } */ typedef ProtoBufParser RequestingMember; /* message BannedMember { bytes userId = 1; uint64 timestamp = 2; } */ typedef ProtoBufParser BannedMember; /* message AddMemberAction { Member added = 1; bool joinFromInviteLink = 2; } */ typedef ProtoBufParser AddMemberAction; /* message DeleteMemberAction { bytes deletedUserId = 1; } */ typedef ProtoBufParser DeleteMemberAction; /* message ModifyMemberRoleAction { bytes userId = 1; Member.Role role = 2; } */ typedef ProtoBufParser ModifyMemberRoleAction; /* message ModifyMemberProfileKeyAction { bytes presentation = 1; // Only set when sending to server bytes user_id = 2; // Only set when receiving from server bytes profile_key = 3; // Only set when receiving from server } */ typedef ProtoBufParser ModifyMemberProfileKeyAction; /* message AddPendingMemberAction { PendingMember added = 1; } */ typedef ProtoBufParser AddPendingMemberAction; /* message DeletePendingMemberAction { bytes deletedUserId = 1; } */ typedef ProtoBufParser DeletePendingMemberAction; /* message PromotePendingMemberAction { bytes presentation = 1; // Only set when sending to server bytes user_id = 2; // Only set when receiving from server bytes profile_key = 3; // Only set when receiving from server } */ typedef ProtoBufParser PromotePendingMemberAction; /* message PromotePendingPniAciMemberProfileKeyAction { bytes presentation = 1; // Only set when sending to server bytes userId = 2; // Only set when receiving from server bytes pni = 3; // Only set when receiving from server bytes profileKey = 4; // Only set when receiving from server } */ typedef ProtoBufParser PromotePendingPniAciMemberProfileKeyAction; /* message AddRequestingMemberAction { RequestingMember added = 1; } */ typedef ProtoBufParser AddRequestingMemberAction; /* message DeleteRequestingMemberAction { bytes deletedUserId = 1; } */ typedef ProtoBufParser DeleteRequestingMemberAction; /* message PromoteRequestingMemberAction { bytes userId = 1; Member.Role role = 2; } */ typedef ProtoBufParser PromoteRequestingMemberAction; /* message AddBannedMemberAction { BannedMember added = 1; } */ typedef ProtoBufParser AddBannedMemberAction; /* message DeleteBannedMemberAction { bytes deletedUserId = 1; } */ typedef ProtoBufParser DeleteBannedMemberAction; /* message ModifyTitleAction { bytes title = 1; } */ typedef ProtoBufParser ModifyTitleAction; /* message ModifyDescriptionAction { bytes description = 1; } */ typedef ProtoBufParser ModifyDescriptionAction; /* message ModifyAvatarAction { string avatar = 1; } */ typedef ProtoBufParser ModifyAvatarAction; /* message ModifyDisappearingMessagesTimerAction { bytes timer = 1; } */ typedef ProtoBufParser ModifyDisappearingMessagesTimerAction; /* message ModifyAttributesAccessControlAction { AccessControl.AccessRequired attributesAccess = 1; } */ typedef ProtoBufParser<> ModifyAttributesAccessControlAction; /* message ModifyMembersAccessControlAction { AccessControl.AccessRequired membersAccess = 1; } */ typedef ProtoBufParser<> ModifyMembersAccessControlAction; /* message ModifyAddFromInviteLinkAccessControlAction { AccessControl.AccessRequired addFromInviteLinkAccess = 1; } */ typedef ProtoBufParser<> ModifyAddFromInviteLinkAccessControlAction; /* message ModifyInviteLinkPasswordAction { bytes inviteLinkPassword = 1; } */ typedef ProtoBufParser ModifyInviteLinkPasswordAction; /* message ModifyAnnouncementsOnlyAction { bool announcementsOnly = 1; } */ typedef ProtoBufParser ModifyAnnouncementsOnlyAction; /* message Action { bytes sourceServiceId = 1; uint32 revision = 2; repeated AddMemberAction addMembers = 3; repeated DeleteMemberAction deleteMembers = 4; repeated ModifyMemberRoleAction modifyMemberRoles = 5; repeated ModifyMemberProfileKeyAction modifyMemberProfileKeys = 6; repeated AddPendingMemberAction addPendingMembers = 7; repeated DeletePendingMemberAction deletePendingMembers = 8; repeated PromotePendingMemberAction promotePendingMembers = 9; ModifyTitleAction modifyTitle = 10; ModifyAvatarAction modifyAvatar = 11; ModifyDisappearingMessagesTimerAction modifyDisappearingMessagesTimer = 12; ModifyAttributesAccessControlAction modifyAttributesAccess = 13; ModifyMembersAccessControlAction modifyMemberAccess = 14; ModifyAddFromInviteLinkAccessControlAction modifyAddFromInviteLinkAccess = 15; repeated AddRequestingMemberAction addRequestingMembers = 16; repeated DeleteRequestingMemberAction deleteRequestingMembers = 17; repeated PromoteRequestingMemberAction promoteRequestingMembers = 18; ModifyInviteLinkPasswordAction modifyInviteLinkPassword = 19; ModifyDescriptionAction modifyDescription = 20; ModifyAnnouncementsOnlyAction modifyAnnouncementsOnly = 21; repeated AddBannedMemberAction addBannedMembers = 22; repeated DeleteBannedMemberAction deleteBannedMembers = 23; repeated PromotePendingPniAciMemberProfileKeyAction promotePendingPniAciMembers = 24; } */ typedef ProtoBufParser, std::vector, std::vector, std::vector, std::vector, std::vector, std::vector, ModifyTitleAction, ModifyAvatarAction, ModifyDisappearingMessagesTimerAction, ModifyAttributesAccessControlAction, ModifyMembersAccessControlAction, ModifyAddFromInviteLinkAccessControlAction, std::vector, std::vector, std::vector, ModifyInviteLinkPasswordAction, ModifyDescriptionAction, ModifyAnnouncementsOnlyAction, std::vector, std::vector, std::vector> Action; /* message GroupChange { bytes actions = 1; // bytes = Action bytes serverSignature = 2; uint32 changeEpoch = 3; } */ typedef ProtoBufParser GroupChange; /* message GroupContextV2 { optional bytes masterKey = 1; optional uint32 revision = 2; optional bytes groupChange = 3; // == GroupChange } */ typedef ProtoBufParser GroupContextV2; /* message DecryptedGroupV2Context { signalservice.GroupContextV2 context = 1; DecryptedGroupChange change = 2; DecryptedGroup groupState = 3; DecryptedGroup previousGroupState = 4; } */ typedef ProtoBufParser DecryptedGroupV2Context; /* message GroupChangeChatUpdate { oneof update { GenericGroupUpdate genericGroupUpdate = 1; GroupCreationUpdate groupCreationUpdate = 2; GroupNameUpdate groupNameUpdate = 3; GroupAvatarUpdate groupAvatarUpdate = 4; GroupDescriptionUpdate groupDescriptionUpdate = 5; GroupMembershipAccessLevelChangeUpdate groupMembershipAccessLevelChangeUpdate = 6; GroupAttributesAccessLevelChangeUpdate groupAttributesAccessLevelChangeUpdate = 7; GroupAnnouncementOnlyChangeUpdate groupAnnouncementOnlyChangeUpdate = 8; GroupAdminStatusUpdate groupAdminStatusUpdate = 9; GroupMemberLeftUpdate groupMemberLeftUpdate = 10; GroupMemberRemovedUpdate groupMemberRemovedUpdate = 11; SelfInvitedToGroupUpdate selfInvitedToGroupUpdate = 12; SelfInvitedOtherUserToGroupUpdate selfInvitedOtherUserToGroupUpdate = 13; GroupUnknownInviteeUpdate groupUnknownInviteeUpdate = 14; GroupInvitationAcceptedUpdate groupInvitationAcceptedUpdate = 15; GroupInvitationDeclinedUpdate groupInvitationDeclinedUpdate = 16; GroupMemberJoinedUpdate groupMemberJoinedUpdate = 17; GroupMemberAddedUpdate groupMemberAddedUpdate = 18; GroupSelfInvitationRevokedUpdate groupSelfInvitationRevokedUpdate = 19; GroupInvitationRevokedUpdate groupInvitationRevokedUpdate = 20; GroupJoinRequestUpdate groupJoinRequestUpdate = 21; GroupJoinRequestApprovalUpdate groupJoinRequestApprovalUpdate = 22; GroupJoinRequestCanceledUpdate groupJoinRequestCanceledUpdate = 23; GroupInviteLinkResetUpdate groupInviteLinkResetUpdate = 24; GroupInviteLinkEnabledUpdate groupInviteLinkEnabledUpdate = 25; GroupInviteLinkAdminApprovalUpdate groupInviteLinkAdminApprovalUpdate = 26; GroupInviteLinkDisabledUpdate groupInviteLinkDisabledUpdate = 27; GroupMemberJoinedByLinkUpdate groupMemberJoinedByLinkUpdate = 28; GroupV2MigrationUpdate groupV2MigrationUpdate = 29; GroupV2MigrationSelfInvitedUpdate groupV2MigrationSelfInvitedUpdate = 30; GroupV2MigrationInvitedMembersUpdate groupV2MigrationInvitedMembersUpdate = 31; GroupV2MigrationDroppedMembersUpdate groupV2MigrationDroppedMembersUpdate = 32; GroupSequenceOfRequestsAndCancelsUpdate groupSequenceOfRequestsAndCancelsUpdate = 33; GroupExpirationTimerUpdate groupExpirationTimerUpdate = 34; } repeated Update updates = 1; } */ typedef ProtoBufParser GenericGroupUpdate; typedef ProtoBufParser GroupCreationUpdate; typedef ProtoBufParser GroupNameUpdate; typedef ProtoBufParser GroupAvatarUpdate; typedef ProtoBufParser GroupDescriptionUpdate; typedef ProtoBufParser GroupMembershipAccessLevelChangeUpdate; typedef ProtoBufParser GroupAttributesAccessLevelChangeUpdate; typedef ProtoBufParser GroupAnnouncementOnlyChangeUpdate; typedef ProtoBufParser GroupAdminStatusUpdate; typedef ProtoBufParser GroupMemberLeftUpdate; typedef ProtoBufParser GroupMemberRemovedUpdate; typedef ProtoBufParser SelfInvitedToGroupUpdate; typedef ProtoBufParser SelfInvitedOtherUserToGroupUpdate; typedef ProtoBufParser GroupUnknownInviteeUpdate; typedef ProtoBufParser GroupInvitationAcceptedUpdate; typedef ProtoBufParser GroupInvitationDeclinedUpdate; typedef ProtoBufParser GroupMemberJoinedUpdate; typedef ProtoBufParser GroupMemberAddedUpdate; typedef ProtoBufParser GroupSelfInvitationRevokedUpdate; typedef ProtoBufParser Invitee; typedef ProtoBufParser> GroupInvitationRevokedUpdate; typedef ProtoBufParser GroupJoinRequestUpdate; typedef ProtoBufParser GroupJoinRequestApprovalUpdate; typedef ProtoBufParser GroupJoinRequestCanceledUpdate; typedef ProtoBufParser GroupInviteLinkResetUpdate; typedef ProtoBufParser GroupInviteLinkEnabledUpdate; typedef ProtoBufParser GroupInviteLinkAdminApprovalUpdate; typedef ProtoBufParser GroupInviteLinkDisabledUpdate; typedef ProtoBufParser GroupMemberJoinedByLinkUpdate; typedef ProtoBufParser<> GroupV2MigrationUpdate; typedef ProtoBufParser<> GroupV2MigrationSelfInvitedUpdate; typedef ProtoBufParser GroupV2MigrationInvitedMembersUpdate; typedef ProtoBufParser GroupV2MigrationDroppedMembersUpdate; typedef ProtoBufParser GroupSequenceOfRequestsAndCancelsUpdate; typedef ProtoBufParser GroupExpirationTimerUpdate; typedef ProtoBufParser Update; typedef ProtoBufParser> GroupChangeChatUpdate; /* message GV2UpdateDescription { optional DecryptedGroupV2Context gv2ChangeDescription = 1; backup.GroupChangeChatUpdate groupChangeUpdate = 2; } */ typedef ProtoBufParser GV2UpdateDescription; /* message ProfileChangeDetails { message StringChange { string previous = 1; string newValue = 2; } StringChange profileNameChange = 1; StringChange learnedProfileName = 2; } */ typedef ProtoBufParser StringChange; typedef ProtoBufParser ProfileChangeDetails; /* message MessageExtras { oneof extra { GV2UpdateDescription gv2UpdateDescription = 1; signalservice.GroupContext gv1Context = 2; ProfileChangeDetails profileChangeDetails = 3; } } */ typedef ProtoBufParser MessageExtras; #endif signalbackup-tools-20250313-1/headerframe/000077500000000000000000000000001476450434500201715ustar00rootroot00000000000000signalbackup-tools-20250313-1/headerframe/headerframe.h000066400000000000000000000160521476450434500226110ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef HEADERFRAME_H_ #define HEADERFRAME_H_ #include #include "../backupframe/backupframe.h" #include "../base64/base64.h" #include "../common_be.h" #include "../common_bytes.h" class HeaderFrame : public BackupFrame { enum FIELD : unsigned int { INVALID = 0, IV = 1, // byte[] SALT = 2, // byte[] VERSION = 3 // uint32 }; static Registrar s_registrar; public: inline explicit HeaderFrame(uint64_t count = 0); inline HeaderFrame(unsigned char const *data, size_t length, uint64_t count = 0); inline virtual ~HeaderFrame() override = default; inline virtual HeaderFrame *clone() const override; inline virtual HeaderFrame *move_clone() override; inline virtual FRAMETYPE frameType() const override; inline unsigned char *iv() const; inline uint64_t iv_length() const; inline unsigned char *salt() const; inline uint64_t salt_length() const; inline uint32_t version() const; inline static BackupFrame *create(unsigned char const *data, size_t length, uint64_t count = 0); //inline static BackupFrame *createFromHumanData(std::ifstream *datastream, uint64_t count = 0); inline virtual void printInfo() const override; inline std::pair getData() const override; inline std::string getHumanData() const override; inline virtual bool validate(uint64_t) const override; inline unsigned int getField(std::string_view const &str) const; private: inline uint64_t dataSize() const override; }; inline HeaderFrame::HeaderFrame(uint64_t count) : BackupFrame(count) {} inline HeaderFrame *HeaderFrame::clone() const { return new HeaderFrame(*this); } inline HeaderFrame *HeaderFrame::move_clone() { return new HeaderFrame(std::move(*this)); } inline HeaderFrame::HeaderFrame(unsigned char const *data, size_t length, uint64_t count) : BackupFrame(data, length, count) { d_ok = iv_length() == 16; } inline BackupFrame::FRAMETYPE HeaderFrame::frameType() const // virtual override { return FRAMETYPE::HEADER; } inline unsigned char *HeaderFrame::iv() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::IV) return std::get<1>(p); return nullptr; } inline uint64_t HeaderFrame::iv_length() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::IV) return std::get<2>(p); return 0; } inline unsigned char *HeaderFrame::salt() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::SALT) return std::get<1>(p); return nullptr; } inline uint64_t HeaderFrame::salt_length() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::SALT) return std::get<2>(p); return 0; } inline uint32_t HeaderFrame::version() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::VERSION) return bytesToUint32(std::get<1>(p), std::get<2>(p)); return 0; } inline BackupFrame *HeaderFrame::create(unsigned char const *data, size_t length, uint64_t count) // static { return new HeaderFrame(data, length, count); } inline void HeaderFrame::printInfo() const { //DEBUGOUT("TYPE: HEADERFRAME"); Logger::message("Frame number: ", d_count); Logger::message(" Size: ", d_constructedsize); Logger::message(" Type: HEADER"); Logger::message(" - IV: ", bepaald::bytesToHexString(iv(), iv_length())); Logger::message(" - SALT: ", bepaald::bytesToHexString(salt(), salt_length())); Logger::message(" - VERSION: ", version()); } inline uint64_t HeaderFrame::dataSize() const { uint64_t size = 0; for (auto const &fd : d_framedata) { switch (std::get<0>(fd)) { case FIELD::IV: case FIELD::SALT: { uint64_t blobsize = std::get<2>(fd); // length of actual data // length of length size += varIntSize(blobsize); size += blobsize + 1; // plus one for fieldtype + wiretype break; } case FIELD::VERSION: { uint64_t value = bytesToInt64(std::get<1>(fd), std::get<2>(fd)); size += varIntSize(value); size += 1; // for fieldtype + wiretype break; } } } // for size of this entire frame. size += varIntSize(size); return ++size; } inline std::pair HeaderFrame::getData() const { uint64_t size = dataSize(); unsigned char *data = new unsigned char[size]; uint64_t datapos = 0; datapos += setFieldAndWire(FRAMETYPE::HEADER, WIRETYPE::LENGTHDELIM, data + datapos); datapos += setFrameSize(size, data + datapos); for (auto const &fd : d_framedata) { switch (std::get<0>(fd)) { case FIELD::IV: case FIELD::SALT: { datapos += putLengthDelimType(fd, data + datapos); break; } case FIELD::VERSION: { datapos += putVarIntType(fd, data + datapos); break; } } } return {data, size}; } inline std::string HeaderFrame::getHumanData() const { std::string data; for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::IV) { data += "IV:bytes:"; data += Base64::bytesToBase64String(std::get<1>(p), std::get<2>(p)) + "\n"; } else if (std::get<0>(p) == FIELD::SALT) { data += "SALT:bytes:"; data += Base64::bytesToBase64String(std::get<1>(p), std::get<2>(p)) + "\n"; } else if (std::get<0>(p) == FIELD::VERSION) data += "VERSION:uint32:" + bepaald::toString(bytesToUint32(std::get<1>(p), std::get<2>(p))) + "\n"; } return data; } inline bool HeaderFrame::validate(uint64_t) const { if (d_framedata.empty()) return false; int foundiv = 0; int foundsalt = 0; for (auto const &p : d_framedata) { if (std::get<0>(p) != FIELD::IV && std::get<0>(p) != FIELD::SALT && std::get<0>(p) != FIELD::VERSION) // all possible fields, version only in newer backups return false; // must contain salt and iv, each 1 time. if (std::get<0>(p) == FIELD::IV) ++foundiv; if (std::get<0>(p) == FIELD::SALT) ++foundsalt; } // salt length is 32, iv is 16 return foundsalt == 1 && foundiv == 1 && salt_length() == 32 && iv_length() == 16; } inline unsigned int HeaderFrame::getField(std::string_view const &str) const { if (str == "IV") return FIELD::IV; if (str == "SALT") return FIELD::SALT; if (str == "VERSION") return FIELD::VERSION; return FIELD::INVALID; } #endif signalbackup-tools-20250313-1/headerframe/headerframe.ih000066400000000000000000000014031476450434500227540ustar00rootroot00000000000000/* Copyright (C) 2019-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "headerframe.h" signalbackup-tools-20250313-1/headerframe/statics.cc000066400000000000000000000015361476450434500221570ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "headerframe.ih" HeaderFrame::Registrar HeaderFrame::s_registrar(FRAMETYPE::HEADER, HeaderFrame::create); signalbackup-tools-20250313-1/homebrew/000077500000000000000000000000001476450434500175365ustar00rootroot00000000000000signalbackup-tools-20250313-1/homebrew/signalbackup-tools.rb000066400000000000000000000013571476450434500236720ustar00rootroot00000000000000# Documentation: https://docs.brew.sh/Formula-Cookbook # https://rubydoc.brew.sh/Formula class SignalbackupTools < Formula desc "A tool to work with Signal backup files" homepage "https://github.com/bepaald/signalbackup-tools" license "GPL-3.0-or-later" head "https://github.com/bepaald/signalbackup-tools.git", branch: "master" depends_on "cmake" =>:build depends_on "openssl@3" depends_on "sqlite" def install system "cmake", "-B", "build", *std_cmake_args system "cmake", "--build", "build" bin.install "build/signalbackup-tools" end test do # not a 'good' test, but not sure what else is possible here `#{bin}/signalbackup-tools --help` result=$?.success? assert *result end end signalbackup-tools-20250313-1/invalidframe/000077500000000000000000000000001476450434500203675ustar00rootroot00000000000000signalbackup-tools-20250313-1/invalidframe/invalidframe.h000066400000000000000000000033251476450434500232040ustar00rootroot00000000000000/* Copyright (C) 2020-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef INVALIDFRAME_H_ #define INVALIDFRAME_H_ #include "../backupframe/backupframe.h" class InvalidFrame : public BackupFrame { static Registrar s_registrar; public: inline explicit InvalidFrame(uint64_t count = 0); inline virtual ~InvalidFrame() override = default; inline virtual InvalidFrame *clone() const override; inline virtual InvalidFrame *move_clone() override; inline virtual FRAMETYPE frameType() const override; inline virtual void printInfo() const override; }; inline InvalidFrame::InvalidFrame(uint64_t count) : BackupFrame(count) {} inline InvalidFrame *InvalidFrame::clone() const { return new InvalidFrame(*this); } inline InvalidFrame *InvalidFrame::move_clone() { return new InvalidFrame(std::move(*this)); } inline BackupFrame::FRAMETYPE InvalidFrame::frameType() const { return FRAMETYPE::INVALID; } inline void InvalidFrame::printInfo() const { Logger::message("Frame number: ", d_count); Logger::message(" Type: INVALID"); } #endif signalbackup-tools-20250313-1/jsondatabase/000077500000000000000000000000001476450434500203645ustar00rootroot00000000000000signalbackup-tools-20250313-1/jsondatabase/jsondatabase.cc000066400000000000000000000162601476450434500233360ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "jsondatabase.ih" #include "../common_filesystem.h" JsonDatabase::JsonDatabase(std::string const &jsonfile, bool verbose, bool truncate) : d_ok(false), d_verbose(verbose), d_truncate(truncate) { // open file, get size and read data std::ifstream sourcefile(jsonfile, std::ios_base::binary | std::ios_base::in); if (!sourcefile.is_open()) { Logger::error("Failed to open file for reading: ", jsonfile); return; } //sourcefile.seekg(0, std::ios_base::end); //long long int datasize = sourcefile.tellg(); //sourcefile.seekg(0, std::ios_base::beg); uint64_t datasize = bepaald::fileSize(jsonfile); if (datasize == 0 || datasize == static_cast(-1)) [[unlikely]] { Logger::error("Bad filesize (", datasize, ")"); return; } std::unique_ptr data(new char[datasize]); if (!sourcefile.read(data.get(), datasize)) { Logger::error("Failed to read json data"); return; } // create tables if (!d_database.exec("CREATE TABLE chats(idx INT, id TEXT, name TEXT, type TEXT)") || !d_database.exec("CREATE TABLE tmp_json_tree (value TEXT, path TEXT)") || !d_database.exec("CREATE TABLE messages(chatidx INT, id INT, type TEXT, date INT, " "from_name TEXT, from_id TEXT, body TEXT, " "reply_to_id INT, forwarded_from TEXT, " "saved_from TEXT, photo TEXT, width INT, height INT, " "file TEXT, media_type TEXT, mime_type TEXT, " "contact_vcard TEXT, poll)")) { Logger::error("Failed to set up sql tables"); return; } // INSERT DATA INTO CHATS TABLE if (d_verbose) [[unlikely]] Logger::message_start("Inserting chats from json..."); if (!d_database.exec("INSERT INTO chats SELECT " "key, " "json_extract(value, '$.id') AS id, " "json_extract(value, '$.name') AS name, " "json_extract(value, '$.type') AS type " "FROM json_each(?, '$.chats.list')", SqliteDB::StaticTextParam(data.get(), datasize))) { Logger::error("Failed to fill sql table"); return; } if (d_database.changed() == 0) // maybe single-chat-json ? { if (d_verbose) [[unlikely]] { Logger::message("No chats-list found, trying single chat list"); Logger::message_start("Inserting chats from json..."); } if (!d_database.exec("INSERT INTO chats SELECT " "0, " "json_extract(?1, '$.id') AS id, " "json_extract(?1, '$.name') AS name, " "json_extract(?1, '$.type') AS type", SqliteDB::StaticTextParam(data.get(), datasize))) { Logger::error("Failed to fill sql table"); return; } } if (d_verbose) [[unlikely]] Logger::message_end("done! (", d_database.changed(), ")"); // std::cout << std::endl << "CHATS: " << std::endl; // d_database.prettyPrint(d_truncate, "SELECT COUNT(*) FROM chats"); // d_database.prettyPrint(d_truncate, "SELECT * FROM chats LIMIT 10"); // INSERT DATA INTO MESSAGES TABLE // note: to glob-match '$.chats.list[0].messages' for any number, we create a character class // for the first '[' -> '[[]', since GLOB has no escape characters if (d_verbose) [[unlikely]] Logger::message_start("Inserting messages from json..."); if (!d_database.exec("INSERT INTO tmp_json_tree SELECT value, path " "FROM json_tree(?) WHERE path GLOB '$.chats.list[[][0-9]*].messages'", SqliteDB::StaticTextParam(data.get(), datasize))) return; if (d_database.changed() == 0) { if (d_verbose) [[unlikely]] { Logger::message("Json tree appears empty, trying to interpret json as single-chat-export"); Logger::message_start("Inserting messages from json..."); } if (!d_database.exec("INSERT INTO tmp_json_tree SELECT value, '$.chats.list[0].messages' AS path " "FROM json_tree(?) WHERE path = '$.messages'", SqliteDB::StaticTextParam(data.get(), datasize))) return; } if (!d_database.exec("INSERT INTO messages SELECT " "REPLACE(REPLACE(path, '$.chats.list[', ''), '].messages', '') AS chatidx, " "json_extract(value, '$.id') AS id, " "json_extract(value, '$.type') AS type, " "json_extract(value, '$.date_unixtime') AS date, " "json_extract(value, '$.from') AS from_name, " "json_extract(value, '$.from_id') AS from_id, " "json_extract(value, '$.text_entities') AS body, " "json_extract(value, '$.reply_to_message_id') AS reply_to_id, " "json_extract(value, '$.forwarded_from') AS forwarded_from, " "json_extract(value, '$.saved_from') AS saved_from, " "json_extract(value, '$.photo') AS photo, " "json_extract(value, '$.width') AS width, " "json_extract(value, '$.height') AS height, " "json_extract(value, '$.file') AS file, " "json_extract(value, '$.media_type') AS media_type, " "json_extract(value, '$.mime_type') AS mime_type, " "json_extract(value, '$.contact_vcard') AS contact_vcard, " "json_extract(value, '$.poll') AS poll FROM tmp_json_tree")) return; // the 'saved_messages' chat has no 'name' field. Since this is note-to-self, the name should be the name of the // 'from' field of all messages in that chat. SqliteDB::QueryResults saved_messages_name; if (d_database.exec("SELECT DISTINCT from_name FROM messages WHERE chatidx IN " "(SELECT DISTINCT idx FROM chats WHERE type = 'saved_messages')", &saved_messages_name) && saved_messages_name.rows() == 1) d_database.exec("UPDATE chats SET name = ? WHERE name IS NULL AND type = 'saved_messages'", saved_messages_name.value(0, 0)); if (d_verbose) [[unlikely]] Logger::message_end("done! (", d_database.changed(), ")"); d_database.exec("DROP TABLE tmp_json_tree"); // std::cout << std::endl << "MESSAGES: " << std::endl; // d_database.prettyPrint(d_truncate, "SELECT COUNT(*) FROM messages"); // d_database.prettyPrint(d_truncate, "SELECT * FROM messages");// WHERE chatidx = 42 LIMIT 10"); d_ok = true; } signalbackup-tools-20250313-1/jsondatabase/jsondatabase.h000066400000000000000000000031051476450434500231720ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef JSONDATABASE_H_ #define JSONDATABASE_H_ #include "../memsqlitedb/memsqlitedb.h" class JsonDatabase { MemSqliteDB d_database; bool d_ok; bool d_verbose; bool d_truncate; public: JsonDatabase(std::string const &jsonfile, bool verbose, bool truncate); JsonDatabase(JsonDatabase const &other) = default; JsonDatabase(JsonDatabase &&other) = default; JsonDatabase &operator=(JsonDatabase const &other) = default; JsonDatabase &operator=(JsonDatabase &&other) = default; inline bool ok() const; inline void listChats() const; friend class SignalBackup; }; inline bool JsonDatabase::ok() const { return d_ok; } inline void JsonDatabase::listChats() const { d_database.prettyPrint(d_truncate, "SELECT idx, id, name, type, (SELECT COUNT(*) FROM messages WHERE messages.chatidx = chats.idx) AS message_count FROM chats"); } #endif signalbackup-tools-20250313-1/jsondatabase/jsondatabase.ih000066400000000000000000000015321476450434500233450ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "jsondatabase.h" #include #include #include #include "../logger/logger.h" signalbackup-tools-20250313-1/keyvalueframe/000077500000000000000000000000001476450434500205665ustar00rootroot00000000000000signalbackup-tools-20250313-1/keyvalueframe/keyvalueframe.h000066400000000000000000000241571476450434500236100ustar00rootroot00000000000000/* Copyright (C) 2021-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef KEYVALUEFRAME_H_ #define KEYVALUEFRAME_H_ #include #include "../backupframe/backupframe.h" #include "../base64/base64.h" #include "../common_be.h" class KeyValueFrame : public BackupFrame { enum FIELD { INVALID = 0, KEY = 1, // string BLOBVALUE = 2, // bytes BOOLEANVALUE = 3, // bool FLOATVALUE = 4, // float INTEGERVALUE = 5, // int32 LONGVALUE = 6, // int64 STRINGVALUE = 7 // string }; static Registrar s_registrar; public: inline explicit KeyValueFrame(uint64_t count = 0); inline KeyValueFrame(unsigned char const *bytes, size_t length, uint64_t count = 0); inline virtual ~KeyValueFrame() override = default; inline virtual KeyValueFrame *clone() const override; inline virtual KeyValueFrame *move_clone() override; inline static BackupFrame *create(unsigned char const *bytes, size_t length, uint64_t count = 0); inline virtual void printInfo() const override; inline virtual FRAMETYPE frameType() const override; inline std::pair getData() const override; inline virtual bool validate(uint64_t) const override; inline std::string getHumanData() const override; inline unsigned int getField(std::string_view const &str) const; inline std::string key() const; inline std::string value() const; inline std::string valueType() const; private: inline uint64_t dataSize() const override; }; inline KeyValueFrame::KeyValueFrame(uint64_t count) : BackupFrame(count) {} inline KeyValueFrame::KeyValueFrame(unsigned char const *bytes, size_t length, uint64_t count) : BackupFrame(bytes, length, count) {} inline KeyValueFrame *KeyValueFrame::clone() const { return new KeyValueFrame(*this); } inline KeyValueFrame *KeyValueFrame::move_clone() { return new KeyValueFrame(std::move(*this)); } inline BackupFrame *KeyValueFrame::create(unsigned char const *bytes, size_t length, uint64_t count) { return new KeyValueFrame(bytes, length, count); } inline void KeyValueFrame::printInfo() const // virtual { //DEBUGOUT("TYPE: KEYVALUEFRAME"); Logger::message("Frame number: ", d_count); Logger::message(" Size: ", d_constructedsize); Logger::message(" Type: KEYVALUEFRAME"); for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::KEY) Logger::message(" - (key : \"", bepaald::bytesToString(std::get<1>(p), std::get<2>(p)), "\" (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::BLOBVALUE) Logger::message(" - (blobvalue : \"", bepaald::bytesToString(std::get<1>(p), std::get<2>(p)), "\" (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::BOOLEANVALUE) Logger::message(" - (booleanvalue : \"", std::boolalpha, (bytesToInt64(std::get<1>(p), std::get<2>(p)) ? true : false), "\")"); else if (std::get<0>(p) == FIELD::FLOATVALUE) // note, this is untested, none of my backups contain a KVFrame with this field Logger::message(" - (floatvalue : \"", bepaald::toString(*reinterpret_cast(std::get<1>(p))), "\" (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::INTEGERVALUE) Logger::message(" - (integervalue : \"", bytesToInt32(std::get<1>(p), std::get<2>(p)), "\" (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::LONGVALUE) Logger::message(" - (longvalue : \"", bytesToUint64(std::get<1>(p), std::get<2>(p)), "\" (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::STRINGVALUE) Logger::message(" - (stringvalue : \"", bepaald::bytesToString(std::get<1>(p), std::get<2>(p)), "\" (", std::get<2>(p), " bytes)"); } } inline BackupFrame::FRAMETYPE KeyValueFrame::frameType() const // virtual override { return FRAMETYPE::KEYVALUE; } inline uint64_t KeyValueFrame::dataSize() const { uint64_t size = 0; for (auto const &fd : d_framedata) { switch (std::get<0>(fd)) { case FIELD::KEY: case FIELD::STRINGVALUE: case FIELD::BLOBVALUE: { uint64_t stringsize = std::get<2>(fd); size += varIntSize(stringsize); size += stringsize + 1; // +1 for fieldtype + wiretype break; } case FIELD::INTEGERVALUE: case FIELD::LONGVALUE: case FIELD::BOOLEANVALUE: { uint64_t val = bytesToInt64(std::get<1>(fd), std::get<2>(fd)); size += varIntSize(val); size += 1; // for fieldtype + wiretype break; } case FIELD::FLOATVALUE: // note, this is untested, none of my backups contain a KVFrame with this field { size += 5; // fixed32? +1 for fieldtype + wiretype break; } } } // for size of this entire frame. size += varIntSize(size); return ++size; } inline std::pair KeyValueFrame::getData() const { uint64_t size = dataSize(); unsigned char *data = new unsigned char[size]; uint64_t datapos = 0; datapos += setFieldAndWire(FRAMETYPE::KEYVALUE, WIRETYPE::LENGTHDELIM, data + datapos); datapos += setFrameSize(size, data + datapos); for (auto const &fd : d_framedata) { switch (std::get<0>(fd)) { case FIELD::KEY: case FIELD::STRINGVALUE: case FIELD::BLOBVALUE: { datapos += putLengthDelimType(fd, data + datapos); break; } case FIELD::INTEGERVALUE: case FIELD::LONGVALUE: case FIELD::BOOLEANVALUE: { datapos += putVarIntType(fd, data + datapos); break; } case FIELD::FLOATVALUE: { datapos += putFixed32Type(fd, data + datapos); // untested break; } } } return {data, size}; } // not sure about the requirements, but I'm guessing // 1 key and at least one value is required inline bool KeyValueFrame::validate(uint64_t) const { if (d_framedata.empty()) return false; bool foundkey = false; bool foundvalue = false; for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::KEY) foundkey = true; if (std::get<0>(p) == FIELD::STRINGVALUE || std::get<0>(p) == FIELD::BOOLEANVALUE || std::get<0>(p) == FIELD::BLOBVALUE || std::get<0>(p) == FIELD::INTEGERVALUE || std::get<0>(p) == FIELD::LONGVALUE || std::get<0>(p) == FIELD::FLOATVALUE) foundvalue = true; } return foundkey && foundvalue; } inline std::string KeyValueFrame::getHumanData() const { std::string data; for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::KEY) data += "KEY:string:" + bepaald::bytesToString(std::get<1>(p), std::get<2>(p)) + "\n"; else if (std::get<0>(p) == FIELD::BLOBVALUE) data += "BLOBVALUE:bytes:" + Base64::bytesToBase64String(std::get<1>(p), std::get<2>(p)) + "\n"; else if (std::get<0>(p) == FIELD::BOOLEANVALUE) data += "BOOLEANVALUE:bool:" + (bytesToInt64(std::get<1>(p), std::get<2>(p)) ? "true"s : "false"s) + "\n"; else if (std::get<0>(p) == FIELD::FLOATVALUE) data += "FLOATVALUE:float:" + Base64::bytesToBase64String(std::get<1>(p), std::get<2>(p)) + "\n"; // warning, untested else if (std::get<0>(p) == FIELD::INTEGERVALUE) data += "INTEGERVALUE:int32:" + bepaald::toString(bytesToInt32(std::get<1>(p), std::get<2>(p))) + "\n"; else if (std::get<0>(p) == FIELD::LONGVALUE) data += "LONGVALUE:int64:" + bepaald::toString(bytesToInt64(std::get<1>(p), std::get<2>(p))) + "\n"; else if (std::get<0>(p) == FIELD::STRINGVALUE) data += "STRINGVALUE:string:" + bepaald::bytesToString(std::get<1>(p), std::get<2>(p)) + "\n"; } return data; } inline unsigned int KeyValueFrame::getField(std::string_view const &str) const { if (str == "KEY") return FIELD::KEY; if (str == "BLOBVALUE") return FIELD::BLOBVALUE; if (str == "BOOLEANVALUE") return FIELD::BOOLEANVALUE; if (str == "FLOATVALUE") return FIELD::FLOATVALUE; if (str == "LONGVALUE") return FIELD::LONGVALUE; if (str == "INTEGERVALUE") return FIELD::INTEGERVALUE; if (str == "STRINGVALUE") return FIELD::STRINGVALUE; return FIELD::INVALID; } inline std::string KeyValueFrame::key() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::KEY) return bepaald::bytesToString(std::get<1>(p), std::get<2>(p)); return std::string(); } inline std::string KeyValueFrame::value() const { for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::STRINGVALUE) return bepaald::bytesToString(std::get<1>(p), std::get<2>(p)); if (std::get<0>(p) == FIELD::INTEGERVALUE || std::get<0>(p) == FIELD::LONGVALUE) return bepaald::toString(bytesToInt64(std::get<1>(p), std::get<2>(p))); if (std::get<0>(p) == FIELD::BLOBVALUE || std::get<0>(p) == FIELD::FLOATVALUE) // float is untested return Base64::bytesToBase64String(std::get<1>(p), std::get<2>(p)); if (std::get<0>(p) == FIELD::BOOLEANVALUE) return (bytesToInt64(std::get<1>(p), std::get<2>(p)) ? "true"s : "false"s); } return std::string(); } inline std::string KeyValueFrame::valueType() const { for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::STRINGVALUE) return "STRING"; if (std::get<0>(p) == FIELD::INTEGERVALUE || std::get<0>(p) == FIELD::LONGVALUE) return "INTEGER"; if (std::get<0>(p) == FIELD::BLOBVALUE) return "BLOB"; if (std::get<0>(p) == FIELD::FLOATVALUE) return "FLOAT"; if (std::get<0>(p) == FIELD::BOOLEANVALUE) return "BOOL"; } return std::string(); } #endif signalbackup-tools-20250313-1/keyvalueframe/keyvalueframe.ih000066400000000000000000000014051476450434500237500ustar00rootroot00000000000000/* Copyright (C) 2021-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "keyvalueframe.h" signalbackup-tools-20250313-1/keyvalueframe/statics.cc000066400000000000000000000015311476450434500225470ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "keyvalueframe.ih" KeyValueFrame::Registrar KeyValueFrame::s_registrar(FRAMETYPE::KEYVALUE, create); signalbackup-tools-20250313-1/logger/000077500000000000000000000000001476450434500172055ustar00rootroot00000000000000signalbackup-tools-20250313-1/logger/isterminal.cc000066400000000000000000000022671476450434500216720ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "logger.h" bool Logger::isTerminal() { #ifdef HAS_UNISTD_H_ // defined if unistd.h is available static const bool result = [] { return isatty(STDOUT_FILENO); }(); return result; #else #if defined(_WIN32) || defined(__MINGW64__) DWORD filetype = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)); return filetype != FILE_TYPE_PIPE && filetype != FILE_TYPE_DISK; // this is not foolproof (eg output is printer)... #endif return false; #endif } signalbackup-tools-20250313-1/logger/logger.h000066400000000000000000000412151476450434500206400ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef LOGGER_H_ #define LOGGER_H_ #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) || defined(__MINGW64__) #define WIN32_LEAN_AND_MEAN 1 #include #else // !windows #include #if __has_include("unistd.h") #define HAS_UNISTD_H_ #include #endif #endif class Logger { enum Flags { NONE = 0, OVERWRITE = 0b1, NONEWLINE = 0b10, }; public: enum class Control { BOLD, NORMAL, ENDOVERWRITE, }; struct ControlChar { std::string code; explicit ControlChar(std::string const &c) : code(c) {} }; template struct VECTOR { std::vector const &data; std::string const &delim; explicit VECTOR(std::vector const &v, std::string const &d = std::string()) : data(v), delim(d) {} }; private: static std::unique_ptr s_instance; std::ofstream *d_file; std::ostringstream *d_strstreambackend; std::basic_ostream *d_currentoutput; bool d_usetimestamps; bool d_used; bool d_controlcodessupported; bool d_overwriting; bool d_dangling; //std::sstream d_previousline; std::set d_warningsgiven; public: inline static void setFile(std::string const &f); inline static void setTimestamp(bool val); template inline static void message_overwrite(First const &f, Rest... r); template inline static void message(First const &f, Rest... r); template inline static void message_start(First const &f, Rest... r); inline static void message_start(); template inline static void message_continue(First const &f, Rest... r); template inline static void message_end(First const &f, Rest... r); inline static void message_end(); template inline static void warning(First const &f, Rest... r); template inline static void warning_start(First const &f, Rest... r); template inline static void warning_indent(First const &f, Rest... r); template inline static void error(First const &f, Rest... r); template inline static void error_start(First const &f, Rest... r); template inline static void error_indent(First const &f, Rest... r); template inline static void output_indent(int indent, First const &f, Rest... r); inline static void warnOnce(std::string const &w, bool error = false, std::string::size_type = std::string::npos); inline ~Logger(); private: inline Logger(); inline static void ensureLogger(); inline static void firstUse(); inline static std::ostream &dispTime(std::ostream &stream); inline static void messagePre(); void outputHead(std::string const &file, std::string const &stdandardout, bool overwrite = false, std::pair const &prepost = std::pair(), std::pair const &control = std::pair()); void outputHead(std::string const &head, bool overwrite = false, std::pair const &prepost = std::pair(), std::pair const &control = std::pair()); template inline void outputMsg(Flags flags, First const &f, Rest... r); template inline void outputMsg(Flags flags, T const &t); // specializations for controlchar template inline void outputMsg(Flags flags, Logger::ControlChar const &c, Rest... r); inline void outputMsg(Flags flags, Logger::ControlChar const &c); // specializations for vector type template inline void outputMsg(Flags flags, VECTOR const &vec, Rest... r); template inline void outputMsg(Flags flags, VECTOR const &vec); template inline void outputMsg(Flags flags, std::vector const &vec, Rest... r); template inline void outputMsg(Flags flags, std::vector const &vec); // specializations for control template inline void outputMsg(Flags flags, Control c, Rest... r); inline void outputMsg(Flags flags, Control c); static bool supportsAnsi(); static bool isTerminal(); Logger(Logger const &other) = delete; // NI Logger &operator=(Logger const &other) = delete; // NI }; inline Logger::Logger() : d_file(nullptr), d_strstreambackend(nullptr), d_currentoutput(d_file), d_usetimestamps(false), d_used(false), d_controlcodessupported(isTerminal() && supportsAnsi()), d_overwriting(false), d_dangling(false) {} inline Logger::~Logger() { if (d_strstreambackend) { if (d_strstreambackend->tellp() != 0) { if (d_file) (*d_file) << d_strstreambackend->str(); d_strstreambackend->str(""); d_strstreambackend->clear(); } delete d_strstreambackend; } if (d_file) { (*d_file) << std::flush; d_file->close(); delete d_file; } } inline void Logger::ensureLogger() // static { if (!s_instance.get()) [[unlikely]] s_instance.reset(new Logger); } // prints out a header containing current date and time inline void Logger::firstUse() // static { if (!s_instance->d_used) [[unlikely]] { s_instance->d_used = true; std::time_t cur = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); if (s_instance->d_currentoutput) *(s_instance->d_currentoutput) << " *** Starting log: " << std::put_time(std::localtime(&cur), "%F %T") << " ***" << "\n"; std::cout << " *** Starting log: " << std::put_time(std::localtime(&cur), "%F %T") << " ***" << std::endl; } } inline std::ostream &Logger::dispTime(std::ostream &stream) // static { std::time_t cur = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); return stream << std::put_time(std::localtime(&cur), "%Y-%m-%d %H:%M:%S"); // %F and %T do not work on mingw } inline void Logger::messagePre() //static { ensureLogger(); firstUse(); if (s_instance->d_overwriting) [[unlikely]] { std::cout << std::endl; s_instance->d_overwriting = false; s_instance->d_currentoutput = s_instance->d_file; if (s_instance->d_strstreambackend && s_instance->d_strstreambackend->tellp() != 0) { (*s_instance->d_currentoutput) << s_instance->d_strstreambackend->str(); s_instance->d_strstreambackend->str(""); s_instance->d_strstreambackend->clear(); } } if (s_instance->d_dangling) { s_instance->d_dangling = false; std::cout << std::endl; if (s_instance->d_file) (*s_instance->d_file) << '\n'; } } inline void Logger::setFile(std::string const &f) // static { ensureLogger(); if (s_instance->d_file) return; s_instance->d_file = new std::ofstream(f); s_instance->d_currentoutput = s_instance->d_file; if (!s_instance->d_strstreambackend) s_instance->d_strstreambackend = new std::ostringstream(); firstUse(); } inline void Logger::setTimestamp(bool val) // static { ensureLogger(); s_instance->d_usetimestamps = val; firstUse(); } template inline void Logger::message_overwrite(First const &f, Rest... r) // static { ensureLogger(); firstUse(); s_instance->d_overwriting = true; if (s_instance->d_strstreambackend) { s_instance->d_strstreambackend->str(""); s_instance->d_strstreambackend->clear(); s_instance->d_currentoutput = s_instance->d_strstreambackend; } s_instance->outputHead("", true); s_instance->outputMsg(Flags::OVERWRITE, f, r...); } template inline void Logger::message(First const &f, Rest... r) // static { messagePre(); //outputHead("[MESSAGE] ", "[MESSAGE] "); s_instance->outputHead("", false, {"", ": "}); s_instance->outputMsg(Flags::NONE, f, r...); } template inline void Logger::message_start(First const &f, Rest... r) // static { messagePre(); s_instance->d_dangling = true; //outputHead("[MESSAGE] ", "[MESSAGE] "); s_instance->outputHead("", false, {"", ": "}); s_instance->outputMsg(Flags::NONEWLINE, f, r...); } inline void Logger::message_start() // static { message_start(""); } template inline void Logger::message_continue(First const &f, Rest... r) // static { s_instance->outputHead("", false, {"", ": "}); s_instance->outputMsg(Flags::NONEWLINE, f, r...); } template inline void Logger::message_end(First const &f, Rest... r) // static { s_instance->d_dangling = false; s_instance->outputHead("", false, {"", ": "}); s_instance->outputMsg(Flags::NONE, f, r...); } inline void Logger::message_end() // static { s_instance->d_dangling = false; message(""); } template inline void Logger::warning(First const &f, Rest... r) // static { messagePre(); //outputHead("[WARNING] ", "[\033[38;5;37mWARNING\033[0m] "); s_instance->outputHead("Warning", false, {"[", "]: "}, std::make_pair("\033[1m", "\033[0m")); s_instance->outputMsg(Flags::NONE, f, r...); } template inline void Logger::warning_start(First const &f, Rest... r) // static { messagePre(); s_instance->outputHead("Warning", false, {"[", "]: "}, std::make_pair("\033[1m", "\033[0m")); s_instance->outputMsg(Flags::NONEWLINE, f, r...); } template inline void Logger::warning_indent(First const &f, Rest... r) // static { messagePre(); s_instance->outputHead(" ", false, {" ", " "}); s_instance->outputMsg(Flags::NONE, f, r...); } template inline void Logger::error(First const &f, Rest... r) // static { messagePre(); //outputHead("[ ERROR ] ", "[ \033[1;31mERROR\033[0m ] "); s_instance->outputHead("Error", false, {"[", "]: "}, std::make_pair("\033[1m", "\033[0m")); s_instance->outputMsg(Flags::NONE, f, r...); } template inline void Logger::error_start(First const &f, Rest... r) // static { messagePre(); s_instance->outputHead("Error", false, {"[", "]: "}, std::make_pair("\033[1m", "\033[0m")); s_instance->outputMsg(Flags::NONEWLINE, f, r...); } template inline void Logger::error_indent(First const &f, Rest... r) // static { messagePre(); s_instance->outputHead(" ", false, {" ", " "}); s_instance->outputMsg(Flags::NONE, f, r...); } template inline void Logger::output_indent(int indent, First const &f, Rest... r) // static { messagePre(); s_instance->outputHead(std::string(indent, ' ')); s_instance->outputMsg(Flags::NONE, f, r...); } template inline void Logger::outputMsg(Flags flags, First const &f, Rest... r) { if (d_currentoutput) *(d_currentoutput) << f; std::cout << f; s_instance->outputMsg(flags, r...); } template inline void Logger::outputMsg(Flags flags, T const &t) { if (d_currentoutput) { *(d_currentoutput) << t; if (!(flags & Flags::NONEWLINE)) [[likely]] *(d_currentoutput) << "\n"; } if (flags & Flags::OVERWRITE || flags & Flags::NONEWLINE) [[unlikely]] std::cout << t << std::flush; else std::cout << t << std::endl; } template inline void Logger::outputMsg(Flags flags, VECTOR const &vec, Rest... r) { if (d_currentoutput) for (unsigned int i = 0; i < vec.data.size(); ++i) *(d_currentoutput) << vec.data[i] << ((i < vec.data.size() - 1) ? vec.delim : ""); for (unsigned int i = 0; i < vec.data.size(); ++i) std::cout << vec.data[i] << ((i < vec.data.size() - 1) ? vec.delim : ""); outputMsg(flags, r...); } template inline void Logger::outputMsg(Flags flags, VECTOR const &vec) { if (d_currentoutput) { for (unsigned int i = 0; i < vec.data.size(); ++i) *(d_currentoutput) << vec.data[i] << ((i < vec.data.size() - 1) ? vec.delim : ""); if (!(flags & Flags::NONEWLINE)) [[likely]] *(d_currentoutput) << "\n"; } for (unsigned int i = 0; i < vec.data.size(); ++i) std::cout << vec.data[i] << ((i < vec.data.size() - 1) ? vec.delim : ""); if (flags & Flags::OVERWRITE || flags & Flags::NONEWLINE) [[unlikely]] std::cout << std::flush; else std::cout << std::endl; } template inline void Logger::outputMsg(Flags flags, std::vector const &vec, Rest... r) { outputMsg(flags, VECTOR(vec, ","), r...); } template inline void Logger::outputMsg(Flags flags, std::vector const &vec) { outputMsg(flags, VECTOR(vec, ",")); } template inline void Logger::outputMsg(Flags flags, Logger::ControlChar const &c, Rest... r) { if (d_controlcodessupported) std::cout << c.code; s_instance->outputMsg(flags, r...); } inline void Logger::outputMsg(Flags flags, Logger::ControlChar const &c) { if (d_currentoutput) if (!(flags & Flags::NONEWLINE)) [[likely]] *(d_currentoutput) << "\n"; if (flags & Flags::OVERWRITE || flags & Flags::NONEWLINE) [[unlikely]] std::cout << (d_controlcodessupported ? c.code : "") << std::flush; else std::cout << (d_controlcodessupported ? c.code : "") << std::endl; } template inline void Logger::outputMsg(Flags flags, Control c, Rest... r) { // (no control codes to file) if (d_controlcodessupported) { switch(c) { case Control::BOLD: std::cout << "\033[1m"; break; case Control::NORMAL: std::cout << "\033[0m"; break; case Control::ENDOVERWRITE: // not likely here if (flags & Flags::OVERWRITE) { d_overwriting = false; d_currentoutput = s_instance->d_file; if (d_strstreambackend && d_strstreambackend->tellp() != 0) { (*d_currentoutput) << s_instance->d_strstreambackend->str() << "\n"; d_strstreambackend->str(""); d_strstreambackend->clear(); } std::cout << std::endl; } break; } } outputMsg(flags, r...); } inline void Logger::outputMsg(Flags flags, Control c) { // (no control codes to file) if (d_controlcodessupported) { switch(c) { case Control::BOLD: std::cout << "\033[1m"; break; case Control::NORMAL: std::cout << "\033[0m"; break; case Control::ENDOVERWRITE: if (flags & Flags::OVERWRITE) { d_overwriting = false; d_currentoutput = s_instance->d_file; if (d_strstreambackend && d_strstreambackend->tellp() != 0) { (*d_currentoutput) << s_instance->d_strstreambackend->str() << "\n"; d_strstreambackend->str(""); d_strstreambackend->clear(); } std::cout << std::endl; } break; } } if (flags & Flags::OVERWRITE || flags & Flags::NONEWLINE) [[unlikely]] std::cout << std::flush; else std::cout << std::endl; } inline void Logger::warnOnce(std::string const &w, bool error, std::string::size_type sub_id) { ensureLogger(); if (s_instance->d_warningsgiven.find(w.substr(0, sub_id)) == s_instance->d_warningsgiven.end()) { if (error) Logger::error(w); else Logger::warning(w); s_instance->d_warningsgiven.emplace(w, 0, sub_id); } } #endif signalbackup-tools-20250313-1/logger/outputhead.cc000066400000000000000000000040041476450434500216740ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "logger.h" void Logger::outputHead(std::string const &file, std::string const &standardout, bool overwrite, std::pair const &prepost, std::pair const &control) { if (d_currentoutput) { if (!file.empty()) *(d_currentoutput) << prepost.first; *(d_currentoutput) << file; if (d_usetimestamps) dispTime(*(d_currentoutput)); if (!file.empty()) *(d_currentoutput) << prepost.second; } if (overwrite) std::cout << (d_controlcodessupported ? "\33[2K\r" : "\r"); if (!standardout.empty()) std::cout << prepost.first; // print any control codes if supported if (d_controlcodessupported) std::cout << control.first; std::cout << standardout; // print any control codes if supported if (d_controlcodessupported) std::cout << control.second; if (d_usetimestamps) dispTime(std::cout); if (!standardout.empty()) std::cout << prepost.second; } void Logger::outputHead(std::string const &head, bool overwrite, std::pair const &prepost, std::pair const &control) { outputHead(head, head, overwrite, prepost, control); } signalbackup-tools-20250313-1/logger/statics.cc000066400000000000000000000014641476450434500211730ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "logger.h" std::unique_ptr Logger::s_instance(nullptr); signalbackup-tools-20250313-1/logger/supportsansi.cc000066400000000000000000000031411476450434500222650ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "logger.h" // This function was taken from https://github.com/agauniyal/rang/ // Used here to (poorly!) detect support for ansi escape codes bool Logger::supportsAnsi() { #if defined(_WIN32) || defined(__MINGW64__) HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); DWORD mode = 0; GetConsoleMode(hConsole, &mode); return mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING; #endif static const bool result = [] { const char *Terms[] = { "ansi", "color", "console", "cygwin", "gnome", "konsole", "kterm", "linux", "msys", "putty", "rxvt", "screen", "vt100", "xterm" }; const char *env_p = std::getenv("TERM"); if (env_p == nullptr) return false; return std::any_of(std::begin(Terms), std::end(Terms), [&](const char *term) { return std::strstr(env_p, term) != nullptr; }); }(); return result; } signalbackup-tools-20250313-1/main.cc000066400000000000000000000653631476450434500171760ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include #include #if __cpp_lib_span >= 202002L #include #endif #include "main.h" #include "arg/arg.h" #include "common_be.h" #include "signalbackup/signalbackup.h" #include "logger/logger.h" #include "desktopdatabase/desktopdatabase.h" #include "signalplaintextbackupdatabase/signalplaintextbackupdatabase.h" #include "jsondatabase/jsondatabase.h" #include "dummybackup/dummybackup.h" #if __has_include("autoversion.h") #include "autoversion.h" #endif int main(int argc, char *argv[]) { #if defined(_WIN32) || defined(__MINGW64__) // set utf8 output unsigned int oldcodepage = GetConsoleOutputCP(); SetConsoleOutputCP(65001); // enable ansi escape codes HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); DWORD mode = 0; GetConsoleMode(hConsole, &mode); mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; SetConsoleMode(hConsole, mode); #endif Arg arg(argc, argv); if (!arg.logfile().empty()) Logger::setFile(arg.logfile()); #ifdef VERSIONDATE #if defined(_WIN32) || defined(__MINGW64__) Logger::message("signalbackup-tools (", argv[0], ") source version ", VERSIONDATE, " (Win)", " (SQLite: ", SQLITE_VERSION, ", OpenSSL: ", OPENSSL_VERSION_TEXT, ")"); #else Logger::message("signalbackup-tools (", argv[0], ") source version ", VERSIONDATE, " (SQLite: ", SQLITE_VERSION, ", OpenSSL: ", OPENSSL_VERSION_TEXT, ")"); #endif #endif if (!arg.ok()) { //std::cout << "Error parsing arguments" << std::endl; //std::cout << "Try '" << argv[0] << " --help' for available options" << std::endl; Logger::error("Failed to parse arguments"); Logger::error_indent("Try '", argv[0], " --help' for available options"); return 1; } if (arg.verbose()) [[unlikely]] Logger::message("Parsed command line arguments."); //std::cout << "Parsed command line arguments." << std::endl; if (arg.help()) { arg.usage(); return 0; } //**** OPTIONS THAT DO NOT REQUIRE SIGNAL BACKUP INPUT ****// std::unique_ptr ddb; std::unique_ptr ptdb; auto initDesktopDatabase = [&]() { if (!ddb) ddb.reset(new DesktopDatabase(arg.desktopdirs_1(), arg.desktopdirs_2(), arg.rawdesktopdb(), arg.desktopkey(), arg.verbose(), arg.ignorewal(), arg.desktopdbversion(), arg.truncate(), arg.showdesktopkey(), arg.dbusverbose())); return ddb->ok(); }; #if __cpp_lib_span >= 202002L auto initPlaintextDatabase = [&](std::span const &xmlfiles) #else auto initPlaintextDatabase = [&](std::vector const &xmlfiles) #endif { if (!ptdb) ptdb.reset(new SignalPlaintextBackupDatabase(xmlfiles, arg.truncate(), arg.verbose(), arg.mapxmlcontactnames(), arg.mapxmlcontactnamesfromfile(), arg.mapxmladdresses(), arg.mapxmladdressesfromfile(), arg.setcountrycode(), arg.xmlautogroupnames())); return ptdb->ok(); }; if (!arg.generatedummy().empty()) { DummyBackup d(arg.verbose(), arg.truncate(), arg.showprogress()); if (!d.ok() || !d.exportBackup(arg.generatedummy(), arg.opassphrase(), arg.overwrite(), SignalBackup::DROPATTACHMENTDATA, false /*onlydb*/)) return 1; } // show desktop key if (arg.showdesktopkey()) if (!initDesktopDatabase()) return 1; // run desktop sqlquery if (!arg.rundtsqlquery().empty()) { if (!initDesktopDatabase()) return 1; for (auto const &q : arg.rundtsqlquery()) ddb->runQuery(q, arg.querymode()); } if (!arg.rundtprettysqlquery().empty()) { if (!initDesktopDatabase()) return 1; for (auto const &q : arg.rundtprettysqlquery()) ddb->runQuery(q, "pretty"); } if (!arg.dumpdesktopdb().empty()) { if (!initDesktopDatabase()) return 1; if (!ddb->dumpDb(arg.dumpdesktopdb(), arg.overwrite())) return 1; } if (!arg.listjsonchats().empty()) { JsonDatabase jdb(arg.listjsonchats(), arg.verbose(), arg.truncate()); if (!jdb.ok()) return 1; jdb.listChats(); } if (!arg.listxmlcontacts().empty()) { if (!initPlaintextDatabase(arg.listxmlcontacts())) return 1; ptdb->listContacts(); } if (!arg.exportdesktophtml().empty() || !arg.exportdesktoptxt().empty()) { if (!initDesktopDatabase()) return 1; DummyBackup dummydb(ddb, arg.verbose(), arg.truncate(), arg.showprogress()); if (!dummydb.ok()) { if (arg.verbose()) [[unlikely]] Logger::error("DummyBackup not initialized ok"); return 1; } if (!dummydb.importFromDesktop(ddb, true /*arg.skipmessagereorder()*/, arg.limittodates(), true /*addincompletedata*/, false /*importcontacts*/, false /*autolimittodates*/, true /*importstickers*/, arg.setselfid(), true /*targetisdummy*/)) return 1; if (!arg.exportdesktophtml().empty()) if (!dummydb.exportHtml(arg.exportdesktophtml(), {} /*limittothreads*/, arg.limittodates(), arg.split_by(), (arg.split_bool() ? arg.split() : -1), arg.setselfid(), arg.includecalllog(), arg.searchpage(), arg.stickerpacks(), arg.migratedb(), arg.overwrite(), arg.append(), arg.light(), arg.themeswitching(), arg.addexportdetails(), arg.includeblockedlist(), arg.includefullcontactlist(), false /*arg.includesettings()*/, arg.includereceipts(), arg.originalfilenames(), arg.linkify(), arg.chatfolders(), arg.compactfilenames(), arg.pagemenu(), arg.htmlignoremediatypes())) return 1; if (!arg.exportdesktoptxt().empty()) if (!dummydb.exportTxt(arg.exportdesktoptxt(), {} /*limittothreads*/, arg.limittodates(), arg.setselfid(), arg.migratedb(), arg.overwrite())) return 1; } if (!arg.exportplaintextbackuphtml().empty()) { // skip last entry, it is the output #if __cpp_lib_span >= 202002L if (!initPlaintextDatabase(std::span(arg.exportplaintextbackuphtml().begin(), arg.exportplaintextbackuphtml().end() - 1))) #else std::vector xmlfiles(arg.exportplaintextbackuphtml().begin(), arg.exportplaintextbackuphtml().end() - 1); if (!initPlaintextDatabase(xmlfiles)) #endif return 1; DummyBackup dummydb(ptdb, arg.setselfid(), arg.verbose(), arg.truncate(), arg.showprogress()); if (!dummydb.ok()) return 1; if (!dummydb.importFromPlaintextBackup(ptdb, true /*arg.skipmessagereorder()*/, arg.mapxmlcontacts(), arg.limittodates(), arg.selectxmlchats(), true /*addincompletedata*/, arg.xmlmarkdelivered(), arg.xmlmarkread(), false /*autolimittodates*/, arg.setselfid(), true /*isdummydb*/)) return 1; if (!dummydb.exportHtml(arg.exportplaintextbackuphtml().back(), {} /*limittothreads*/, arg.limittodates(), arg.split_by(), (arg.split_bool() ? arg.split() : -1), arg.setselfid(), arg.includecalllog(), arg.searchpage(), arg.stickerpacks(), arg.migratedb(), arg.overwrite(), arg.append(), arg.light(), arg.themeswitching(), arg.addexportdetails(), arg.includeblockedlist(), arg.includefullcontactlist(), false /*arg.includesettings()*/, arg.includereceipts(), arg.originalfilenames(), arg.linkify(), arg.chatfolders(), arg.compactfilenames(), arg.pagemenu(), arg.htmlignoremediatypes())) return 1; } // dump desktop attachments... //***** *****// if (!arg.input_required() && arg.input().empty()) // no input is required -> all following operations require it return 0; // -> none of the following was requested (but still decode if // input was provided) if (arg.input().empty()) // at the very least an input file is needed { Logger::error("No input provided."); Logger::error_indent("Run with `", argv[0], " [] [OPTIONS]'"); Logger::error_indent("Try '", argv[0], " --help' for available options"); return 1; } bool ipw_interactive = false; if ((arg.passphrase().empty() || arg.interactive()) && // prompt for input passphrase !bepaald::isDir(arg.input())) { std::string pw; Logger::message_start("Please provide passphrase for input file '", arg.input(), "': "); if (!getPassword(&pw)) { Logger::error("Failed to set passphrase"); return 1; } arg.setpassphrase(pw); ipw_interactive = true; } if (!arg.source().empty() && (arg.interactive() || arg.sourcepassphrase().empty())) { std::string spw; Logger::message_start("Please provide passphrase for source file '", arg.source(), "': "); if (!getPassword(&spw)) { Logger::error("Failed to set passphrase"); return 1; } arg.setsourcepassphrase(spw); } // Ask for output password if // output is written // AND its a regular file (not dir) // AND input password was not _initially_ set // AND either interactive is requested, output password was not provided if (!arg.output().empty() && ((bepaald::fileOrDirExists(arg.output()) && !bepaald::isDir(arg.output())) || (!bepaald::fileOrDirExists(arg.output()) && (arg.output().back() != '/' && arg.output().back() != std::filesystem::path::preferred_separator))) && ipw_interactive && (arg.interactive() || arg.opassphrase().empty())) { std::string opw; Logger::message_start("Please provide passphrase for output file '", arg.output(), "' (leave empty to use input passphrase): "); if (!getPassword(&opw)) { Logger::error("Failed to set passphrase"); return 1; } arg.setopassphrase(opw); } // check output exists (file exists OR dir is not empty) if (!arg.output().empty() && bepaald::fileOrDirExists(arg.output()) && ((!bepaald::isDir(arg.output()) || (/*bepaald::isDir(arg.output()) && */!bepaald::isEmpty(arg.output()))) && !arg.overwrite())) { if (bepaald::isDir(arg.output())) Logger::error("Output directory `", arg.output(), "' not empty. Use --overwrite to clear contents before export."); else Logger::error("Output file `", arg.output(), "' exists. Use --overwrite to overwrite."); return 1; } MEMINFO("Start of program, before opening input"); // open input if (arg.verbose()) [[unlikely]] Logger::message("Opening input"); std::unique_ptr sb(new SignalBackup(arg.input(), arg.passphrase(), arg.verbose(), arg.truncate(), arg.showprogress(), arg.replaceattachments_bool(), arg.assumebadframesizeonbadmac(), arg.editattachmentsize(), arg.stoponerror(), arg.fulldecode())); if (!sb->ok()) { Logger::error("Failed to open backup"); return 1; } if (arg.verbose()) [[unlikely]] Logger::message("Input opened successfully"); MEMINFO("Input opened"); std::vector limittothreads = arg.limittothreads(); if (!addThreadIdsFromString(sb.get(), arg.limittothreadsbyname(), &limittothreads)) return 1; if (arg.listthreads()) sb->listThreads(); if (arg.listrecipients()) sb->listRecipients(); if (arg.showdbinfo()) sb->showDBInfo(); if (!arg.source().empty()) { SignalBackup src(arg.source(), arg.sourcepassphrase(), arg.verbose(), arg.truncate(), arg.showprogress(), !arg.replaceattachments().empty()); std::vector threads = arg.importthreads(); if (threads.size() == 1 && threads[0] == -1) // import all threads! { MEMINFO("Before first time reading source"); Logger::message("Requested ALL threads, reading source to get thread list"); if (!src.ok()) { Logger::error("Failed to open source database"); return 1; } MEMINFO("After first time reading source"); //src->summarize(); //sourcesummarized = true; Logger::message("Getting list of thread id's..."); threads = src.threadIds(); // std::cout << "Got: " << std::flush; // for (unsigned int i = 0; i < threads.size(); ++i) // std::cout << threads[i] << ((i < threads.size() - 1) ? "," : "\n"); Logger::message("Got: ", threads); } // add any threads listed by thread name if (arg.importthreadsbyname().size()) if (!addThreadIdsFromString(&src, arg.importthreadsbyname(), &threads)) return 1; for (unsigned int i = 0; i < threads.size(); ++i) { MEMINFO("Before reading source: ", i + 1, "/", threads.size()); Logger::message("\nImporting thread ", threads[i], " (", i + 1, "/", threads.size(), ") from source file: ", arg.source()); SignalBackup sourcecopy(src); if (!sourcecopy.ok()) { Logger::error("Failed to open source database"); return 1; } // if (!sourcesummarized) // { // source->summarize(); // sourcesummarized = true; // } MEMINFO("After reading source: ", i + 1, "/", threads.size(), " before import"); if (!sb->importThread(&sourcecopy, threads[i])) { Logger::error("A fatal error occurred while trying to import thread ", threads[i], ". Aborting"); //std::cout << "A fatal error occurred while trying to import thread " << threads[i] << ". Aborting" << std::endl; return 1; } MEMINFO("After import"); } } if (arg.importfromdesktop()) { if (!initDesktopDatabase()) return 1; MEMINFO("Before importfromdesktop"); if (!sb->importFromDesktop(ddb, arg.skipmessagereorder(), arg.limittodates(), (arg.addincompletedataforhtmlexport() | arg.importdesktopcontacts()), arg.importdesktopcontacts(), arg.autolimitdates(), arg.importstickers(), arg.setselfid(), arg.targetisdummy())) return 1; MEMINFO("After importfromdesktop"); } if (!arg.importplaintextbackup().empty()) { if (!initPlaintextDatabase(arg.importplaintextbackup())) return 1; if (!sb->importFromPlaintextBackup(ptdb, arg.skipmessagereorder(), arg.mapxmlcontacts(), arg.limittodates(), arg.selectxmlchats(), arg.addincompletedataforhtmlexport(), arg.xmlmarkdelivered(), arg.xmlmarkread(), arg.autolimitdates(), arg.setselfid(), arg.targetisdummy())) return 1; } if (!arg.importtelegram().empty()) if (!sb->importTelegramJson(arg.importtelegram(), arg.selectjsonchats(), arg.mapjsoncontacts(), arg.preventjsonmapping(), arg.jsonprependforward(), arg.skipmessagereorder(), arg.jsonmarkdelivered(), arg.jsonmarkread(), arg.setselfid())) return 1; if (arg.removedoubles_bool()) sb->removeDoubles(arg.removedoubles()); if (!arg.croptodates().empty()) { if (arg.croptodates().size() % 2 != 0) { Logger::error("Wrong number of date-strings to croptodate"); return 1; } std::vector> dates; for (unsigned int i = 0; i < arg.croptodates().size(); i += 2) dates.push_back({arg.croptodates()[i], arg.croptodates()[i + 1]}); sb->cropToDates(dates); // e.g.: sb->cropToDates({{"2019-09-18 00:00:00", "2020-09-18 00:00:00"}}); } if (!arg.croptothreads().empty() || !arg.croptothreadsbyname().empty()) { std::vector threads = arg.croptothreads(); if (!addThreadIdsFromString(sb.get(), arg.croptothreadsbyname(), &threads)) return 1; sb->cropToThread(threads); } if (!arg.mergerecipients().empty()) { Logger::message("Merging recipients..."); if (!sb->mergeRecipients(arg.mergerecipients())) return 1; } if (!arg.mergegroups().empty()) { Logger::message("Merging groups..."); sb->mergeGroups(arg.mergegroups()); } if (!arg.dumpmedia().empty()) if (!sb->dumpMedia(arg.dumpmedia(), arg.limittodates(), limittothreads, arg.excludestickers(), arg.overwrite())) return 1; if (!arg.dumpavatars().empty()) if (!sb->dumpAvatars(arg.dumpavatars(), arg.limitcontacts(), arg.overwrite())) return 1; if (arg.deleteattachments() || !arg.replaceattachments().empty()) { if (!sb->deleteAttachments(arg.onlyinthreads(), arg.onlyolderthan(), arg.onlynewerthan(), arg.onlylargerthan(), arg.onlytype(), arg.appendbody(), arg.prependbody(), arg.replaceattachments())) return 1; } // if (!arg.importwachat().empty()) // if (!sb->importWAChat(arg.importwachat(), arg.setwatimefmt(), arg.setselfid())) // return 1; if (!arg.setchatcolors().empty()) if (!sb->setChatColors(arg.setchatcolors())) return 1; if (!arg.runsqlquery().empty()) for (unsigned int i = 0; i < arg.runsqlquery().size(); ++i) sb->runQuery(arg.runsqlquery()[i], arg.querymode()); if (!arg.runprettysqlquery().empty()) for (unsigned int i = 0; i < arg.runprettysqlquery().size(); ++i) sb->runQuery(arg.runprettysqlquery()[i], "pretty"); if (!arg.exporthtml().empty()) if (!sb->exportHtml(arg.exporthtml(), limittothreads, arg.limittodates(), arg.split_by(), (arg.split_bool() ? arg.split() : -1), arg.setselfid(), arg.includecalllog(), arg.searchpage(), arg.stickerpacks(), arg.migratedb(), arg.overwrite(), arg.append(), arg.light(), arg.themeswitching(), arg.addexportdetails(), arg.includeblockedlist(), arg.includefullcontactlist(), arg.includesettings(), arg.includereceipts(), arg.originalfilenames(), arg.linkify(), arg.chatfolders(), arg.compactfilenames(), arg.pagemenu(), arg.htmlignoremediatypes())) return 1; if (!arg.exporttxt().empty()) if (!sb->exportTxt(arg.exporttxt(), limittothreads, arg.limittodates(), arg.setselfid(), arg.migratedb(), arg.overwrite())) return 1; if (!arg.exportcsv().empty()) for (unsigned int i = 0; i < arg.exportcsv().size(); ++i) sb->exportCsv(arg.exportcsv()[i].second, arg.exportcsv()[i].first, arg.overwrite()); if (!arg.exportxml().empty()) if (!sb->exportXml(arg.exportxml(), arg.overwrite(), arg.setselfid(), arg.includemms(), SignalBackup::DROPATTACHMENTDATA)) { Logger::error("Failed to export backup to '", arg.exportxml(), "'"); return 1; } // // temporary, to generate truncated backup's missing data from Signal Desktop database INCOMPLETE // if (!arg.hhenkel().empty()) // { // sb->hhenkel(arg.hhenkel()); // } // temporary, to switch sender and recipient in single one-to-one conversation INCOMPLETE if (arg.hiperfall() != -1) if (!sb->hiperfall(arg.hiperfall(), arg.setselfid())) { std::cout << "Some error occurred..." << std::endl; return 1; } // // temporary, to import messages from truncated database into older, but complete database // if (!arg.sleepyh34d().empty()) // { // if (!sb->sleepyh34d(arg.sleepyh34d()[0], (arg.sleepyh34d().size() > 1) ? arg.sleepyh34d()[1] : arg.passphrase())) // { // std::cout << "Error during import" << std::endl; // return 1; // } // } // temporary, if (arg.arc() != -1) { if (!sb->arc(arg.arc(), arg.setselfid())) { Logger::error("Failed somehow"); return 1; } } // // temporary, to investigate #95 // if (!arg.carowit_1().empty()) // return sb->carowit(arg.carowit_1(), arg.carowit_2()); if (arg.scanmissingattachments()) sb->scanMissingAttachments(); if (arg.findrecipient() != -1) sb->findRecipient(arg.findrecipient()); if (arg.scramble()) sb->scramble(); if (arg.reordermmssmsids() || !arg.source().empty()) // reorder mms after messing with mms._id if (!sb->reorderMmsSmsIds()) { Logger::error("reordering mms"); return 1; } if (arg.checkdbintegrity()) sb->checkDbIntegrity(); // if (arg.devcustom()) // { // sb->devCustom(); // return 0; // } /* CUSTOM */ if (arg.custom_hugogithubs()) if (!sb->custom_hugogithubs()) { Logger::error("An error occurred running custom function"); return 1; } // if (arg.migrate_to_191_CUSTOM()) // if (!sb->migrate_to_191_CUSTOM(arg.setselfid())) // { // Logger::error("Migration failed"); // return 1; // } if (arg.migrate_to_191()) if (!sb->migrate_to_191(arg.setselfid())) { Logger::error("Migration failed"); return 1; } MEMINFO("Before output"); // export output if (!arg.output().empty()) { sb->checkDbIntegrity(true); if (!sb->exportBackup(arg.output(), arg.opassphrase(), arg.overwrite(), SignalBackup::DROPATTACHMENTDATA, arg.onlydb())) { Logger::error("Failed to export backup to '", arg.output(), "'"); return 1; } } MEMINFO("After output"); #if defined(_WIN32) || defined(__MINGW64__) SetConsoleOutputCP(oldcodepage); #endif MEMINFO("At program end"); return 0; } bool addThreadIdsFromString(SignalBackup const *const backup, std::vector const &names, std::vector *threads) { for (unsigned int i = 0; i < names.size(); ++i) { long long int r = backup->getRecipientIdFromName(names[i], true); if (r == -1) r = backup->getRecipientIdFromPhone(names[i], true); if (r == -1) r = backup->getRecipientIdFromUsername(names[i], true); if (r == -1) { Logger::error("Failed to find threadId for recipient '", names[i], "'"); return false; } long long int t = backup->getThreadIdFromRecipient(r); if (t == -1) { Logger::error("Failed to find threadId for recipient '", names[i], "'"); return false; } if (!bepaald::contains(threads, t)) threads->push_back(t); } std::sort(threads->begin(), threads->end()); return true; } /* Database version notes In database versions <= 23: recipients_ids were phone numbers (eg "+31601513210" or "__textsecure_group__!...") Avatars were linked to these contacts by AvatarFrame::name() which was also phone number -> "+31601513210" Groups had avatar data inside sqltable (groups.avatar, groups.group_id == "__textsecure_group__!...") 23 < dbv <= 27: recipient ids were just id (eg "4") Avatars were still identified by AvatarFrame::name() -> phone number Groups had avatar data inside sqltable (groups.avatar, groups.group_id == "__textsecure_group__!...") 27 < dbv < 54: recipient ids were just id (eg "4") Avatars were identified by AvatarFrame::recipient() -> "4" Groups had avatar data inside sqltable (groups.avatar, groups.group_id == "__textsecure_group__!...") >= 54: Same, groups have avatar in separate AvatarFrame, linked via groups.group_id == recipient.group_id -> recipient._id == AvatarFrame.recipient() */ /* OLD GROUPS STATUS MESSAGES Signal-Android/libsignal/service/src/main/proto/SignalService.proto: message AttachmentPointer { enum Flags { VOICE_MESSAGE = 1; } optional fixed64 id = 1; optional string contentType = 2; optional bytes key = 3; optional uint32 size = 4; optional bytes thumbnail = 5; optional bytes digest = 6; optional string fileName = 7; optional uint32 flags = 8; optional uint32 width = 9; optional uint32 height = 10; } message GroupContext { enum Type { UNKNOWN = 0; UPDATE = 1; DELIVER = 2; QUIT = 3; REQUEST_INFO = 4; } optional bytes id = 1; optional Type type = 2; optional string name = 3; repeated string members = 4; optional AttachmentPointer avatar = 5; } */ /* Notes on importing attachments - all values are imported, but "_data", "thumbnail" and "data_random" are reset by importer. thumbnail is set to NULL, so probably a good idea to unset aspect_ratio as well? It is called "THUMBNAIL_ASPECT_RATIO" in src. _id : make sure not used already (int) mid : belongs to certain specific mms._id (int) seq : ??? take over? (int default 0) ct : type, eg "image/jpeg" (text) name : ??? not file_name, or maybe filename? (text) chset : ??? (int) cd : ??? content disposition (text) fn : ??? (text) cid : ??? (text) cl : ??? content location (text) ctt_s : ??? (int) ctt_t : ??? (text) encrypted : ??? (int) pending_push : ??? probably can just take this over, or actually make sure its false (int) _data : path to encrypted data, set when importing database (text) data_size : set to size? (int) file_name : filename? or maybe internal filename (/some/path/partXXXXXX.mms) (text) thumbnail : set to NULL by importer (text) aspect_ratio : maybe set to aspect ratio (if applicable) or Note: called THUMBNAIL_ASPECT_RATIO & THUMBNAIL = NULL (real) unique_id : take over, it has to match AttFrame value (int) digest : ??? (blob) fast_preflight : ??? (text) voice_note : indicates whether its a voice note i guess (int) data_random : ??? set by importer (blob) thumbnail_random : ??? (null) quote : indicates whether the attachment is in a quote maybe?? (int) width : width (int) height : height (int) caption : captino (text) */ signalbackup-tools-20250313-1/main.h000066400000000000000000000061561476450434500170330ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef MAIN_H_ #define MAIN_H_ #include #if defined(_WIN32) || defined(__MINGW64__) #define WIN32_LEAN_AND_MEAN 1 #include #include #else // !windows #include #include #endif class SignalBackup; inline bool getPassword(std::string *pw) { if (!pw) return false; #if defined(_WIN32) || defined(__MINGW64__) int constexpr backspace = 8; int constexpr enter = 13; #define GETCHAR _getch // we're using _getch instead of getchar because then we'd need to disable line buffering #define PUTCHAR _putch // but windows then still (incorrectly?) buffers the enter key breaking things. #else struct termios tty_attr; if (tcgetattr(STDIN_FILENO, &tty_attr) < 0) return false; const tcflag_t c_lflag = tty_attr.c_lflag; // Save old flags tty_attr.c_lflag &= ~ICANON; // disable canonicle mode (not line-buffered) tty_attr.c_lflag &= ~ECHO; // disable echo if (tcsetattr(STDIN_FILENO, 0, &tty_attr) < 0) return false; int constexpr backspace = 127; int constexpr enter = 10; #define GETCHAR getchar #define PUTCHAR putchar #endif char replacement = '*'; //for (char c = 0; (c = GETCHAR()) != enter && c != EOF;) for (char c = 0; (c = GETCHAR()) != enter && c != std::char_traits::eof();) { #if defined(_WIN32) || defined(__MINGW64__) // windows _getch() swallows C-c and C-z (even though docs say it // doesn't). So we check for them here. Not sure what the proper // response to C-z actually is... if (c == 3) // C-c GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); if (c == 26) // C-z break; #endif if (c == backspace) // backspace { PUTCHAR(0x8); PUTCHAR(' '); PUTCHAR(0x8); if (pw->size()) pw->pop_back(); continue; } if (c == ' ' || c == '-') // dont replace these common separators for readability PUTCHAR(c); else PUTCHAR(replacement); pw->push_back(c); } PUTCHAR('\n'); #if defined(_WIN32) || defined(__MINGW64__) #else // !windows // restore terminal settings tty_attr.c_lflag = c_lflag; if (tcsetattr(STDIN_FILENO, 0, &tty_attr) < 0) return false; #endif return true; } inline bool addThreadIdsFromString(SignalBackup const *const backup, std::vector const &names, std::vector *threads); #endif signalbackup-tools-20250313-1/memfiledb/000077500000000000000000000000001476450434500176525ustar00rootroot00000000000000signalbackup-tools-20250313-1/memfiledb/memfiledb.h000066400000000000000000000271221476450434500217530ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef MEMFILEDB_H_ #define MEMFILEDB_H_ #include #include #include #include #include class MemFileDB { struct MemFile { sqlite3_file base; // Base class. Must be first. unsigned char *data; uint64_t datasize; }; static sqlite3_vfs s_memfilevfs; static char constexpr s_name[] = {'M', 'e', 'm', 'f', 'i', 'l', 'e', 'V', 'F', 'S', '\0'}; public: static sqlite3_vfs *sqlite3_memfilevfs(std::pair *data); static char const *vfsName() { return s_name; } private: static int ioWrite(sqlite3_file *, void const *, int, sqlite_int64); static int ioClose(sqlite3_file *pFile); static int ioRead(sqlite3_file *pFile, void *zBuf, int iAmt, sqlite_int64 iOfst); static int ioTruncate(sqlite3_file *pFile, sqlite_int64 size); static int ioSync(sqlite3_file *pFile, int flags); static int ioFileSize(sqlite3_file *pFile, sqlite_int64 *pSize); static int ioLock(sqlite3_file *pFile, int eLock); static int ioUnlock(sqlite3_file *pFile, int eLock); static int ioCheckReservedLock(sqlite3_file *pFile, int *pResOut); static int ioFileControl(sqlite3_file *pFile, int op, void *pArg); static int ioSectorSize(sqlite3_file *pFile); static int ioDeviceCharacteristics(sqlite3_file *pFile); static int open(sqlite3_vfs *pVfs, char const *zName, sqlite3_file *pFile, int flags, int *pOutFlags); static int del(sqlite3_vfs *pVfs, char const *zPath, int dirSync); static int access(sqlite3_vfs *pVfs, char const *zPath, int flags, int *pResOut); static int fullPathname(sqlite3_vfs *pVfs, char const *zPath, int nPathOut, char *zPathOut); static sqlite3_io_methods constexpr s_io = {1, /* iVersion */ MemFileDB::ioClose, /* xClose */ MemFileDB::ioRead, /* xRead */ MemFileDB::ioWrite, /* xWrite */ MemFileDB::ioTruncate, /* xTruncate */ MemFileDB::ioSync, /* xSync */ MemFileDB::ioFileSize, /* xFileSize */ MemFileDB::ioLock, /* xLock */ MemFileDB::ioUnlock, /* xUnlock */ MemFileDB::ioCheckReservedLock, /* xCheckReservedLock */ MemFileDB::ioFileControl, /* xFileControl */ MemFileDB::ioSectorSize, /* xSectorSize */ MemFileDB::ioDeviceCharacteristics, /* xDeviceCharacteristics */ /* since we specified iversion == 1 above, the next fields actually should not exist, but just to suppress gcc warnings.... */ nullptr, /* xShmMap */ nullptr, /* xShmLock */ nullptr, /* xShmBarrier */ nullptr, /* xShmUnmap */ nullptr, /* xFetch */ nullptr, /* xUnfetch */}; }; inline int MemFileDB::ioClose(sqlite3_file *pFile [[maybe_unused]]) { //std::cout << "Called: " << __FUNCTION__ << std::endl; return SQLITE_OK; } inline int MemFileDB::ioRead(sqlite3_file *pFile, void *zBuf, int iAmt, sqlite_int64 iOfst) { //std::cout << "Called: " << __FUNCTION__ << std::endl; if (static_cast(iOfst) >= reinterpret_cast(pFile)->datasize || !reinterpret_cast(pFile)->data) { //std::cout << " !!! ERROR_READ !!!" << std::endl; return SQLITE_IOERR_READ; } int toread = iAmt; bool shortread = false; if (static_cast(iOfst + iAmt) > reinterpret_cast(pFile)->datasize) { //std::cout << "SHORTREAD" << std::endl; toread -= ((iOfst + iAmt) - reinterpret_cast(pFile)->datasize); shortread = true; } std::memcpy(zBuf, reinterpret_cast(pFile)->data + iOfst, toread); if (shortread) return SQLITE_IOERR_SHORT_READ; return SQLITE_OK; } inline int MemFileDB::ioWrite(sqlite3_file *pFile [[maybe_unused]], void const *, int, sqlite_int64) { //std::cout << "Called: " << __FUNCTION__ << std::endl; //std::cout << " !!! ERROR_WRITE !!!" << std::endl; return SQLITE_READONLY; } inline int MemFileDB::ioTruncate(sqlite3_file *pFile [[maybe_unused]], sqlite_int64 size [[maybe_unused]]) { //std::cout << "Called: " << __FUNCTION__ << std::endl; //std::cout << " !!! ERROR_TRUNC !!!" << std::endl; return SQLITE_IOERR_TRUNCATE; } inline int MemFileDB::ioSync(sqlite3_file *pFile [[maybe_unused]], int flags [[maybe_unused]]) { //std::cout << "Called: " << __FUNCTION__ << std::endl; //return SQLITE_OK; // read only, there's never anything to sync return SQLITE_IOERR_FSYNC; } inline int MemFileDB::ioFileSize(sqlite3_file *pFile, sqlite_int64 *pSize) { //std::cout << "Called: " << __FUNCTION__ << std::endl; *pSize = reinterpret_cast(pFile)->datasize; return SQLITE_OK; } inline int MemFileDB::ioLock(sqlite3_file *pFile [[maybe_unused]], int eLock [[maybe_unused]]) { //std::cout << "Called: " << __FUNCTION__ << std::endl; return SQLITE_OK; } inline int MemFileDB::ioUnlock(sqlite3_file *pFile [[maybe_unused]], int eLock [[maybe_unused]]) { //std::cout << "Called: " << __FUNCTION__ << std::endl; return SQLITE_OK; } inline int MemFileDB::ioCheckReservedLock(sqlite3_file *pFile [[maybe_unused]], int *pResOut) { //std::cout << "Called: " << __FUNCTION__ << std::endl; *pResOut = 0; return SQLITE_OK; } inline int MemFileDB::ioFileControl(sqlite3_file *pFile [[maybe_unused]], int op [[maybe_unused]], void *pArg [[maybe_unused]]) { //std::cout << "Called: " << __FUNCTION__ << std::endl; return SQLITE_NOTFOUND; } inline int MemFileDB::ioSectorSize(sqlite3_file *pFile [[maybe_unused]]) { //std::cout << "Called: " << __FUNCTION__ << std::endl; return 0; } inline int MemFileDB::ioDeviceCharacteristics(sqlite3_file *pFile [[maybe_unused]]) { //std::cout << "Called: " << __FUNCTION__ << std::endl; return 0; } inline int MemFileDB::fullPathname(sqlite3_vfs *pVfs [[maybe_unused]], /* VFS */ char const *zPath [[maybe_unused]], /* Input path (possibly a relative path) */ int nPathOut [[maybe_unused]], /* Size of output buffer in bytes */ char *zPathOut) /* Pointer to output buffer */ { //std::cout << "Called: " << __FUNCTION__ << std::endl; if (nPathOut >= static_cast(strlen(zPath))) { std::strcpy(zPathOut, zPath); return SQLITE_OK; } else return SQLITE_CANTOPEN; } inline int MemFileDB::open(sqlite3_vfs *pVfs, /* VFS */ char const *zName [[maybe_unused]], /* File to open, or 0 for a temp file */ sqlite3_file *pFile, /* Pointer to DemoFile struct to populate */ int flags [[maybe_unused]], /* Input SQLITE_OPEN_XXX flags */ int *pOutFlags) /* Output SQLITE_OPEN_XXX flags (or NULL) */ { //std::cout << "Called: " << __FUNCTION__ << std::endl; MemFile *p = reinterpret_cast(pFile); /* Populate this structure */ std::memset(p, 0, sizeof(MemFile)); if (pOutFlags) *pOutFlags = flags | SQLITE_READONLY | SQLITE_OPEN_MEMORY; p->base.pMethods = &s_io; p->data = reinterpret_cast const *>(pVfs->pAppData)->first; p->datasize = reinterpret_cast const *>(pVfs->pAppData)->second; return SQLITE_OK; } inline int MemFileDB::del(sqlite3_vfs *pVfs [[maybe_unused]], const char *zPath [[maybe_unused]], int dirSync [[maybe_unused]]) { //std::cout << "Called: " << __FUNCTION__ << std::endl; //return SQLITE_OK; //std::cout << " !!! ERROR_DEL !!!" << std::endl; return SQLITE_IOERR_DELETE; } inline int MemFileDB::access(sqlite3_vfs *pVfs, char const *zPath [[maybe_unused]], int flags [[maybe_unused]], int *pResOut) { //std::cout << "Called: " << __FUNCTION__ << std::endl; if (reinterpret_cast const *>(pVfs->pAppData)->first && reinterpret_cast const *>(pVfs->pAppData)->second > 0) { *pResOut = 0; return SQLITE_OK; } //std::cout << " !!! ERROR_ACCESS !!! " << std::endl; return SQLITE_IOERR_ACCESS; } inline sqlite3_vfs *MemFileDB::sqlite3_memfilevfs(std::pair *data) { //std::cout << "Called: " << __FUNCTION__ << std::endl; std::memset(&s_memfilevfs, 0, sizeof(s_memfilevfs)); s_memfilevfs = {1, /* iVersion */ sizeof(MemFile), /* szOsFile */ sizeof(s_name) + 8, /* mxPathname // needs enough room for 'path' (I set it to filename used in sqlite3_open() call in fullPathname()) + 8 for '-journal' (and other) suffix added by sqlite*/ 0, /* pNext */ vfsName(), /* zName */ data, /* pAppData */ open, /* xOpen */ del, /* xDelete */ access, /* xAccess */ fullPathname, /* xFullPathname */ nullptr, /* xDlOpen */ nullptr, /* xDlError */ nullptr, /* xDlSym */ nullptr, /* xDlClose */ nullptr, /* xRandomness */ nullptr, /* xSleep */ nullptr, /* xCurrentTime */ nullptr, /* xGetLastError */ /* since we specified iversion == 1 above, the next fields actually should not exist, but just to suppress gcc warnings.... */ nullptr, /* xCurrentTimeInt64 */ nullptr, /* xSetSystemCall */ nullptr, /* xGetSystemCall */ nullptr, /* xNextSystemCall */ }; return &s_memfilevfs; } #endif signalbackup-tools-20250313-1/memfiledb/statics.cc000066400000000000000000000014611476450434500216350ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "memfiledb.h" sqlite3_vfs MemFileDB::s_memfilevfs; // static signalbackup-tools-20250313-1/memsqlitedb/000077500000000000000000000000001476450434500202345ustar00rootroot00000000000000signalbackup-tools-20250313-1/memsqlitedb/memsqlitedb.h000066400000000000000000000023411476450434500227130ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef MEMSQLITEDB_H_ #define MEMSQLITEDB_H_ #include "../sqlitedb/sqlitedb.h" class MemSqliteDB : public SqliteDB { public: inline MemSqliteDB(); inline explicit MemSqliteDB(std::pair *data); ~MemSqliteDB() = default; }; inline MemSqliteDB::MemSqliteDB() : SqliteDB(":memory:") { exec("PRAGMA synchronous = OFF"); } inline MemSqliteDB::MemSqliteDB(std::pair *data) : SqliteDB(data) { exec("PRAGMA synchronous = OFF"); } #endif signalbackup-tools-20250313-1/messagerangeproto/000077500000000000000000000000001476450434500214535ustar00rootroot00000000000000signalbackup-tools-20250313-1/messagerangeproto/messagerangeproto.h000066400000000000000000000042421476450434500253530ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef MESSAGERANGEPROTO_H_ #define MESSAGERANGEPROTO_H_ #include "../protobufparser/protobufparser.h" // protospec (app/src/main/proto/Database.proto): // message BodyRangeList { // message BodyRange { // enum Style { // BOLD = 0; // ITALIC = 1; // SPOILER = 2; // STRIKETHROUGH = 3; // MONOSPACE = 4; // } // // message Button { // string label = 1; // string action = 2; // } // // int32 start = 1; // int32 length = 2; // // oneof associatedValue { // string mentionUuid = 3; // Style style = 4; // string link = 5; // Button button = 6; // } // } // repeated BodyRange ranges = 1; // } typedef ProtoBufParser ONE OF ProtoBufParser> // / BodyRange; typedef ProtoBufParser> BodyRanges; #endif signalbackup-tools-20250313-1/mimetypes/000077500000000000000000000000001476450434500177425ustar00rootroot00000000000000signalbackup-tools-20250313-1/mimetypes/mimetypes.h000066400000000000000000000024271476450434500221340ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef MIMETYPES_H_ #define MIMETYPES_H_ #include #include #include class MimeTypes { static std::map const s_mimetypemap; public: inline static std::string_view getExtension(std::string const &mime, std::string const &def = std::string()); }; inline std::string_view MimeTypes::getExtension(std::string const &mime, std::string const &def) // static { if (s_mimetypemap.find(mime.c_str()) != s_mimetypemap.end()) return s_mimetypemap.at(mime.c_str()); return def; } #endif signalbackup-tools-20250313-1/mimetypes/statics.cc000066400000000000000000000767041476450434500217410ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "mimetypes.h" /* List taken from https://svn.apache.org/viewvc/httpd/httpd/trunk/docs/conf/mime.types REVISION 1918129 and appended a little. Original header: # This file maps Internet media types to unique file extension(s). # Although created for httpd, this file is used by many software systems # and has been placed in the public domain for unlimited redisribution. # # The table below contains both registered and (common) unregistered types. # A type that has no unique extension can be ignored -- they are listed # here to guide configurations toward known types and to make it easier to # identify "new" types. File extensions are also commonly used to indicate # content languages and encodings, so choose them carefully. # # Internet media types should be registered as described in RFC 4288. # The registry is at . */ std::map const MimeTypes::s_mimetypemap = { {"application/andrew-inset","ez"}, {"application/applixware","aw"}, {"application/atom+xml","atom"}, {"application/atomcat+xml","atomcat"}, {"application/atomsvc+xml","atomsvc"}, {"application/ccxml+xml","ccxml"}, {"application/cdmi-capability","cdmia"}, {"application/cdmi-container","cdmic"}, {"application/cdmi-domain","cdmid"}, {"application/cdmi-object","cdmio"}, {"application/cdmi-queue","cdmiq"}, {"application/cu-seeme","cu"}, {"application/davmount+xml","davmount"}, {"application/docbook+xml","dbk"}, {"application/dssc+der","dssc"}, {"application/dssc+xml","xdssc"}, {"application/ecmascript","ecma"}, {"application/emma+xml","emma"}, {"application/epub+zip","epub"}, {"application/exi","exi"}, {"application/font-tdpfr","pfr"}, {"application/gml+xml","gml"}, {"application/gpx+xml","gpx"}, {"application/gxf","gxf"}, {"application/hyperstudio","stk"}, {"application/inkml+xml","ink"}, {"application/ipfix","ipfix"}, {"application/java-archive","jar"}, {"application/java-serialized-object","ser"}, {"application/java-vm","class"}, {"application/javascript","js"}, {"application/json","json"}, {"application/jsonml+json","jsonml"}, {"application/lost+xml","lostxml"}, {"application/mac-binhex40","hqx"}, {"application/mac-compactpro","cpt"}, {"application/mads+xml","mads"}, {"application/marc","mrc"}, {"application/marcxml+xml","mrcx"}, {"application/mathematica","ma"}, {"application/mathml+xml","mathml"}, {"application/mbox","mbox"}, {"application/mediaservercontrol+xml","mscml"}, {"application/metalink+xml","metalink"}, {"application/metalink4+xml","meta4"}, {"application/mets+xml","mets"}, {"application/mods+xml","mods"}, {"application/mp21","m21"}, {"application/mp4","mp4s"}, {"application/msword","doc"}, {"application/mxf","mxf"}, {"application/octet-stream","bin"}, {"application/oda","oda"}, {"application/oebps-package+xml","opf"}, {"application/ogg","ogx"}, {"application/omdoc+xml","omdoc"}, {"application/onenote","onetoc"}, {"application/oxps","oxps"}, {"application/patch-ops-error+xml","xer"}, {"application/pdf","pdf"}, {"application/pgp-encrypted","pgp"}, {"application/pgp-signature","asc"}, {"application/pics-rules","prf"}, {"application/pkcs10","p10"}, {"application/pkcs7-mime","p7m"}, {"application/pkcs7-signature","p7s"}, {"application/pkcs8","p8"}, {"application/pkix-attr-cert","ac"}, {"application/pkix-cert","cer"}, {"application/pkix-crl","crl"}, {"application/pkix-pkipath","pkipath"}, {"application/pkixcmp","pki"}, {"application/pls+xml","pls"}, {"application/postscript","ai"}, {"application/prs.cww","cww"}, {"application/pskc+xml","pskcxml"}, {"application/rdf+xml","rdf"}, {"application/reginfo+xml","rif"}, {"application/relax-ng-compact-syntax","rnc"}, {"application/resource-lists+xml","rl"}, {"application/resource-lists-diff+xml","rld"}, {"application/rls-services+xml","rs"}, {"application/rpki-ghostbusters","gbr"}, {"application/rpki-manifest","mft"}, {"application/rpki-roa","roa"}, {"application/rsd+xml","rsd"}, {"application/rss+xml","rss"}, {"application/rtf","rtf"}, {"application/sbml+xml","sbml"}, {"application/scvp-cv-request","scq"}, {"application/scvp-cv-response","scs"}, {"application/scvp-vp-request","spq"}, {"application/scvp-vp-response","spp"}, {"application/sdp","sdp"}, {"application/set-payment-initiation","setpay"}, {"application/set-registration-initiation","setreg"}, {"application/shf+xml","shf"}, {"application/smil+xml","smi"}, {"application/sparql-query","rq"}, {"application/sparql-results+xml","srx"}, {"application/srgs","gram"}, {"application/srgs+xml","grxml"}, {"application/sru+xml","sru"}, {"application/ssdl+xml","ssdl"}, {"application/ssml+xml","ssml"}, {"application/tei+xml","tei"}, {"application/thraud+xml","tfi"}, {"application/timestamped-data","tsd"}, {"application/vnd.3gpp.pic-bw-large","plb"}, {"application/vnd.3gpp.pic-bw-small","psb"}, {"application/vnd.3gpp.pic-bw-var","pvb"}, {"application/vnd.3gpp2.tcap","tcap"}, {"application/vnd.3m.post-it-notes","pwn"}, {"application/vnd.accpac.simply.aso","aso"}, {"application/vnd.accpac.simply.imp","imp"}, {"application/vnd.acucobol","acu"}, {"application/vnd.acucorp","atc"}, {"application/vnd.adobe.air-application-installer-package+zip","air"}, {"application/vnd.adobe.formscentral.fcdt","fcdt"}, {"application/vnd.adobe.fxp","fxp"}, {"application/vnd.adobe.xdp+xml","xdp"}, {"application/vnd.adobe.xfdf","xfdf"}, {"application/vnd.ahead.space","ahead"}, {"application/vnd.airzip.filesecure.azf","azf"}, {"application/vnd.airzip.filesecure.azs","azs"}, {"application/vnd.amazon.ebook","azw"}, {"application/vnd.americandynamics.acc","acc"}, {"application/vnd.amiga.ami","ami"}, {"application/vnd.android.package-archive","apk"}, {"application/vnd.anser-web-certificate-issue-initiation","cii"}, {"application/vnd.anser-web-funds-transfer-initiation","fti"}, {"application/vnd.antix.game-component","atx"}, {"application/vnd.apple.installer+xml","mpkg"}, {"application/vnd.apple.mpegurl","m3u8"}, {"application/vnd.aristanetworks.swi","swi"}, {"application/vnd.astraea-software.iota","iota"}, {"application/vnd.audiograph","aep"}, {"application/vnd.blueice.multipass","mpm"}, {"application/vnd.bmi","bmi"}, {"application/vnd.businessobjects","rep"}, {"application/vnd.chemdraw+xml","cdxml"}, {"application/vnd.chipnuts.karaoke-mmd","mmd"}, {"application/vnd.cinderella","cdy"}, {"application/vnd.claymore","cla"}, {"application/vnd.cloanto.rp9","rp9"}, {"application/vnd.clonk.c4group","c4g"}, {"application/vnd.cluetrust.cartomobile-config","c11amc"}, {"application/vnd.cluetrust.cartomobile-config-pkg","c11amz"}, {"application/vnd.commonspace","csp"}, {"application/vnd.contact.cmsg","cdbcmsg"}, {"application/vnd.cosmocaller","cmc"}, {"application/vnd.crick.clicker","clkx"}, {"application/vnd.crick.clicker.keyboard","clkk"}, {"application/vnd.crick.clicker.palette","clkp"}, {"application/vnd.crick.clicker.template","clkt"}, {"application/vnd.crick.clicker.wordbank","clkw"}, {"application/vnd.criticaltools.wbs+xml","wbs"}, {"application/vnd.ctc-posml","pml"}, {"application/vnd.cups-ppd","ppd"}, {"application/vnd.curl.car","car"}, {"application/vnd.curl.pcurl","pcurl"}, {"application/vnd.dart","dart"}, {"application/vnd.data-vision.rdz","rdz"}, {"application/vnd.dece.data","uvf"}, {"application/vnd.dece.ttml+xml","uvt"}, {"application/vnd.dece.unspecified","uvx"}, {"application/vnd.dece.zip","uvz"}, {"application/vnd.denovo.fcselayout-link","fe_launch"}, {"application/vnd.dna","dna"}, {"application/vnd.dolby.mlp","mlp"}, {"application/vnd.dpgraph","dpg"}, {"application/vnd.dreamfactory","dfac"}, {"application/vnd.ds-keypoint","kpxx"}, {"application/vnd.dvb.ait","ait"}, {"application/vnd.dvb.service","svc"}, {"application/vnd.dynageo","geo"}, {"application/vnd.ecowin.chart","mag"}, {"application/vnd.enliven","nml"}, {"application/vnd.epson.esf","esf"}, {"application/vnd.epson.msf","msf"}, {"application/vnd.epson.quickanime","qam"}, {"application/vnd.epson.salt","slt"}, {"application/vnd.epson.ssf","ssf"}, {"application/vnd.eszigno3+xml","es3"}, {"application/vnd.ezpix-album","ez2"}, {"application/vnd.ezpix-package","ez3"}, {"application/vnd.fdf","fdf"}, {"application/vnd.fdsn.mseed","mseed"}, {"application/vnd.fdsn.seed","seed"}, {"application/vnd.flographit","gph"}, {"application/vnd.fluxtime.clip","ftc"}, {"application/vnd.framemaker","fm"}, {"application/vnd.frogans.fnc","fnc"}, {"application/vnd.frogans.ltf","ltf"}, {"application/vnd.fsc.weblaunch","fsc"}, {"application/vnd.fujitsu.oasys","oas"}, {"application/vnd.fujitsu.oasys2","oa2"}, {"application/vnd.fujitsu.oasys3","oa3"}, {"application/vnd.fujitsu.oasysgp","fg5"}, {"application/vnd.fujitsu.oasysprs","bh2"}, {"application/vnd.fujixerox.ddd","ddd"}, {"application/vnd.fujixerox.docuworks","xdw"}, {"application/vnd.fujixerox.docuworks.binder","xbd"}, {"application/vnd.fuzzysheet","fzs"}, {"application/vnd.genomatix.tuxedo","txd"}, {"application/vnd.geogebra.file","ggb"}, {"application/vnd.geogebra.slides","ggs"}, {"application/vnd.geogebra.tool","ggt"}, {"application/vnd.geometry-explorer","gex"}, {"application/vnd.geonext","gxt"}, {"application/vnd.geoplan","g2w"}, {"application/vnd.geospace","g3w"}, {"application/vnd.gmx","gmx"}, {"application/vnd.google-earth.kml+xml","kml"}, {"application/vnd.google-earth.kmz","kmz"}, {"application/vnd.grafeq","gqf"}, {"application/vnd.groove-account","gac"}, {"application/vnd.groove-help","ghf"}, {"application/vnd.groove-identity-message","gim"}, {"application/vnd.groove-injector","grv"}, {"application/vnd.groove-tool-message","gtm"}, {"application/vnd.groove-tool-template","tpl"}, {"application/vnd.groove-vcard","vcg"}, {"application/vnd.hal+xml","hal"}, {"application/vnd.handheld-entertainment+xml","zmm"}, {"application/vnd.hbci","hbci"}, {"application/vnd.hhe.lesson-player","les"}, {"application/vnd.hp-hpgl","hpgl"}, {"application/vnd.hp-hpid","hpid"}, {"application/vnd.hp-hps","hps"}, {"application/vnd.hp-jlyt","jlt"}, {"application/vnd.hp-pcl","pcl"}, {"application/vnd.hp-pclxl","pclxl"}, {"application/vnd.hydrostatix.sof-data","sfd-hdstx"}, {"application/vnd.ibm.minipay","mpy"}, {"application/vnd.ibm.modcap","afp"}, {"application/vnd.ibm.rights-management","irm"}, {"application/vnd.ibm.secure-container","sc"}, {"application/vnd.iccprofile","icc"}, {"application/vnd.igloader","igl"}, {"application/vnd.immervision-ivp","ivp"}, {"application/vnd.immervision-ivu","ivu"}, {"application/vnd.insors.igm","igm"}, {"application/vnd.intercon.formnet","xpw"}, {"application/vnd.intergeo","i2g"}, {"application/vnd.intu.qbo","qbo"}, {"application/vnd.intu.qfx","qfx"}, {"application/vnd.ipunplugged.rcprofile","rcprofile"}, {"application/vnd.irepository.package+xml","irp"}, {"application/vnd.is-xpr","xpr"}, {"application/vnd.isac.fcs","fcs"}, {"application/vnd.jam","jam"}, {"application/vnd.jcp.javame.midlet-rms","rms"}, {"application/vnd.jisp","jisp"}, {"application/vnd.joost.joda-archive","joda"}, {"application/vnd.kahootz","ktz"}, {"application/vnd.kde.karbon","karbon"}, {"application/vnd.kde.kchart","chrt"}, {"application/vnd.kde.kformula","kfo"}, {"application/vnd.kde.kivio","flw"}, {"application/vnd.kde.kontour","kon"}, {"application/vnd.kde.kpresenter","kpr"}, {"application/vnd.kde.kspread","ksp"}, {"application/vnd.kde.kword","kwd"}, {"application/vnd.kenameaapp","htke"}, {"application/vnd.kidspiration","kia"}, {"application/vnd.kinar","kne"}, {"application/vnd.koan","skp"}, {"application/vnd.kodak-descriptor","sse"}, {"application/vnd.las.las+xml","lasxml"}, {"application/vnd.llamagraphics.life-balance.desktop","lbd"}, {"application/vnd.llamagraphics.life-balance.exchange+xml","lbe"}, {"application/vnd.lotus-1-2-3","123"}, {"application/vnd.lotus-approach","apr"}, {"application/vnd.lotus-freelance","pre"}, {"application/vnd.lotus-notes","nsf"}, {"application/vnd.lotus-organizer","org"}, {"application/vnd.lotus-screencam","scm"}, {"application/vnd.lotus-wordpro","lwp"}, {"application/vnd.macports.portpkg","portpkg"}, {"application/vnd.mcd","mcd"}, {"application/vnd.medcalcdata","mc1"}, {"application/vnd.mediastation.cdkey","cdkey"}, {"application/vnd.mfer","mwf"}, {"application/vnd.mfmp","mfm"}, {"application/vnd.micrografx.flo","flo"}, {"application/vnd.micrografx.igx","igx"}, {"application/vnd.mif","mif"}, {"application/vnd.mobius.daf","daf"}, {"application/vnd.mobius.dis","dis"}, {"application/vnd.mobius.mbk","mbk"}, {"application/vnd.mobius.mqy","mqy"}, {"application/vnd.mobius.msl","msl"}, {"application/vnd.mobius.plc","plc"}, {"application/vnd.mobius.txf","txf"}, {"application/vnd.mophun.application","mpn"}, {"application/vnd.mophun.certificate","mpc"}, {"application/vnd.mozilla.xul+xml","xul"}, {"application/vnd.ms-artgalry","cil"}, {"application/vnd.ms-cab-compressed","cab"}, {"application/vnd.ms-excel","xls"}, {"application/vnd.ms-excel.addin.macroenabled.12","xlam"}, {"application/vnd.ms-excel.sheet.binary.macroenabled.12","xlsb"}, {"application/vnd.ms-excel.sheet.macroenabled.12","xlsm"}, {"application/vnd.ms-excel.template.macroenabled.12","xltm"}, {"application/vnd.ms-fontobject","eot"}, {"application/vnd.ms-htmlhelp","chm"}, {"application/vnd.ms-ims","ims"}, {"application/vnd.ms-lrm","lrm"}, {"application/vnd.ms-officetheme","thmx"}, {"application/vnd.ms-pki.seccat","cat"}, {"application/vnd.ms-pki.stl","stl"}, {"application/vnd.ms-powerpoint","ppt"}, {"application/vnd.ms-powerpoint.addin.macroenabled.12","ppam"}, {"application/vnd.ms-powerpoint.presentation.macroenabled.12","pptm"}, {"application/vnd.ms-powerpoint.slide.macroenabled.12","sldm"}, {"application/vnd.ms-powerpoint.slideshow.macroenabled.12","ppsm"}, {"application/vnd.ms-powerpoint.template.macroenabled.12","potm"}, {"application/vnd.ms-project","mpp"}, {"application/vnd.ms-word.document.macroenabled.12","docm"}, {"application/vnd.ms-word.template.macroenabled.12","dotm"}, {"application/vnd.ms-works","wps"}, {"application/vnd.ms-wpl","wpl"}, {"application/vnd.ms-xpsdocument","xps"}, {"application/vnd.mseq","mseq"}, {"application/vnd.musician","mus"}, {"application/vnd.muvee.style","msty"}, {"application/vnd.mynfc","taglet"}, {"application/vnd.neurolanguage.nlu","nlu"}, {"application/vnd.nitf","ntf"}, {"application/vnd.noblenet-directory","nnd"}, {"application/vnd.noblenet-sealer","nns"}, {"application/vnd.noblenet-web","nnw"}, {"application/vnd.nokia.n-gage.data","ngdat"}, {"application/vnd.nokia.n-gage.symbian.install","n-gage"}, {"application/vnd.nokia.radio-preset","rpst"}, {"application/vnd.nokia.radio-presets","rpss"}, {"application/vnd.novadigm.edm","edm"}, {"application/vnd.novadigm.edx","edx"}, {"application/vnd.novadigm.ext","ext"}, {"application/vnd.oasis.opendocument.chart","odc"}, {"application/vnd.oasis.opendocument.chart-template","otc"}, {"application/vnd.oasis.opendocument.database","odb"}, {"application/vnd.oasis.opendocument.formula","odf"}, {"application/vnd.oasis.opendocument.formula-template","odft"}, {"application/vnd.oasis.opendocument.graphics","odg"}, {"application/vnd.oasis.opendocument.graphics-template","otg"}, {"application/vnd.oasis.opendocument.image","odi"}, {"application/vnd.oasis.opendocument.image-template","oti"}, {"application/vnd.oasis.opendocument.presentation","odp"}, {"application/vnd.oasis.opendocument.presentation-template","otp"}, {"application/vnd.oasis.opendocument.spreadsheet","ods"}, {"application/vnd.oasis.opendocument.spreadsheet-template","ots"}, {"application/vnd.oasis.opendocument.text","odt"}, {"application/vnd.oasis.opendocument.text-master","odm"}, {"application/vnd.oasis.opendocument.text-template","ott"}, {"application/vnd.oasis.opendocument.text-web","oth"}, {"application/vnd.olpc-sugar","xo"}, {"application/vnd.oma.dd2+xml","dd2"}, {"application/vnd.openofficeorg.extension","oxt"}, {"application/vnd.openxmlformats-officedocument.presentationml.presentation","pptx"}, {"application/vnd.openxmlformats-officedocument.presentationml.slide","sldx"}, {"application/vnd.openxmlformats-officedocument.presentationml.slideshow","ppsx"}, {"application/vnd.openxmlformats-officedocument.presentationml.template","potx"}, {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","xlsx"}, {"application/vnd.openxmlformats-officedocument.spreadsheetml.template","xltx"}, {"application/vnd.openxmlformats-officedocument.wordprocessingml.document","docx"}, {"application/vnd.openxmlformats-officedocument.wordprocessingml.template","dotx"}, {"application/vnd.osgeo.mapguide.package","mgp"}, {"application/vnd.osgi.dp","dp"}, {"application/vnd.osgi.subsystem","esa"}, {"application/vnd.palm","pdb"}, {"application/vnd.pawaafile","paw"}, {"application/vnd.pg.format","str"}, {"application/vnd.pg.osasli","ei6"}, {"application/vnd.picsel","efif"}, {"application/vnd.pmi.widget","wg"}, {"application/vnd.pocketlearn","plf"}, {"application/vnd.powerbuilder6","pbd"}, {"application/vnd.previewsystems.box","box"}, {"application/vnd.proteus.magazine","mgz"}, {"application/vnd.publishare-delta-tree","qps"}, {"application/vnd.pvi.ptid1","ptid"}, {"application/vnd.quark.quarkxpress","qxd"}, {"application/vnd.realvnc.bed","bed"}, {"application/vnd.recordare.musicxml","mxl"}, {"application/vnd.recordare.musicxml+xml","musicxml"}, {"application/vnd.rig.cryptonote","cryptonote"}, {"application/vnd.rim.cod","cod"}, {"application/vnd.rn-realmedia","rm"}, {"application/vnd.rn-realmedia-vbr","rmvb"}, {"application/vnd.route66.link66+xml","link66"}, {"application/vnd.sailingtracker.track","st"}, {"application/vnd.seemail","see"}, {"application/vnd.sema","sema"}, {"application/vnd.semd","semd"}, {"application/vnd.semf","semf"}, {"application/vnd.shana.informed.formdata","ifm"}, {"application/vnd.shana.informed.formtemplate","itp"}, {"application/vnd.shana.informed.interchange","iif"}, {"application/vnd.shana.informed.package","ipk"}, {"application/vnd.simtech-mindmapper","twd"}, {"application/vnd.smaf","mmf"}, {"application/vnd.smart.teacher","teacher"}, {"application/vnd.solent.sdkm+xml","sdkm"}, {"application/vnd.spotfire.dxp","dxp"}, {"application/vnd.spotfire.sfs","sfs"}, {"application/vnd.stardivision.calc","sdc"}, {"application/vnd.stardivision.draw","sda"}, {"application/vnd.stardivision.impress","sdd"}, {"application/vnd.stardivision.math","smf"}, {"application/vnd.stardivision.writer","sdw"}, {"application/vnd.stardivision.writer-global","sgl"}, {"application/vnd.stepmania.package","smzip"}, {"application/vnd.stepmania.stepchart","sm"}, {"application/vnd.sun.xml.calc","sxc"}, {"application/vnd.sun.xml.calc.template","stc"}, {"application/vnd.sun.xml.draw","sxd"}, {"application/vnd.sun.xml.draw.template","std"}, {"application/vnd.sun.xml.impress","sxi"}, {"application/vnd.sun.xml.impress.template","sti"}, {"application/vnd.sun.xml.math","sxm"}, {"application/vnd.sun.xml.writer","sxw"}, {"application/vnd.sun.xml.writer.global","sxg"}, {"application/vnd.sun.xml.writer.template","stw"}, {"application/vnd.sus-calendar","sus"}, {"application/vnd.svd","svd"}, {"application/vnd.symbian.install","sis"}, {"application/vnd.syncml+xml","xsm"}, {"application/vnd.syncml.dm+wbxml","bdm"}, {"application/vnd.syncml.dm+xml","xdm"}, {"application/vnd.tao.intent-module-archive","tao"}, {"application/vnd.tcpdump.pcap","pcap"}, {"application/vnd.tmobile-livetv","tmo"}, {"application/vnd.trid.tpt","tpt"}, {"application/vnd.triscape.mxs","mxs"}, {"application/vnd.trueapp","tra"}, {"application/vnd.ufdl","ufd"}, {"application/vnd.uiq.theme","utz"}, {"application/vnd.umajin","umj"}, {"application/vnd.unity","unityweb"}, {"application/vnd.uoml+xml","uoml"}, {"application/vnd.vcx","vcx"}, {"application/vnd.visio","vsd"}, {"application/vnd.visionary","vis"}, {"application/vnd.vsf","vsf"}, {"application/vnd.wap.wbxml","wbxml"}, {"application/vnd.wap.wmlc","wmlc"}, {"application/vnd.wap.wmlscriptc","wmlsc"}, {"application/vnd.webturbo","wtb"}, {"application/vnd.wolfram.player","nbp"}, {"application/vnd.wolfram.mathematica","nb"}, {"application/vnd.wolfram.cdf.text","cdf"}, {"application/vnd.wordperfect","wpd"}, {"application/vnd.wqd","wqd"}, {"application/vnd.wt.stf","stf"}, {"application/vnd.xara","xar"}, {"application/vnd.xfdl","xfdl"}, {"application/vnd.yamaha.hv-dic","hvd"}, {"application/vnd.yamaha.hv-script","hvs"}, {"application/vnd.yamaha.hv-voice","hvp"}, {"application/vnd.yamaha.openscoreformat","osf"}, {"application/vnd.yamaha.openscoreformat.osfpvg+xml","osfpvg"}, {"application/vnd.yamaha.smaf-audio","saf"}, {"application/vnd.yamaha.smaf-phrase","spf"}, {"application/vnd.yellowriver-custom-menu","cmp"}, {"application/vnd.zul","zir"}, {"application/vnd.zzazz.deck+xml","zaz"}, {"application/voicexml+xml","vxml"}, {"application/wasm","wasm"}, {"application/widget","wgt"}, {"application/winhlp","hlp"}, {"application/wsdl+xml","wsdl"}, {"application/wspolicy+xml","wspolicy"}, {"application/x-7z-compressed","7z"}, {"application/x-abiword","abw"}, {"application/x-ace-compressed","ace"}, {"application/x-apple-diskimage","dmg"}, {"application/x-authorware-bin","aab"}, {"application/x-authorware-map","aam"}, {"application/x-authorware-seg","aas"}, {"application/x-bcpio","bcpio"}, {"application/x-bittorrent","torrent"}, {"application/x-blorb","blb"}, {"application/x-bzip","bz"}, {"application/x-bzip2","bz2"}, {"application/x-cbr","cbr"}, {"application/x-cdlink","vcd"}, {"application/x-cfs-compressed","cfs"}, {"application/x-chat","chat"}, {"application/x-chess-pgn","pgn"}, {"application/x-conference","nsc"}, {"application/x-cpio","cpio"}, {"application/x-csh","csh"}, {"application/x-debian-package","deb"}, {"application/x-dgc-compressed","dgc"}, {"application/x-director","dir"}, {"application/x-doom","wad"}, {"application/x-dtbncx+xml","ncx"}, {"application/x-dtbook+xml","dtb"}, {"application/x-dtbresource+xml","res"}, {"application/x-dvi","dvi"}, {"application/x-envoy","evy"}, {"application/x-eva","eva"}, {"application/x-font-bdf","bdf"}, {"application/x-font-ghostscript","gsf"}, {"application/x-font-linux-psf","psf"}, {"application/x-font-pcf","pcf"}, {"application/x-font-snf","snf"}, {"application/x-font-type1","pfa"}, {"application/x-freearc","arc"}, {"application/x-futuresplash","spl"}, {"application/x-gca-compressed","gca"}, {"application/x-glulx","ulx"}, {"application/x-gnumeric","gnumeric"}, {"application/x-gramps-xml","gramps"}, {"application/x-gtar","gtar"}, {"application/x-hdf","hdf"}, {"application/x-install-instructions","install"}, {"application/x-iso9660-image","iso"}, {"application/x-java-jnlp-file","jnlp"}, {"application/x-latex","latex"}, {"application/x-lzh-compressed","lzh"}, {"application/x-mie","mie"}, {"application/x-mobipocket-ebook","prc"}, {"application/x-ms-application","application"}, {"application/x-ms-shortcut","lnk"}, {"application/x-ms-wmd","wmd"}, {"application/x-ms-wmz","wmz"}, {"application/x-ms-xbap","xbap"}, {"application/x-msaccess","mdb"}, {"application/x-msbinder","obd"}, {"application/x-mscardfile","crd"}, {"application/x-msclip","clp"}, {"application/x-msdownload","exe"}, {"application/x-msmediaview","mvb"}, {"application/x-msmetafile","wmf"}, {"application/x-msmoney","mny"}, {"application/x-mspublisher","pub"}, {"application/x-msschedule","scd"}, {"application/x-msterminal","trm"}, {"application/x-mswrite","wri"}, {"application/x-netcdf","nc"}, {"application/x-nzb","nzb"}, {"application/x-pkcs12","p12"}, {"application/x-pkcs7-certificates","p7b"}, {"application/x-pkcs7-certreqresp","p7r"}, {"application/x-rar-compressed","rar"}, {"application/x-research-info-systems","ris"}, {"application/x-sh","sh"}, {"application/x-shar","shar"}, {"application/x-shockwave-flash","swf"}, {"application/x-silverlight-app","xap"}, {"application/x-sql","sql"}, {"application/x-stuffit","sit"}, {"application/x-stuffitx","sitx"}, {"application/x-subrip","srt"}, {"application/x-sv4cpio","sv4cpio"}, {"application/x-sv4crc","sv4crc"}, {"application/x-t3vm-image","t3"}, {"application/x-tads","gam"}, {"application/x-tar","tar"}, {"application/x-tcl","tcl"}, {"application/x-tex","tex"}, {"application/x-tex-tfm","tfm"}, {"application/x-texinfo","texinfo"}, {"application/x-tgif","obj"}, {"application/x-ustar","ustar"}, {"application/x-wais-source","src"}, {"application/x-x509-ca-cert","der"}, {"application/x-xfig","fig"}, {"application/x-xliff+xml","xlf"}, {"application/x-xpinstall","xpi"}, {"application/x-xz","xz"}, {"application/x-zmachine","z1"}, {"application/xaml+xml","xaml"}, {"application/xcap-diff+xml","xdf"}, {"application/xenc+xml","xenc"}, {"application/xhtml+xml","xhtml"}, {"application/xml","xml"}, {"application/xml-dtd","dtd"}, {"application/xop+xml","xop"}, {"application/xproc+xml","xpl"}, {"application/xslt+xml","xslt"}, {"application/xspf+xml","xspf"}, {"application/xv+xml","mxml"}, {"application/yang","yang"}, {"application/yin+xml","yin"}, {"application/zip","zip"}, {"audio/aac","aac"}, {"audio/adpcm","adp"}, {"audio/basic","au"}, {"audio/midi","mid"}, {"audio/mp4","m4a"}, {"audio/mpeg","mpga"}, {"audio/ogg","oga"}, {"audio/ogg; codecs=opus","opus"}, {"audio/s3m","s3m"}, {"audio/silk","sil"}, {"audio/vnd.dece.audio","uva"}, {"audio/vnd.digital-winds","eol"}, {"audio/vnd.dra","dra"}, {"audio/vnd.dts","dts"}, {"audio/vnd.dts.hd","dtshd"}, {"audio/vnd.lucent.voice","lvp"}, {"audio/vnd.ms-playready.media.pya","pya"}, {"audio/vnd.nuera.ecelp4800","ecelp4800"}, {"audio/vnd.nuera.ecelp7470","ecelp7470"}, {"audio/vnd.nuera.ecelp9600","ecelp9600"}, {"audio/vnd.rip","rip"}, {"audio/webm","weba"}, {"audio/x-aac","aac"}, {"audio/x-aiff","aif"}, {"audio/x-caf","caf"}, {"audio/x-flac","flac"}, {"audio/x-matroska","mka"}, {"audio/x-mpegurl","m3u"}, {"audio/x-ms-wax","wax"}, {"audio/x-ms-wma","wma"}, {"audio/x-pn-realaudio","ram"}, {"audio/x-pn-realaudio-plugin","rmp"}, {"audio/x-wav","wav"}, {"audio/xm","xm"}, {"chemical/x-cdx","cdx"}, {"chemical/x-cif","cif"}, {"chemical/x-cmdf","cmdf"}, {"chemical/x-cml","cml"}, {"chemical/x-csml","csml"}, {"chemical/x-xyz","xyz"}, {"font/collection","ttc"}, {"font/otf","otf"}, {"font/ttf","ttf"}, {"font/woff","woff"}, {"font/woff2","woff2"}, {"image/avif","avif"}, {"image/bmp","bmp"}, {"image/cgm","cgm"}, {"image/g3fax","g3"}, {"image/gif","gif"}, {"image/ief","ief"}, {"image/jpeg","jpg"}, {"image/ktx","ktx"}, {"image/png","png"}, {"image/prs.btif","btif"}, {"image/sgi","sgi"}, {"image/svg+xml","svg"}, {"image/svg+xml-compressed","svgz"}, {"image/tiff","tiff"}, {"image/vnd.adobe.photoshop","psd"}, {"image/vnd.dece.graphic","uvi"}, {"image/vnd.djvu","djvu"}, {"image/vnd.dvb.subtitle","sub"}, {"image/vnd.dwg","dwg"}, {"image/vnd.dxf","dxf"}, {"image/vnd.fastbidsheet","fbs"}, {"image/vnd.fpx","fpx"}, {"image/vnd.fst","fst"}, {"image/vnd.fujixerox.edmics-mmr","mmr"}, {"image/vnd.fujixerox.edmics-rlc","rlc"}, {"image/vnd.ms-modi","mdi"}, {"image/vnd.ms-photo","wdp"}, {"image/vnd.net-fpx","npx"}, {"image/vnd.wap.wbmp","wbmp"}, {"image/vnd.xiff","xif"}, {"image/webp","webp"}, {"image/x-3ds","3ds"}, {"image/x-cmu-raster","ras"}, {"image/x-cmx","cmx"}, {"image/x-freehand","fh"}, {"image/x-icon","ico"}, {"image/x-mrsid-image","sid"}, {"image/x-pcx","pcx"}, {"image/x-pict","pic"}, {"image/x-portable-anymap","pnm"}, {"image/x-portable-bitmap","pbm"}, {"image/x-portable-graymap","pgm"}, {"image/x-portable-pixmap","ppm"}, {"image/x-rgb","rgb"}, {"image/x-tga","tga"}, {"image/x-xbitmap","xbm"}, {"image/x-xpixmap","xpm"}, {"image/x-xwindowdump","xwd"}, {"message/rfc822","eml"}, {"model/iges","igs"}, {"model/mesh","msh"}, {"model/vnd.collada+xml","dae"}, {"model/vnd.dwf","dwf"}, {"model/vnd.gdl","gdl"}, {"model/vnd.gtw","gtw"}, {"model/vnd.mts","mts"}, {"model/vnd.vtu","vtu"}, {"model/vrml","wrl"}, {"model/x3d+binary","x3db"}, {"model/x3d+vrml","x3dv"}, {"model/x3d+xml","x3d"}, {"text/cache-manifest","appcache"}, {"text/calendar","ics"}, {"text/css","css"}, {"text/csv","csv"}, {"text/html","html"}, {"text/javascript","js"}, {"text/n3","n3"}, {"text/plain","txt"}, {"text/prs.lines.tag","dsc"}, {"text/richtext","rtx"}, {"text/sgml","sgml"}, {"text/tab-separated-values","tsv"}, {"text/troff","t"}, {"text/turtle","ttl"}, {"text/uri-list","uri"}, {"text/vcard","vcard"}, {"text/vnd.curl","curl"}, {"text/vnd.curl.dcurl","dcurl"}, {"text/vnd.curl.mcurl","mcurl"}, {"text/vnd.curl.scurl","scurl"}, {"text/vnd.dvb.subtitle","sub"}, {"text/vnd.fly","fly"}, {"text/vnd.fmi.flexstor","flx"}, {"text/vnd.graphviz","gv"}, {"text/vnd.in3d.3dml","3dml"}, {"text/vnd.in3d.spot","spot"}, {"text/vnd.sun.j2me.app-descriptor","jad"}, {"text/vnd.wap.wml","wml"}, {"text/vnd.wap.wmlscript","wmls"}, {"text/x-asm","s"}, {"text/x-c","c"}, {"text/x-fortran","f"}, {"text/x-java-source","java"}, {"text/x-nfo","nfo"}, {"text/x-opml","opml"}, {"text/x-pascal","p"}, {"text/x-setext","etx"}, {"text/x-signal-plain","txt"}, // the (custom) type used for too large message bodies {"text/x-sfv","sfv"}, {"text/x-uuencode","uu"}, {"text/x-vcalendar","vcs"}, {"text/x-vcard","vcf"}, {"video/3gpp","3gp"}, {"video/3gpp2","3g2"}, {"video/h261","h261"}, {"video/h263","h263"}, {"video/h264","h264"}, {"video/jpeg","jpgv"}, {"video/jpm","jpm"}, {"video/mj2","mj2"}, {"video/mp2t","ts"}, {"video/mp4","mp4"}, {"video/mpeg","mpg"}, {"video/ogg","ogv"}, {"video/quicktime","mov"}, {"video/vnd.dece.hd","uvh"}, {"video/vnd.dece.mobile","uvm"}, {"video/vnd.dece.pd","uvp"}, {"video/vnd.dece.sd","uvs"}, {"video/vnd.dece.video","uvv"}, {"video/vnd.dvb.file","dvb"}, {"video/vnd.fvt","fvt"}, {"video/vnd.mpegurl","m4u"}, {"video/vnd.ms-playready.media.pyv","pyv"}, {"video/vnd.uvvu.mp4","uvu"}, {"video/vnd.vivo","viv"}, {"video/webm","webm"}, {"video/x-f4v","f4v"}, {"video/x-fli","fli"}, {"video/x-flv","flv"}, {"video/x-m4v","m4v"}, {"video/x-matroska","mkv"}, {"video/x-mng","mng"}, {"video/x-ms-asf","asf"}, {"video/x-ms-vob","vob"}, {"video/x-ms-wm","wm"}, {"video/x-ms-wmv","wmv"}, {"video/x-ms-wmx","wmx"}, {"video/x-ms-wvx","wvx"}, {"video/x-msvideo","avi"}, {"video/x-sgi-movie","movie"}, {"video/x-smv","smv"}, {"x-conference/x-cooltalk","ice"} }; signalbackup-tools-20250313-1/msgtypes/000077500000000000000000000000001476450434500176015ustar00rootroot00000000000000signalbackup-tools-20250313-1/msgtypes/msgtypes.h000066400000000000000000000215321476450434500216300ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef MSGTYPES_H_ #define MSGTYPES_H_ // see /src/org/thoughtcrime/securesms/database/MmsSmsColumns.java // app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java // app/src/main/java/org/thoughtcrime/securesms/database/MessageTypes.java struct Types { static uint64_t constexpr BASE_TYPE_MASK = 0x1F; static uint64_t constexpr INCOMING_CALL_TYPE = 1; // LATER: INCOMING_AUDIO_CALL_TYPE static uint64_t constexpr INCOMING_AUDIO_CALL_TYPE = 1; static uint64_t constexpr OUTGOING_CALL_TYPE = 2; // LATER: OUTGOING_AUDIO_CALL_TYPE static uint64_t constexpr OUTGOING_AUDIO_CALL_TYPE = 2; static uint64_t constexpr MISSED_CALL_TYPE = 3; // LATER: MISSED_AUDIO_CALL_TYPE static uint64_t constexpr MISSED_AUDIO_CALL_TYPE = 3; static uint64_t constexpr JOINED_TYPE = 4; static uint64_t constexpr UNSUPPORTED_MESSAGE_TYPE = 5; static uint64_t constexpr INVALID_MESSAGE_TYPE = 6; static uint64_t constexpr PROFILE_CHANGE_TYPE = 7; static uint64_t constexpr MISSED_VIDEO_CALL_TYPE = 8; static uint64_t constexpr GV1_MIGRATION_TYPE = 9; static uint64_t constexpr INCOMING_VIDEO_CALL_TYPE = 10; static uint64_t constexpr OUTGOING_VIDEO_CALL_TYPE = 11; static uint64_t constexpr GROUP_CALL_TYPE = 12; static uint64_t constexpr BAD_DECRYPT_TYPE = 13; static uint64_t constexpr CHANGE_NUMBER_TYPE = 14; static uint64_t constexpr BOOST_REQUEST_TYPE = 15; static uint64_t constexpr THREAD_MERGE_TYPE = 16; static uint64_t constexpr BASE_INBOX_TYPE = 20; static uint64_t constexpr BASE_OUTBOX_TYPE = 21; static uint64_t constexpr BASE_SENDING_TYPE = 22; static uint64_t constexpr BASE_SENT_TYPE = 23; static uint64_t constexpr BASE_SENT_FAILED_TYPE = 24; static uint64_t constexpr BASE_PENDING_SECURE_SMS_FALLBACK = 25; static uint64_t constexpr BASE_PENDING_INSECURE_SMS_FALLBACK = 26; static uint64_t constexpr BASE_DRAFT_TYPE = 27; static uint64_t constexpr OUTGOING_MESSAGE_TYPES[] = {BASE_OUTBOX_TYPE, BASE_SENT_TYPE, BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE, BASE_PENDING_SECURE_SMS_FALLBACK, BASE_PENDING_INSECURE_SMS_FALLBACK, OUTGOING_CALL_TYPE, OUTGOING_VIDEO_CALL_TYPE}; static uint64_t constexpr KEY_EXCHANGE_MASK = 0xFF00; static uint64_t constexpr KEY_EXCHANGE_BIT = 0x8000; static uint64_t constexpr KEY_EXCHANGE_IDENTITY_VERIFIED_BIT = 0x4000; static uint64_t constexpr KEY_EXCHANGE_IDENTITY_DEFAULT_BIT = 0x2000; static uint64_t constexpr KEY_EXCHANGE_CORRUPTED_BIT = 0x1000; static uint64_t constexpr KEY_EXCHANGE_INVALID_VERSION_BIT = 0x800; static uint64_t constexpr KEY_EXCHANGE_BUNDLE_BIT = 0x400; static uint64_t constexpr KEY_EXCHANGE_IDENTITY_UPDATE_BIT = 0x200; static uint64_t constexpr KEY_EXCHANGE_CONTENT_FORMAT = 0x100; static uint64_t constexpr GROUP_UPDATE_BIT = 0x10000; static uint64_t constexpr GROUP_LEAVE_BIT = 0x20000; static uint64_t constexpr GROUP_QUIT_BIT = GROUP_LEAVE_BIT; static uint64_t constexpr EXPIRATION_TIMER_UPDATE_BIT = 0x40000; static uint64_t constexpr GROUP_V2_BIT = 0x80000; static uint64_t constexpr GROUP_V2_LEAVE_BITS = GROUP_V2_BIT | GROUP_LEAVE_BIT | GROUP_UPDATE_BIT; static uint64_t constexpr SECURE_MESSAGE_BIT = 0x800000; static uint64_t constexpr END_SESSION_BIT = 0x400000; static uint64_t constexpr PUSH_MESSAGE_BIT = 0x200000; static uint64_t constexpr ENCRYPTION_REMOTE_NO_SESSION_BIT = 0x08000000; static uint64_t constexpr SPECIAL_TYPES_MASK = 0xF00000000L; static uint64_t constexpr SPECIAL_TYPE_STORY_REACTION = 0x100000000L; static uint64_t constexpr SPECIAL_TYPE_GIFT_BADGE = 0x200000000L; static uint64_t constexpr SPECIAL_TYPE_PAYMENTS_NOTIFICATION = 0x300000000L; static uint64_t constexpr SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST = 0x400000000L; static uint64_t constexpr SPECIAL_TYPE_REPORTED_SPAM = 0x500000000L; static uint64_t constexpr SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED = 0x600000000L; static uint64_t constexpr SPECIAL_TYPE_PAYMENTS_ACTIVATED = 0x800000000L; static uint64_t constexpr SPECIAL_TYPE_PAYMENTS_TOMBSTONE = 0x900000000L; inline static bool isGroupUpdate(uint64_t type) { return (type & GROUP_UPDATE_BIT) != 0; } inline static bool isGroupV2(uint64_t type) { return (type & GROUP_V2_BIT) != 0; } inline static bool isGroupQuit(uint64_t type) { return (type & GROUP_QUIT_BIT) != 0; } inline static bool isOutgoing(uint64_t type) { for (uint64_t const outgoingType : OUTGOING_MESSAGE_TYPES) { if ((type & BASE_TYPE_MASK) == outgoingType) return true; } return false; } inline static bool isInboxType(uint64_t type) { return (type & BASE_TYPE_MASK) == BASE_INBOX_TYPE; } inline static bool isCallType(uint64_t type) { return isIncomingCall(type) || isOutgoingCall(type) || isMissedCall(type) || isIncomingVideoCall(type) || isOutgoingVideoCall(type) || isMissedVideoCall(type) || isGroupCall(type); } inline static bool isGroupCall(uint64_t type) { return type == GROUP_CALL_TYPE; } inline static bool isIncomingCall(uint64_t type) { return type == INCOMING_CALL_TYPE; } inline static bool isOutgoingCall(uint64_t type) { return type == OUTGOING_CALL_TYPE; } inline static bool isMissedCall(uint64_t type) { return type == MISSED_CALL_TYPE; } inline static bool isIncomingVideoCall(uint64_t type) { return type == INCOMING_VIDEO_CALL_TYPE; } inline static bool isOutgoingVideoCall(uint64_t type) { return type == OUTGOING_VIDEO_CALL_TYPE; } inline static bool isMissedVideoCall(uint64_t type) { return type == MISSED_VIDEO_CALL_TYPE; } inline static bool isJoined(uint64_t type) { return (type & BASE_TYPE_MASK) == JOINED_TYPE; } inline static bool isNumberChange(uint64_t type) { return type == CHANGE_NUMBER_TYPE; } inline static bool isDonationRequest(uint64_t type) { return type == BOOST_REQUEST_TYPE; } inline static bool isExpirationTimerUpdate(uint64_t type) { return (type & EXPIRATION_TIMER_UPDATE_BIT) != 0; } inline static bool isIdentityUpdate(uint64_t type) { return (type & KEY_EXCHANGE_MASK) == KEY_EXCHANGE_IDENTITY_UPDATE_BIT; } inline static bool isIdentityVerified(uint64_t type) { return (type & KEY_EXCHANGE_MASK) == KEY_EXCHANGE_IDENTITY_VERIFIED_BIT; } inline static bool isIdentityDefault(uint64_t type) { return (type & KEY_EXCHANGE_MASK) == KEY_EXCHANGE_IDENTITY_DEFAULT_BIT; } inline static bool isSecureType(uint64_t type) { return (type & SECURE_MESSAGE_BIT) != 0; } inline static bool isEndSession(uint64_t type) { return (type & END_SESSION_BIT) != 0; } inline static bool isProfileChange(uint64_t type) { return (type & BASE_TYPE_MASK) == PROFILE_CHANGE_TYPE; } inline static bool isMessageRequestAccepted(uint64_t type) { return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED; } inline static bool isStatusMessage(uint64_t type) { return isCallType(type) || isGroupUpdate(type) || isGroupV2(type) || isGroupQuit(type) || isIdentityUpdate(type) || isIdentityVerified(type) || isIdentityDefault(type) || isExpirationTimerUpdate(type) || isJoined(type) || isProfileChange(type) || isEndSession(type) || type == GV1_MIGRATION_TYPE || isNumberChange(type) || isDonationRequest(type) || isMessageRequestAccepted(type); } }; #endif signalbackup-tools-20250313-1/protobufparser/000077500000000000000000000000001476450434500210035ustar00rootroot00000000000000signalbackup-tools-20250313-1/protobufparser/protobufparser.h000066400000000000000000001523041476450434500242360ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef PROTOBUFPARSER_H_ #define PROTOBUFPARSER_H_ #include #include #include #include #include "../base64/base64.h" #include "../common_be.h" #include "../logger/logger.h" template struct is_vector : public std::false_type {}; template struct is_vector> : public std::true_type {}; template struct is_optional : public std::false_type {}; template struct is_optional> : public std::true_type {}; template < template class Template, typename T > struct is_specialization_of : std::false_type {}; template < template class Template, typename... Args > struct is_specialization_of< Template, Template > : std::true_type {}; struct ZigZag32 {}; struct ZigZag64 {}; struct Fixed32 { uint32_t value; }; struct Fixed64 { uint64_t value; }; struct SFixed32 { int32_t value; }; struct SFixed64 { int64_t value; }; namespace protobuffer { namespace optional { typedef double DOUBLE; typedef float FLOAT; typedef int32_t ENUM; typedef int32_t INT32; typedef int64_t INT64; typedef uint32_t UINT32; typedef uint64_t UINT64; typedef ZigZag32 SINT32; typedef ZigZag64 SINT64; typedef Fixed32 FIXED32; typedef Fixed64 FIXED64; typedef SFixed32 SFIXED32; typedef SFixed64 SFIXED64; typedef bool BOOL; typedef std::string STRING; typedef unsigned char *BYTES; typedef int DUMMY; } namespace repeated { typedef std::vector DOUBLE; typedef std::vector FLOAT; typedef std::vector ENUM; typedef std::vector INT32; typedef std::vector INT64; typedef std::vector UINT32; typedef std::vector UINT64; typedef std::vector SINT32; typedef std::vector SINT64; typedef std::vector FIXED32; typedef std::vector FIXED64; typedef std::vector SFIXED32; typedef std::vector SFIXED64; typedef std::vector BOOL; typedef std::vector STRING; typedef std::vector BYTES; typedef int DUMMY; } } // these might be able to go back into Protobufparser when gcc bug is fixed: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85282 namespace ProtoBufParserReturn { // primary template struct item_return {}; // for optionals template struct item_return { typedef std::optional type; }; template <> struct item_return{ typedef std::optional type; }; template <> struct item_return{ typedef std::optional type; }; template <> struct item_return{ typedef std::optional type; }; template <> struct item_return{ typedef std::optional type; }; template <> struct item_return{ typedef std::optional type; }; template <> struct item_return{ typedef std::optional type; }; template <> struct item_return{ typedef std::optional> type; }; template <> struct item_return{ typedef std::optional> type; }; // for vectors: template struct item_return { typedef T type; }; template <> struct item_return, true>{ typedef std::vector type; }; template <> struct item_return, true>{ typedef std::vector type; }; template <> struct item_return, true>{ typedef std::vector type; }; template <> struct item_return, true>{ typedef std::vector type; }; template <> struct item_return, true>{ typedef std::vector type; }; template <> struct item_return, true>{ typedef std::vector type; }; template <> struct item_return, true>{ typedef std::vector> type; }; template <> struct item_return, true>{ typedef std::vector> type; }; } template class ProtoBufParser { enum WIRETYPE : int { VARINT = 0, FIXED64 = 1, LENGTH_DELIMITED = 2, STARTGROUP = 3, ENDGROUP = 4, FIXED32 = 5 }; unsigned char *d_data; int64_t d_size; public: inline ProtoBufParser(); inline explicit ProtoBufParser(std::string const &base64); explicit ProtoBufParser(std::pair, size_t> const &data); explicit ProtoBufParser(unsigned char const *data, int64_t size); inline ProtoBufParser(ProtoBufParser const &other); inline ProtoBufParser &operator=(ProtoBufParser const &other); inline ProtoBufParser(ProtoBufParser &&other); inline ProtoBufParser &operator=(ProtoBufParser &&other); ~ProtoBufParser(); inline bool operator==(ProtoBufParser const &other) const; inline bool operator!=(ProtoBufParser const &other) const; inline int64_t size() const; inline unsigned char *data() const; inline std::string getDataString() const; inline void setData(std::string const &base64); inline void setData(unsigned char const *data, int64_t size); template inline typename ProtoBufParserReturn::item_return::type getFieldAs(int num) const; template inline typename ProtoBufParserReturn::item_return::type getFieldsAs(int num) const; // T must be std::vector template inline auto getField() const -> typename ProtoBufParserReturn::item_return(std::tuple()))>::type, is_vector(std::tuple()))>::type>{}>::type; template int deleteFields(int num, T const *value = nullptr); template bool deleteFirstField(int num, T const *value = nullptr); // add repeated things template inline bool addField(typename std::remove_reference(std::tuple()))>::type::value_type const &value); template inline typename std::enable_if(std::tuple()))>::type, protobuffer::repeated::BYTES>::value, bool>::type addField(std::pair const &value); // specialization for repeated BYTES // add optional things template inline bool addField(typename std::remove_reference(std::tuple()))>::type const &value); template inline typename std::enable_if(std::tuple()))>::type, protobuffer::optional::BYTES>::value, bool>::type addField(std::pair const &value); // specialization for optional BYTES inline void print(int indent = 0) const; protected: inline void clear(); private: template inline bool addFieldInternal(T const &value); int64_t readVarInt(int *pos, unsigned char const *data, int size, bool zigzag = false) const; int64_t getVarIntFieldLength(int pos, unsigned char const *data, int size) const; std::pair getField(int num, bool *isvarint) const; std::pair getField(int num, bool *isvarint, int *pos) const; void getPosAndLengthForField(int num, int startpos, int *pos, int *fieldlength) const; bool fieldExists(int num) const; template inline constexpr uint64_t fieldSize() const; inline uint64_t varIntSize(uint64_t value) const; template static inline constexpr unsigned int getType(); // printing this horrorshow... template> inline void printHelper1(int indent) const; template inline void printHelper2(std::index_sequence, int indent) const; template struct printHelperWrapper { void printHelper3(ProtoBufParser const *ptr, int indent) const { ptr->printHelper4(indent); } }; template inline void printHelper4(int indent) const; template inline void printSingle(int indent, std::string const &typestring) const; template inline void printRepeated(int indent, std::string const &typestring) const; }; template inline ProtoBufParser::ProtoBufParser() : d_data(nullptr), d_size(0) {} template inline ProtoBufParser::ProtoBufParser(ProtoBufParser const &other) : d_data(nullptr), d_size(other.d_size) { d_data = new unsigned char[d_size]; if (d_size) std::memcpy(d_data, other.d_data, d_size); } template inline ProtoBufParser &ProtoBufParser::operator=(ProtoBufParser const &other) { if (this != &other) { bepaald::destroyPtr(&d_data, &d_size); d_size = other.d_size; d_data = new unsigned char[d_size]; if (d_size) std::memcpy(d_data, other.d_data, d_size); } return *this; } template inline ProtoBufParser::ProtoBufParser(ProtoBufParser &&other) : d_data(other.d_data), d_size(other.d_size) { other.d_data = nullptr; other.d_size = 0; } template inline ProtoBufParser &ProtoBufParser::operator=(ProtoBufParser &&other) { if (this != &other) { bepaald::destroyPtr(&d_data, &d_size); d_data = other.d_data; d_size = other.d_size; other.d_data = nullptr; other.d_size = 0; } return *this; } template ProtoBufParser::ProtoBufParser(std::string const &base64) : d_data(nullptr), d_size(0) { std::pair l_data = Base64::base64StringToBytes(base64); d_data = l_data.first; d_size = l_data.second; //std::cout << "INPUT: " << bepaald::bytesToHexString(d_data, d_size) << std::endl; } template ProtoBufParser::ProtoBufParser(std::pair, size_t> const &data) : ProtoBufParser(data.first.get(), data.second) {} template ProtoBufParser::ProtoBufParser(unsigned char const *data, int64_t size) : d_data(nullptr), d_size(size) { d_data = new unsigned char[d_size]; if (d_size) std::memcpy(d_data, data, d_size); } template ProtoBufParser::~ProtoBufParser() { bepaald::destroyPtr(&d_data, &d_size); } template inline void ProtoBufParser::clear() { bepaald::destroyPtr(&d_data, &d_size); } template inline bool ProtoBufParser::operator==(ProtoBufParser const &other) const { return d_size == other.d_size && std::memcmp(d_data, other.d_data, d_size) == 0; } template inline bool ProtoBufParser::operator!=(ProtoBufParser const &other) const { return d_size != other.d_size || std::memcmp(d_data, other.d_data, d_size) != 0; } template inline void ProtoBufParser::setData(std::string const &base64) { // destroy old if (d_data) delete[] d_data; std::pair l_data = Base64::base64StringToBytes(base64); d_data = l_data.first; d_size = l_data.second; } template inline void ProtoBufParser::setData(unsigned char const *data, int64_t size) { // destroy old if (d_data) delete[] d_data; d_data = new unsigned char[size]; if (data) std::memcpy(d_data, data, size); d_size = size; } template inline int64_t ProtoBufParser::size() const { return d_size; } template inline unsigned char *ProtoBufParser::data() const { return d_data; } template inline std::string ProtoBufParser::getDataString() const { if (d_size) return Base64::bytesToBase64String(d_data, d_size); return std::string(); } // for optional? template template inline typename ProtoBufParserReturn::item_return::type ProtoBufParser::getFieldAs(int num) const { bool varint = false; std::pair fielddata(getField(num, &varint)); if (fielddata.first) { if constexpr (std::is_constructible::value) // this handles std::string and ProtoBufParser ? return T(reinterpret_cast(fielddata.first), fielddata.second); else if constexpr (std::is_constructible::value) return T(fielddata.first, fielddata.second); else if constexpr (std::is_same::value) // binary blob return std::pair{reinterpret_cast(fielddata.first), fielddata.second}; else if constexpr (std::is_same::value) return fielddata; else // some numerical type (double / float / (u)int32/64 / bool / Enum) { if (varint) [[likely]] // wiretype was varint -> raw data needs to be decoded into the actual number { if constexpr (std::is_same::value || std::is_same::value) { int pos = 0; return readVarInt(&pos, fielddata.first, fielddata.second, true); } else { int pos = 0; return readVarInt(&pos, fielddata.first, fielddata.second); } } else { if constexpr (std::is_same::value || std::is_same::value || std::is_same::value || std::is_same::value) { if (sizeof(T) == fielddata.second) [[likely]] { typename ProtoBufParserReturn::item_return::type::value_type result; // ie.: uint32_t result; (stripped off std::optional std::memcpy(reinterpret_cast(&result), reinterpret_cast(fielddata.first), fielddata.second); return result; } else { Logger::error("ProtoBufParser: REQUESTED TYPE TOO SMALL (1)"); } } else if constexpr (!std::is_same::value && !std::is_same::value) { if (sizeof(T) == fielddata.second) [[likely]] { T result; std::memcpy(reinterpret_cast(&result), reinterpret_cast(fielddata.first), fielddata.second); return result; } else { Logger::error("ProtoBufParser: REQUESTED TYPE TOO SMALL (2): ", fielddata.second, " ", sizeof(T)); } } } } } return {}; } // for repeated template template inline typename ProtoBufParserReturn::item_return::type ProtoBufParser::getFieldsAs(int num) const { typename ProtoBufParserReturn::item_return::type result; // == for example, for repeated::BYTES -> std::vector> int pos = 0; while (true) { bool varint = false; std::pair fielddata(getField(num, &varint, &pos)); if (fielddata.first) { if constexpr (std::is_constructible::type::value_type, char *, int64_t>::value) result.emplace_back(typename ProtoBufParserReturn::item_return::type::value_type(reinterpret_cast(fielddata.first), fielddata.second)); else if constexpr (std::is_constructible::type::value_type, unsigned char *, int64_t>::value) result.emplace_back(typename ProtoBufParserReturn::item_return::type::value_type(fielddata.first, fielddata.second)); else if constexpr (std::is_same::type::value_type, char *>::value) result.emplace_back(std::pair{reinterpret_cast(fielddata.first), fielddata.second}); else if constexpr (std::is_same::type::value_type, unsigned char *>::value) result.emplace_back(fielddata); else // maybe check return type is numerical? if constexpr (typename ProtoBufParserReturn::item_return::type::value_type == numerical type); { if (varint) [[likely]] // wiretype was varint -> raw data needs to be decoded into the actual number { if constexpr (std::is_same::value || std::is_same::value) { int pos2 = 0; result.push_back(readVarInt(&pos2, fielddata.first, fielddata.second, true)); } else { int pos2 = 0; result.push_back(readVarInt(&pos2, fielddata.first, fielddata.second)); } } else { if (sizeof(typename ProtoBufParserReturn::item_return::type::value_type) == fielddata.second) [[likely]] { typename ProtoBufParserReturn::item_return::type::value_type fixednumerical; // could be int32, int64, float or double std::memcpy(reinterpret_cast(&fixednumerical), reinterpret_cast(fielddata.first), fielddata.second); result.push_back(fixednumerical); } else { Logger::error("ProtoBufParser: REQUESTED TYPE TOO SMALL (3)"); } } } pos += fielddata.second; } else break; } return result; } template template inline auto ProtoBufParser::getField() const -> typename ProtoBufParserReturn::item_return(std::tuple()))>::type, is_vector(std::tuple()))>::type>{}>::type { if constexpr (!is_vector(std::tuple()))>::type>{}) return getFieldAs(std::tuple()))>::type>(idx); else return getFieldsAs(std::tuple()))>::type>(idx); } template template int ProtoBufParser::deleteFields(int num, T const *value) { int deleted = 0; while (deleteFirstField(num, value)) ++deleted; return deleted; } template template bool ProtoBufParser::deleteFirstField(int num, T const *value [[maybe_unused]]) { int startpos = 0; int pos = -1; int fieldlength = -1; while (startpos < d_size) { getPosAndLengthForField(num, startpos, &pos, &fieldlength); if (pos == -1 || fieldlength == -1) return false; //std::cout << "DATA: " << bepaald::bytesToHexString(d_data, d_size) << std::endl; //std::cout << "Got requested field at pos " << pos << " (length " << fieldlength << ")" << std::endl; //std::cout << "FIELD: " << bepaald::bytesToHexString(d_data + pos, fieldlength) << std::endl; if constexpr (!std::is_same::value) { //std::cout << "Asked to delete specific: " << *value << std::endl; bool del = false; int tmppos = pos; bool isvarint = false; if constexpr (std::is_constructible::value) // meant for probably for std::strings { std::pair l_data = getField(num, &isvarint, &tmppos); T tmp(reinterpret_cast(l_data.first), l_data.second); //std::cout << "Created tmp1: " << tmp << std::endl; if (tmp == *value) del = true; } else if constexpr (std::is_same>::value) { std::pair l_data = getField(num, &isvarint, &tmppos); if (value->second == l_data.second && std::memcmp(reinterpret_cast(value->first), l_data.first, l_data.second) == 0) del = true; } else if constexpr (std::is_same>::value) { std::pair l_data = getField(num, &isvarint, &tmppos); if (value->second == l_data.second && std::memcmp(value->first, l_data.first, l_data.second) == 0) del = true; } else if constexpr (is_specialization_of::value) { //std::cout << "YO666" << std::endl; std::pair l_data = getField(num, &isvarint, &tmppos); T tmp(l_data.first, l_data.second); if (tmp == *value) del = true; } else if constexpr (std::is_integral::value) { std::pair l_data = getField(num, &isvarint, &tmppos); if (isvarint) { int lpos = 0; T vint = readVarInt(&lpos, l_data.first, l_data.second, false); // zigzag not (yet) supported if (vint == *value) del = true; } else // fixed numerical (int32 (enum), int64, float or double) { T tmp = 0; std::memcpy(reinterpret_cast(&tmp), reinterpret_cast(l_data.first), l_data.second); if (tmp == *value) del = true; } } if (del) { //std::cout << "GOT HIT!" << std::endl; } else { //std::cout << "First find is no hit, looping!" << std::endl; startpos = pos + fieldlength; pos = -1; fieldlength = -1; continue; } } // std::cout << "Got field " << num << " at pos " << pos << " (length " << fieldlength << ")" << std::endl; unsigned char *newdata = new unsigned char[d_size - fieldlength]; std::memcpy(newdata, d_data, pos); std::memcpy(newdata + pos, d_data + pos + fieldlength, d_size - (pos + fieldlength)); delete[] d_data; d_data = newdata; d_size = d_size - fieldlength; //std::cout << "After delete" << std::endl; //std::cout << "DATA: " << bepaald::bytesToHexString(d_data, d_size) << std::endl; return true; } return false; } // field is different from varint because first byte only has 4 bits left... template template inline constexpr uint64_t ProtoBufParser::fieldSize() const { if constexpr (idx <= 0xf) return 1; if constexpr (idx <= 0x7ff) return 2; if constexpr (idx <= 0x3ffff) return 3; if constexpr (idx <= 0x1ffffff) return 4; if constexpr (idx <= 0xffffffff) return 5; if constexpr (idx <= 0x7fffffffff) return 6; if constexpr (idx <= 0x3fffffffffff) return 7; if constexpr (idx <= 0x1fffffffffffff) return 8; if constexpr (idx <= 0xfffffffffffffff) return 9; return 10; } template inline uint64_t ProtoBufParser::varIntSize(uint64_t value) const { if (value <= 0x7f) return 1; if (value <= 0x3fff) return 2; if (value <= 0x1fffff) return 3; if (value <= 0xfffffff) return 4; if (value <= 0x7ffffffff) return 5; if (value <= 0x3ffffffffff) return 6; if (value <= 0x1ffffffffffff) return 7; if (value <= 0xffffffffffffff) return 8; if (value <= 0x7fffffffffffffff) return 9; return 10; } template template inline constexpr unsigned int ProtoBufParser::getType() //static { if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::STRING>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::STRING>::value || std::is_same(std::tuple()))>::type, protobuffer::optional::BYTES>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::BYTES>::value) return WIRETYPE::LENGTH_DELIMITED; else if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::ENUM>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::ENUM>::value || std::is_same(std::tuple()))>::type, protobuffer::optional::INT32>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::INT32>::value || std::is_same(std::tuple()))>::type, protobuffer::optional::INT64>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::INT64>::value || std::is_same(std::tuple()))>::type, protobuffer::optional::UINT32>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::UINT32>::value || std::is_same(std::tuple()))>::type, protobuffer::optional::UINT64>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::UINT64>::value || std::is_same(std::tuple()))>::type, protobuffer::optional::SINT32>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::SINT32>::value || std::is_same(std::tuple()))>::type, protobuffer::optional::SINT64>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::SINT64>::value) return WIRETYPE::VARINT; else if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::FLOAT>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::FLOAT>::value || std::is_same(std::tuple()))>::type, protobuffer::optional::FIXED32>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::FIXED32>::value || std::is_same(std::tuple()))>::type, protobuffer::optional::SFIXED32>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::SFIXED32>::value || std::is_same(std::tuple()))>::type, protobuffer::optional::BOOL>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::BOOL>::value) return WIRETYPE::FIXED32; else if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::DOUBLE>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::DOUBLE>::value || std::is_same(std::tuple()))>::type, protobuffer::optional::FIXED64>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::FIXED64>::value || std::is_same(std::tuple()))>::type, protobuffer::optional::SFIXED64>::value || std::is_same(std::tuple()))>::type, protobuffer::repeated::SFIXED64>::value) return WIRETYPE::FIXED64; else if constexpr (is_specialization_of(std::tuple()))>::type>{}) return WIRETYPE::LENGTH_DELIMITED; else if constexpr (is_vector(std::tuple()))>::type>{}) if constexpr (is_specialization_of(std::tuple()))>::type::value_type>{}) return WIRETYPE::LENGTH_DELIMITED; } template template inline bool ProtoBufParser::addFieldInternal(T const &value) { unsigned int field = idx; unsigned int constexpr type = getType(); unsigned int fielddatasize = 0; if constexpr (type == WIRETYPE::LENGTH_DELIMITED) { if constexpr (is_specialization_of{}) // bytes fielddatasize = value.second; else // string fielddatasize = value.size(); } else if constexpr (type == WIRETYPE::VARINT) fielddatasize = varIntSize(value); else if constexpr (type == WIRETYPE::FIXED32) fielddatasize = 4; else if constexpr (type == WIRETYPE::FIXED64) fielddatasize = 8; int size = fieldSize() + (type == WIRETYPE::LENGTH_DELIMITED ? varIntSize(fielddatasize) : 0) + fielddatasize; unsigned char *mem = new unsigned char[size]{}; // set first byte of field (+ wire) unsigned int mempos = 0; mem[mempos] = (field << 3) & 0b00000000'00000000'00000000'01111000; mem[mempos] |= (type); field >>= 4; // shift out the used part of idx while (field) { // previous byte add 0b1000000; mem[mempos++] |= 0b10000000; mem[mempos] = field & 0b00000000'00000000'00000000'01111111; field >>= 7; // shift out the used part of idx } // put length (as varint) if type is length_delim, or put actual value if type is varint ++mempos; if constexpr (type == WIRETYPE::LENGTH_DELIMITED || type == WIRETYPE::VARINT) { uint64_t varint = 0; if constexpr (type == WIRETYPE::LENGTH_DELIMITED) varint = fielddatasize; else if constexpr (type == WIRETYPE::VARINT) varint = value; while (varint > 127) { mem[mempos] = (static_cast(varint & 127)) | 128; varint >>= 7; ++mempos; } mem[mempos++] = (static_cast(varint)) & 127; } // put actual data if constexpr (type == WIRETYPE::LENGTH_DELIMITED) { if constexpr (is_specialization_of{}) // bytes std::memcpy(mem + mempos, value.first, fielddatasize); else // string std::memcpy(mem + mempos, value.data(), fielddatasize); } else if constexpr (type == WIRETYPE::FIXED32 || type == WIRETYPE::FIXED64) std::memcpy(mem + mempos, reinterpret_cast(&value), sizeof(value)); //std::cout << "Adding: " << bepaald::bytesToHexString(mem, size) << std::endl; // build the new protobuf data, by copying old plus new unsigned char *newdata = new unsigned char[d_size + size]; if (d_data) // when creating a new ProtoBuf from nothing, d_data starts empty, which is UB for memcpy std::memcpy(newdata, d_data, d_size); std::memcpy(newdata + d_size, mem, size); delete[] mem; if (d_data) delete[] d_data; d_data = newdata; d_size = d_size + size; //std::cout << "OUTPUT: " << bepaald::bytesToHexString(d_data, d_size) << std::endl; return true; } // add repeated things template template inline bool ProtoBufParser::addField(typename std::remove_reference(std::tuple()))>::type::value_type const &value) { //std::cout << "Repeated -> go!" << std::endl; return addFieldInternal(value); } // specialization for repeated BYTES template template inline typename std::enable_if(std::tuple()))>::type, protobuffer::repeated::BYTES>::value, bool>::type ProtoBufParser::addField(std::pair const &value) { //std::cout << "Repeated -> go!" << std::endl; return addFieldInternal(value); } // add optional things template template inline bool ProtoBufParser::addField(typename std::remove_reference(std::tuple()))>::type const &value) { //std::cout << "Optional -> go!" << std::endl; if (fieldExists(idx)) { //std::cout << "FIELD NOT REPEATED AND ALREADY SET! NOT ADDING!" << std::endl; return false; } return addFieldInternal(value); } // specialization for optional BYTES template template inline typename std::enable_if(std::tuple()))>::type, protobuffer::optional::BYTES>::value, bool>::type ProtoBufParser::addField(std::pair const &value) { //std::cout << "Optional -> go!" << std::endl; if (fieldExists(idx)) { //std::cout << "FIELD NOT REPEATED AND ALREADY SET! NOT ADDING!" << std::endl; return false; } return addFieldInternal(value); } template int64_t ProtoBufParser::readVarInt(int *pos, unsigned char const *data, int size, bool zigzag) const { uint64_t value = 0; uint64_t times = 0; while (*pos < size && (data[*pos]) & 0b10000000) value |= ((static_cast(data[(*pos)++]) & 0b01111111) << (times++ * 7)); value |= ((static_cast(data[(*pos)++]) & 0b01111111) << (times * 7)); if (zigzag) value = -(value & 1) ^ (value >> 1); return value; } template int64_t ProtoBufParser::getVarIntFieldLength(int pos, unsigned char const *data, int size) const { uint64_t length = 0; while (pos < size && (data[pos]) & 0b10000000) { ++length; ++pos; } return ++length; } template void ProtoBufParser::getPosAndLengthForField(int num, int startpos, int *pos, int *fieldlength) const { int localpos = startpos; while (localpos < d_size) { int32_t field = (d_data[localpos] & 0b00000000000000000000000001111000) >> 3; int32_t wiretype = d_data[localpos] & 0b00000000000000000000000000000111; int fieldshift = 4; unsigned int localpos2 = localpos; while (localpos2 < d_size - 1 && d_data[localpos2] & 0b00000000000000000000000010000000) // skipping the shift { field |= (d_data[++localpos2] & 0b00000000000000000000000001111111) << fieldshift; fieldshift += 7; } int nextpos = localpos2 + 1; switch (wiretype) { case WIRETYPE::LENGTH_DELIMITED: { uint64_t localfieldlength = readVarInt(&nextpos, d_data, d_size); if (field == num) { *pos = localpos; *fieldlength = localfieldlength + nextpos - localpos; return; } localpos = nextpos + localfieldlength; break; } case WIRETYPE::VARINT: { uint64_t localfieldlength = getVarIntFieldLength(nextpos, d_data, d_size); if (field == num) { *pos = localpos; *fieldlength = localfieldlength + nextpos - localpos; return; } localpos = nextpos + localfieldlength; break; } case WIRETYPE::FIXED64: { uint64_t localfieldlength = 8; if (field == num) { *pos = localpos; *fieldlength = localfieldlength + 1; return; } localpos = nextpos + localfieldlength; break; } case WIRETYPE::FIXED32: { uint64_t localfieldlength = 4; if (field == num) { *pos = localpos; *fieldlength = localfieldlength + 1; return; } localpos = nextpos + localfieldlength; break; } case WIRETYPE::STARTGROUP: { if (field == num) Logger::warning("Skipping startgroup for now"); break; } case WIRETYPE::ENDGROUP: { if (field == num) Logger::warning("Skipping endgroup for now"); break; } default: { Logger::error("Unknown wiretype: ", wiretype); return; } } } } template std::pair ProtoBufParser::getField(int num, bool *isvarint) const { int pos = 0; return getField(num, isvarint, &pos); } template std::pair ProtoBufParser::getField(int num, bool *isvarint, int *pos) const { while (*pos < d_size) { //std::cout << "AT: " << *pos << " : " << "0x" << std::hex << static_cast(d_data[*pos] & 0xff) << std::dec << std::endl; int32_t field = (d_data[*pos] & 0b00000000000000000000000001111000) >> 3; int32_t wiretype = d_data[*pos] & 0b00000000000000000000000000000111; int fieldshift = 4; while (d_data[*pos] & 0b00000000000000000000000010000000 && // skipping the shift *pos < d_size - 1) { //std::cout << "Adding byte to varint field number" << std::endl; field |= (d_data[++(*pos)] & 0b00000000000000000000000001111111) << fieldshift; fieldshift += 7; } // std::cout << "field: " << field << std::endl; // std::cout << "wiret: " << wiretype << std::endl; ++(*pos); switch (wiretype) { case WIRETYPE::LENGTH_DELIMITED: { uint64_t fieldlength = readVarInt(pos, d_data, d_size); if (field == num) return std::make_pair(d_data + *pos, fieldlength); *pos += fieldlength; break; } case WIRETYPE::VARINT: { *isvarint = true; //std::cout << "AT: " << *pos << " : " << "0x" << std::hex << static_cast(d_data[*pos] & 0xff) << std::dec << std::endl; uint64_t fieldlength = getVarIntFieldLength(*pos, d_data, d_size); if (field == num) return std::make_pair(d_data + *pos, fieldlength); *pos += fieldlength; break; } case WIRETYPE::FIXED64: { if (field == num) return std::make_pair(d_data + *pos, 8); *pos += 8; break; } case WIRETYPE::FIXED32: { if (field == num) return std::make_pair(d_data + *pos, 4); *pos += 4; break; } case WIRETYPE::STARTGROUP: { if (field == num) Logger::warning("Skipping startgroup for now"); break; } case WIRETYPE::ENDGROUP: { if (field == num) Logger::warning("Skipping endgroup for now"); break; } } } return std::pair(nullptr, 0); } template bool ProtoBufParser::fieldExists(int num) const { int pos = 0; while (pos < d_size) { int32_t field = (d_data[pos] & 0b00000000000000000000000001111000) >> 3; int32_t wiretype = d_data[pos] & 0b00000000000000000000000000000111; int fieldshift = 4; while (pos < d_size - 1 && d_data[pos] & 0b00000000000000000000000010000000) // skipping the shift { field |= (d_data[++pos] & 0b00000000000000000000000001111111) << fieldshift; fieldshift += 7; } if (field == num) return true; ++pos; switch (wiretype) { case WIRETYPE::LENGTH_DELIMITED: { uint64_t fieldlength = readVarInt(&pos, d_data, d_size); pos += fieldlength; break; } case WIRETYPE::VARINT: { uint64_t fieldlength = getVarIntFieldLength(pos, d_data, d_size); pos += fieldlength; break; } case WIRETYPE::FIXED64: { pos += 8; break; } case WIRETYPE::FIXED32: { pos += 4; break; } case WIRETYPE::STARTGROUP: // deprecated/not implemented yet { break; } case WIRETYPE::ENDGROUP: // deprecated/not implemented yet { break; } } } return false; } template template inline void ProtoBufParser::printSingle(int indent, std::string const &typestring) const { auto tmp = getField(); if (tmp.has_value()) Logger::message(std::string(indent, ' '), "Field ", idx + 1, " ", typestring, ": ", tmp.value()); } template template inline void ProtoBufParser::printRepeated(int indent, std::string const &typestring) const { std::vector tmp = getField(); for (unsigned int i = 0; i < tmp.size(); ++i) Logger::message(std::string(indent, ' '), "Field ", idx + 1, " ", typestring, " (", i + 1 , "/", tmp.size(), "): ", tmp[i]); return; } template template inline void ProtoBufParser::printHelper4(int indent) const { // std::cout << "Dealing with field " << idx + 1 << std::endl; if (!fieldExists(idx + 1)) { // std::cout << "Not present" << std::endl; return; } //std::cout << std::string(indent, ' ') << "Dealing with field " << idx + 1 << std::endl; // SINGLE if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::STRING>::value) return printSingle(indent, "(optional::string)"); // { // auto tmp = getField(); // if (tmp.has_value()) // std::cout << std::string(indent, ' ') << "Field " << idx + 1 << " (optional::STRING): " << tmp.value() << std::endl; // return; // } if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::ENUM>::value) return printSingle(indent, "(optional::enum)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::INT32>::value) return printSingle(indent, "(optional::int32)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::INT64>::value) return printSingle(indent, "(optional::int32)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::UINT32>::value) return printSingle(indent, "(optional::uint32)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::UINT64>::value) return printSingle(indent, "(optional::uint64)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::SINT32>::value) return printSingle(indent, "(optional::sint32)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::SINT64>::value) return printSingle(indent, "(optional::sint64)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::FLOAT>::value) return printSingle(indent, "(optional::float)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::FIXED32>::value) return printSingle(indent, "(optional::fixed32)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::SFIXED32>::value) return printSingle(indent, "(optional::sfixed32)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::DOUBLE>::value) return printSingle(indent, "(optional::double)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::FIXED64>::value) return printSingle(indent, "(optional::fixed64)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::SFIXED64>::value) return printSingle(indent, "(optional::sfixed64)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::BOOL>::value) { auto tmp = getField(); if (tmp.has_value()) Logger::message(std::string(indent, ' '), "Field ", idx + 1, " (optional::bool): ", std::boolalpha, tmp.value()); return; } // bytes if constexpr (std::is_same(std::tuple()))>::type, protobuffer::optional::BYTES>::value) { std::optional> tmp = getField(); if (tmp.has_value()) Logger::message(std::string(indent, ' '), "Field ", idx + 1, " (optional::bytes[", tmp.value().second, "]): ", bepaald::bytesToHexString(tmp.value().first, tmp.value().second)); return; } // single protobuffer if constexpr (is_specialization_of(std::tuple()))>::type>{}) { if (getField().has_value()) { Logger::message(std::string(indent, ' '), "Field ", idx + 1, " (optional::protobuf): "); return getField().value().print(indent + 2); } } // REPEATED if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::STRING>::value) return printRepeated(indent, "(repeated::string)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::ENUM>::value) return printRepeated(indent, "(repeated::enum)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::INT32>::value) return printRepeated(indent, "(repeated::int32)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::INT64>::value) return printRepeated(indent, "(repeated::int64)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::UINT32>::value) return printRepeated(indent, "(repeated::uint32)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::UINT64>::value) return printRepeated(indent, "(repeated::uint64)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::SINT32>::value) return printRepeated(indent, "(repeated::sint32)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::SINT64>::value) return printRepeated(indent, "(repeated::sint64)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::FLOAT>::value) return printRepeated(indent, "(repeated::float)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::FIXED32>::value) return printRepeated(indent, "(repeated::fixed32)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::SFIXED32>::value) return printRepeated(indent, "(repeated::sfixed32)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::DOUBLE>::value) return printRepeated(indent, "(repeated::double)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::FIXED64>::value) return printRepeated(indent, "(repeated::fixed64)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::SFIXED64>::value) return printRepeated(indent, "(repeated::sfixed64)"); if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::BOOL>::value) { std::vector tmp = getField(); for (unsigned int i = 0; i < tmp.size(); ++i) Logger::message(std::string(indent, ' '), "Field ", idx + 1, " (repeated::bool) (", i + 1 , "/", tmp.size(), "): ", std::boolalpha, tmp[i]); return; } // bytes if constexpr (std::is_same(std::tuple()))>::type, protobuffer::repeated::BYTES>::value) { std::vector> tmp = getField(); for (unsigned int i = 0; i < tmp.size(); ++i) { std::pair tmp2 = tmp[i]; Logger::message(std::string(indent, ' '), "Field ", idx + 1, " (repeated::bytes) (", i + 1 , "/", tmp.size(), "): ", bepaald::bytesToHexString(tmp2.first, tmp2.second)); return; } return; } // protobuf message if constexpr (is_vector(std::tuple()))>::type>{}) if constexpr (is_specialization_of(std::tuple()))>::type::value_type>{}) { auto tmp = getField(); for (unsigned int i = 0; i < tmp.size(); ++i) { Logger::message(std::string(indent, ' '), "Field ", idx + 1, " (repeated::protobuf) (", i + 1 , "/", tmp.size(), "): "); tmp.at(i).print(indent + 2); } return; } Logger::message(std::string(indent, ' '), "Field ", idx + 1, ": (unhandled protobuf type)"); } template template inline void ProtoBufParser::printHelper2(std::index_sequence, int indent [[maybe_unused]]) const { (printHelperWrapper().printHelper3(this, indent), ...); } template template inline void ProtoBufParser::printHelper1(int indent) const { printHelper2(Indices(), indent); } template inline void ProtoBufParser::print(int indent) const { printHelper1(indent); } #endif signalbackup-tools-20250313-1/rawfileattachmentreader/000077500000000000000000000000001476450434500226135ustar00rootroot00000000000000signalbackup-tools-20250313-1/rawfileattachmentreader/rawfileattachmentreader.h000066400000000000000000000057101476450434500276540ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef RAWATTACHMENTREADER_H_ #define RAWATTACHMENTREADER_H_ #include "../baseattachmentreader/baseattachmentreader.h" #include "../framewithattachment/framewithattachment.h" #include "../common_filesystem.h" class RawFileAttachmentReader : public AttachmentReader { std::string d_filename; public: inline explicit RawFileAttachmentReader(std::string const &filename); RawFileAttachmentReader(RawFileAttachmentReader const &other) = default; RawFileAttachmentReader(RawFileAttachmentReader &&other) = default; RawFileAttachmentReader &operator=(RawFileAttachmentReader const &other) = default; RawFileAttachmentReader &operator=(RawFileAttachmentReader &&other) = default; virtual ~RawFileAttachmentReader() override = default; inline virtual int getAttachment(FrameWithAttachment *frame, bool verbose) override; }; inline RawFileAttachmentReader::RawFileAttachmentReader(std::string const &filename) : d_filename(filename) {} int RawFileAttachmentReader::getAttachment(FrameWithAttachment *frame, bool verbose) // virtual { //std::cout << " *** REALLY GETTING ATTACHMENT (RAW) ***" << std::endl; std::ifstream file(std::filesystem::path(d_filename), std::ios_base::binary | std::ios_base::in); if (!file.is_open()) { Logger::error("Failed to open file '", d_filename, "' for reading attachment"); return 1; } //file.seekg(0, std::ios_base::end); //int64_t attachmentdata_size = file.tellg(); //file.seekg(0, std::ios_base::beg); uint64_t attachmentdata_size = bepaald::fileSize(d_filename); if (attachmentdata_size == 0) [[unlikely]] Logger::warning("Asked to read 0-byte attachment"); if (verbose) [[unlikely]] Logger::message("Reading attachment data, length: ", attachmentdata_size); //std::cout << "Getting attachment: " << d_filename << std::endl; std::unique_ptr decryptedattachmentdata(new unsigned char[attachmentdata_size]); // to hold the data if (!file.read(reinterpret_cast(decryptedattachmentdata.get()), attachmentdata_size)) { Logger::error("Failed to read raw attachment \"", d_filename, "\""); return 1; } frame->setAttachmentDataBacked(decryptedattachmentdata.release(), attachmentdata_size); return 0; } #endif signalbackup-tools-20250313-1/reactionlist/000077500000000000000000000000001476450434500204265ustar00rootroot00000000000000signalbackup-tools-20250313-1/reactionlist/reactionlist.h000066400000000000000000000050211476450434500232750ustar00rootroot00000000000000/* Copyright (C) 2020-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef REACTIONLIST_H_ #define REACTIONLIST_H_ #include "../protobufparser/protobufparser.h" /* message ReactionList { message Reaction { string emoji = 1; uint64 author = 2; uint64 sentTime = 3; uint64 receivedTime = 4; } repeated Reaction reactions = 1; } */ class ReactionList : public ProtoBufParser>> { public: inline explicit ReactionList(std::pair, size_t> const &data); inline unsigned int numReactions() const; inline std::string getEmoji(int idx) const; inline uint64_t getAuthor(int idx) const; inline uint64_t getSentTime(int idx) const; inline uint64_t getReceivedTime(int idx) const; bool setAuthor(unsigned int idx, uint64_t author); }; inline ReactionList::ReactionList(std::pair, size_t> const &data) : ProtoBufParser(data) {} inline unsigned int ReactionList::numReactions() const { return getField<1>().size(); } inline std::string ReactionList::getEmoji(int idx) const { return getField<1>()[idx].getField<1>().value_or(""); } inline uint64_t ReactionList::getAuthor(int idx) const { return getField<1>()[idx].getField<2>().value_or(0); } inline uint64_t ReactionList::getSentTime(int idx) const { return getField<1>()[idx].getField<3>().value_or(0); } inline uint64_t ReactionList::getReceivedTime(int idx) const { return getField<1>()[idx].getField<4>().value_or(0); } #endif signalbackup-tools-20250313-1/reactionlist/reactionlist.ih000066400000000000000000000014041476450434500234470ustar00rootroot00000000000000/* Copyright (C) 2020-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "reactionlist.h" signalbackup-tools-20250313-1/reactionlist/setauthor.cc000066400000000000000000000057431476450434500227640ustar00rootroot00000000000000/* Copyright (C) 2020-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "reactionlist.ih" bool ReactionList::setAuthor(unsigned int idx, uint64_t author) { //std::cout << "BEFORE EDIT : " << getDataString() << std::endl; if (idx >= numReactions()) return false; std::vector> reactions = getField<1>(); std::vector> newreactions; for (unsigned int i = 0; i < reactions.size(); ++i) { if (i != idx) newreactions.push_back(reactions[i]); // just copy all unchanged else { ProtoBufParser tmp; if (!tmp.addField<1>(reactions[i].getField<1>().value()) || !tmp.addField<2>(author) || !tmp.addField<3>(reactions[i].getField<3>().value()) || !tmp.addField<4>(reactions[i].getField<4>().value())) return false; newreactions.push_back(tmp); } } // clear entire list clear(); // set new data for (unsigned int i = 0; i < newreactions.size(); ++i) if (!addField<1>(newreactions[i])) return false; //std::cout << "AFTER EDIT 1 : " << getDataString() << std::endl; return true; // // get the reaction we're editing // ProtoBufParser entry = getField<1>()[idx]; // // delete that reaction from list // if (deleteFields(1, &entry) != 1) // return false; // // delete the current author // if (entry.deleteFields(2) != 1) // return false; // // set new field // if (!(entry.addField<2>(protobuffer::optional::UINT64{author}))) // return false; // //std::cout << "Adding updated reaction" << std::endl; // return addField<1>(entry); } signalbackup-tools-20250313-1/scopeguard/000077500000000000000000000000001476450434500200625ustar00rootroot00000000000000signalbackup-tools-20250313-1/scopeguard/scopeguard.h000066400000000000000000000020031476450434500223620ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef SCOPEGUARD_H_ #define SCOPEGUARD_H_ // a _very_ simple (limited) scope guard template class ScopeGuard { F d_function; public: explicit ScopeGuard(F &&fn) : d_function(std::forward(fn)) {}; ~ScopeGuard() { d_function(); }; }; #endif signalbackup-tools-20250313-1/sharedprefframe/000077500000000000000000000000001476450434500210645ustar00rootroot00000000000000signalbackup-tools-20250313-1/sharedprefframe/sharedprefframe.h000066400000000000000000000234431476450434500244010ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef SHAREDPREFFRAME_H_ #define SHAREDPREFFRAME_H_ #include #include "../common_bytes.h" #include "../backupframe/backupframe.h" class SharedPrefFrame : public BackupFrame { enum FIELD { INVALID = 0, FILE = 1, // string KEY = 2, // string VALUE = 3, // string BOOLEANVALUE = 4, // bool STRINGSETVALUE = 5, // string (repeated) ISSTRINGSETVALUE = 6 // bool }; static Registrar s_registrar; public: inline explicit SharedPrefFrame(uint64_t count = 0); inline SharedPrefFrame(unsigned char const *bytes, size_t length, uint64_t count = 0); inline virtual ~SharedPrefFrame() override = default; inline virtual SharedPrefFrame *clone() const override; inline virtual SharedPrefFrame *move_clone() override; inline static BackupFrame *create(unsigned char const *bytes, size_t length, uint64_t count = 0); inline virtual void printInfo() const override; inline virtual FRAMETYPE frameType() const override; inline std::pair getData() const override; inline virtual bool validate(uint64_t) const override; inline std::string getHumanData() const override; inline unsigned int getField(std::string_view const &str) const; inline std::string key() const; inline std::vector value() const; inline std::string valueType() const; private: inline uint64_t dataSize() const override; }; inline SharedPrefFrame::SharedPrefFrame(uint64_t count) : BackupFrame(count) {} inline SharedPrefFrame::SharedPrefFrame(unsigned char const *bytes, size_t length, uint64_t count) : BackupFrame(bytes, length, count) {} inline SharedPrefFrame *SharedPrefFrame::clone() const { return new SharedPrefFrame(*this); } inline SharedPrefFrame *SharedPrefFrame::move_clone() { return new SharedPrefFrame(std::move(*this)); } inline BackupFrame *SharedPrefFrame::create(unsigned char const *bytes, size_t length, uint64_t count) { return new SharedPrefFrame(bytes, length, count); } inline void SharedPrefFrame::printInfo() const // virtual { Logger::message("Frame number: ", d_count); Logger::message(" Size: ", d_constructedsize); Logger::message(" Type: SHAREDPREFERENCEFRAME"); for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::FILE) Logger::message(" - (file : \"", bepaald::bytesToString(std::get<1>(p), std::get<2>(p)), "\" (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::KEY) Logger::message(" - (key : \"", bepaald::bytesToString(std::get<1>(p), std::get<2>(p)), "\" (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::VALUE) Logger::message(" - (value : \"", bepaald::bytesToString(std::get<1>(p), std::get<2>(p)), "\" (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::BOOLEANVALUE) Logger::message(" - (booleanvalue : \"", std::boolalpha, (bytesToUint64(std::get<1>(p), std::get<2>(p)) ? true : false), "\")"); else if (std::get<0>(p) == FIELD::STRINGSETVALUE) Logger::message(" - (stringsetvalue : \"", bepaald::bytesToString(std::get<1>(p), std::get<2>(p)), "\" (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::ISSTRINGSETVALUE) Logger::message(" - (isstringsetvalue : \"", std::boolalpha, (bytesToUint64(std::get<1>(p), std::get<2>(p)) ? true : false), "\")"); } } inline BackupFrame::FRAMETYPE SharedPrefFrame::frameType() const // virtual override { return FRAMETYPE::SHAREDPREFERENCE; } inline uint64_t SharedPrefFrame::dataSize() const { uint64_t size = 0; for (auto const &fd : d_framedata) { switch (std::get<0>(fd)) { case FIELD::FILE: case FIELD::KEY: case FIELD::VALUE: case FIELD::STRINGSETVALUE: { uint64_t stringsize = std::get<2>(fd); size += varIntSize(stringsize); size += stringsize + 1; // +1 for fieldtype + wiretype break; } case FIELD::BOOLEANVALUE: case FIELD::ISSTRINGSETVALUE: { uint64_t val = bytesToInt64(std::get<1>(fd), std::get<2>(fd)); size += varIntSize(val); size += 1; // for fieldtype + wiretype break; } } } // for size of this entire frame. size += varIntSize(size); return ++size; } inline std::pair SharedPrefFrame::getData() const { uint64_t size = dataSize(); unsigned char *data = new unsigned char[size]; uint64_t datapos = 0; datapos += setFieldAndWire(FRAMETYPE::SHAREDPREFERENCE, WIRETYPE::LENGTHDELIM, data + datapos); datapos += setFrameSize(size, data + datapos); for (auto const &fd : d_framedata) { switch (std::get<0>(fd)) { case FIELD::FILE: case FIELD::KEY: case FIELD::VALUE: case FIELD::STRINGSETVALUE: { datapos += putLengthDelimType(fd, data + datapos); break; } case FIELD::BOOLEANVALUE: case FIELD::ISSTRINGSETVALUE: { datapos += putVarIntType(fd, data + datapos); break; } } } return {data, size}; } // not sure about the requirements, but at least _a_ field should be set // also a key needs a value, and a value needs a key inline bool SharedPrefFrame::validate(uint64_t) const { if (d_framedata.empty()) return false; int foundfile = 0; int foundkey = 0; int foundvalue = 0; bool isstringsetvalue = false; for (auto const &p : d_framedata) { if (std::get<0>(p) != FIELD::FILE && std::get<0>(p) != FIELD::KEY && std::get<0>(p) != FIELD::VALUE && std::get<0>(p) != FIELD::BOOLEANVALUE && std::get<0>(p) != FIELD::STRINGSETVALUE && std::get<0>(p) != FIELD::ISSTRINGSETVALUE) return false; if (std::get<0>(p) == FIELD::FILE) ++foundfile; if (std::get<0>(p) == FIELD::KEY) ++foundkey; if (std::get<0>(p) == FIELD::VALUE || std::get<0>(p) == FIELD::BOOLEANVALUE || std::get<0>(p) == FIELD::STRINGSETVALUE || std::get<0>(p) == FIELD::ISSTRINGSETVALUE) ++foundvalue; if (std::get<0>(p) == FIELD::ISSTRINGSETVALUE) isstringsetvalue = (bytesToUint64(std::get<1>(p), std::get<2>(p)) ? true : false); } return (foundfile + foundkey + foundvalue) > 0 && (isstringsetvalue ? (foundkey <= foundvalue) : (foundkey == foundvalue)); } inline std::string SharedPrefFrame::getHumanData() const { std::string data; for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::FILE) data += "FILE:string:" + bepaald::bytesToString(std::get<1>(p), std::get<2>(p)) + "\n"; else if (std::get<0>(p) == FIELD::KEY) data += "KEY:string:" + bepaald::bytesToString(std::get<1>(p), std::get<2>(p)) + "\n"; else if (std::get<0>(p) == FIELD::VALUE) data += "VALUE:string:" + bepaald::bytesToString(std::get<1>(p), std::get<2>(p)) + "\n"; else if (std::get<0>(p) == FIELD::BOOLEANVALUE) data += "BOOLEANVALUE:bool:" + (bytesToInt64(std::get<1>(p), std::get<2>(p)) ? "true"s : "false"s) + "\n"; else if (std::get<0>(p) == FIELD::STRINGSETVALUE) data += "STRINGSETVALUE:string:" + bepaald::bytesToString(std::get<1>(p), std::get<2>(p)) + "\n"; else if (std::get<0>(p) == FIELD::ISSTRINGSETVALUE) data += "ISSTRINGSETVALUE:bool:" + (bytesToInt64(std::get<1>(p), std::get<2>(p)) ? "true"s : "false"s) + "\n"; } return data; } inline unsigned int SharedPrefFrame::getField(std::string_view const &str) const { if (str == "FILE") return FIELD::FILE; if (str == "KEY") return FIELD::KEY; if (str == "VALUE") return FIELD::VALUE; if (str == "BOOLEANVALUE") return FIELD::BOOLEANVALUE; if (str == "STRINGSETVALUE") return FIELD::STRINGSETVALUE; if (str == "ISSTRINGSETVALUE") return FIELD::ISSTRINGSETVALUE; return FIELD::INVALID; } inline std::string SharedPrefFrame::key() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::KEY) return bepaald::bytesToString(std::get<1>(p), std::get<2>(p)); return std::string(); } inline std::vector SharedPrefFrame::value() const { std::vector ret; for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::VALUE) // string { ret.push_back(bepaald::bytesToString(std::get<1>(p), std::get<2>(p))); break; } if (std::get<0>(p) == FIELD::BOOLEANVALUE) { ret.emplace_back((bytesToInt64(std::get<1>(p), std::get<2>(p)) ? "true"s : "false"s)); break; } if (std::get<0>(p) == FIELD::STRINGSETVALUE) ret.emplace_back(bepaald::bytesToString(std::get<1>(p), std::get<2>(p))); } return ret; } inline std::string SharedPrefFrame::valueType() const { for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::VALUE) return "STRING"; if (std::get<0>(p) == FIELD::BOOLEANVALUE) return "BOOL"; if (std::get<0>(p) == FIELD::STRINGSETVALUE) return "STRINGSET"; if (std::get<0>(p) == FIELD::ISSTRINGSETVALUE) if (bytesToInt64(std::get<1>(p), std::get<2>(p)) == true) return "STRINGSET"; } Logger::warning("Currently unsupported value type requested from SharedPrefrenceFrame"); return std::string(); } #endif signalbackup-tools-20250313-1/sharedprefframe/sharedprefframe.ih000066400000000000000000000014071476450434500245460ustar00rootroot00000000000000/* Copyright (C) 2019-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sharedprefframe.h" signalbackup-tools-20250313-1/sharedprefframe/statics.cc000066400000000000000000000015471476450434500230540ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sharedprefframe.ih" SharedPrefFrame::Registrar SharedPrefFrame::s_registrar(FRAMETYPE::SHAREDPREFERENCE, create); signalbackup-tools-20250313-1/signalbackup/000077500000000000000000000000001476450434500203715ustar00rootroot00000000000000signalbackup-tools-20250313-1/signalbackup/addsmsmessage.cc000066400000000000000000000126641476450434500235310ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::addSMSMessage(std::string const &body, std::string const &address, long long int timestamp, long long int thread, bool incoming) { /* DEPRECATED, unused and almost useless anyway... */ if (d_databaseversion >= 168) { Logger::error("Unsupported database version"); return; } //get address automatically -> msg partner for normal thread, sender for incoming group, groupid (__textsecure__!xxxxx) for outgoing // maybe do something with 'notified'? it is almost always 0, but a few times it is 1 on incoming msgs in my db // INSERT INTO sms(_id, thread_id, address, address_device_id, person, date, date_sent, protocol, read, status, type, reply_path_present, delivery_receipt_count, subject, body, mismatched_identities, service_center, subscription_id, expires_in, expire_started, notified, read_receipt_count, unidentified); if (incoming) { if (d_database.tableContainsColumn("sms", "protocol") && d_database.tableContainsColumn("sms", "reply_path_present") && d_database.tableContainsColumn("sms", "service_center")) // removed in dbv166 d_database.exec("INSERT INTO sms(thread_id, body, " + d_sms_date_received + ", date_sent, " + d_sms_recipient_id + ", type, protocol, read, reply_path_present, service_center) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", {thread, body, timestamp, timestamp, address, 10485780ll, 31337ll, 1ll, 1ll, std::string("GCM")}); else d_database.exec("INSERT INTO sms(thread_id, body, " + d_sms_date_received + ", date_sent, " + d_sms_recipient_id + ", type, read) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", {thread, body, timestamp, timestamp, address, 10485780ll, 1ll}); } else { d_database.exec("INSERT INTO sms(thread_id, body, " + d_sms_date_received + ", date_sent, " + d_sms_recipient_id + ", type, read, delivery_receipt_count) VALUES(?, ?, ?, ?, ?, ?, ?, ?)", {thread, body, timestamp, timestamp, address, 10485783ll, 1ll, 1ll}); } // update message count updateThreadsEntries(thread); /* protocol on incoming is 31337? on outgoing always null what i know about types quickly... type 10485784 = outgoing, send failed 2097684 = incoming, safety number changed? 10551319 = outgoing, updated group 3 = call 2 = call 10485783 = outgoing normal (secure) message, properly received and such 10458780 = incoming normal (secure) message, properly received and such? 10747924 = incoming, disabled disappearing msgs */ } /* void SignalBackup::addMMSMessage() { MMS TABLE: CREATE TABLE mms ( _id INTEGER PRIMARY KEY, ** thread_id INTEGER, ** date INTEGER, ** date_received INTEGER, ** msg_box INTEGER, read INTEGER DEFAULT 0, m_id TEXT, sub TEXT, sub_cs INTEGER, ** body TEXT, part_count INTEGER, ct_t TEXT, ct_l TEXT, ** address TEXT, -> for group messages, this is '__textsecure_group__!xxxxx...' for outgoing, +316xxxxxxxx for incoming address_device_id INTEGER, exp INTEGER, m_cls TEXT, m_type INTEGER, --> in my database either 132 OR 128, always 128 for outgoing, 132 for incoming v INTEGER, m_size INTEGER, pri INTEGER, rr INTEGER, rpt_a INTEGER, resp_st INTEGER, st INTEGER, --> null, or '1' in my database, always null for outgoing, 1 for incoming tr_id TEXT, retr_st INTEGER, retr_txt TEXT, retr_txt_cs INTEGER, read_status INTEGER, ct_cls INTEGER, resp_txt TEXT, d_tm INTEGER, delivery_receipt_count INTEGER DEFAULT 0, --> set auto to number of recipients mismatched_identities TEXT DEFAULT NULL, network_failures TEXT DEFAULT NULL, d_rpt INTEGER, subscription_id INTEGER DEFAULT -1, expires_in INTEGER DEFAULT 0, expire_started INTEGER DEFAULT 0, notified INTEGER DEFAULT 0, --> both 0 and 1 present, most often '0' (32553 vs 199), only 1 on incoming types read_receipt_count INTEGER DEFAULT 0, --> always zero for me... but... quote_id INTEGER DEFAULT 0, quote_author TEXT, quote_body TEXT, quote_attachment INTEGER DEFAULT -1, --> always -1 in my db?? shared_contacts TEXT, quote_missing INTEGER DEFAULT 0, unidentified INTEGER DEFAULT 0, --> both 0 and 1 in my db previews TEXT) --> CREATE TABLE part ( _id INTEGER PRIMARY KEY, mid INTEGER, seq INTEGER DEFAULT 0, ct TEXT, name TEXT, chset INTEGER, cd TEXT, fn TEXT, cid TEXT, cl TEXT,c tt_s INTEGER, ctt_t TEXT, encrypted INTEGER, pending_push INTEGER, _data TEXT, data_size INTEGER, file_name TEXT, thumbnail TEXT, aspect_ratio REAL, unique_id INTEGER NOT NULL, digest BLOB, fast_preflight_id TEXT, voice_note INTEGER DEFAULT 0, data_random BLOB, thumbnail_random BLOB, width INTEGER DEFAULT 0, height INTEGER DEFAULT 0, quote INTEGER DEFAULT 0, caption TEXT DEFAULT NULL) } */ /* replaceAttachment() */ signalbackup-tools-20250313-1/signalbackup/applyranges.cc000066400000000000000000000320031476450434500232230ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "msgrange.h" /* While the data in 'body' is utf8 encoded, the range (start+length) deals with utf16 (one or two 16bit bytes) at each point in the string (which only deals with single 8bit bytes, we need to know the number of bytes to skip to the next char (which is its utf8 size) as well as compensate the index as if the data were 16bit utf16 (possibly multibyte). It's a mess, and I hope it works eg: RANGE: {start: 3, length: 1, replacement: 'AA'} '💩 ...' 0xF0 0x9F 0x92 0xA9 0x20 0xef 0xbf 0xbc ... \_______________/ | \_______/ emoji space placeholder idx0: '0xF0' => utf8 size == 4 => idx0->idx4 => utf16 size == 2 => range{start: 3 + (4 - 2)} idx4: '0x20' => utf8 size == 1 => idx4->idx5 => utf16 size == 1 => range{start: 5 + (1 - 1)} idx5: MATCH! adjust length at this position from utf16 codepoints to 8bit bytes and replace => utf16 length 1 at idx 5 == 3 => replace 3 with 'AA' => 0xF0 0x9F 0x92 0xA9 0x20 0x41 0x41 ... => idx5->idx7 (5 + */ void SignalBackup::applyRanges(std::string *body, std::vector *ranges, std::set *positions_excluded_from_escape) const { // sort the ranges and adjust them // to deal with overlaps prepRanges(ranges); // then, apply the ranges: unsigned int rangesidx = 0; unsigned int bodyidx = 0; while (bodyidx < body->size() && rangesidx < ranges->size()) { //std::cout << "Checking char idx: " << bodyidx << "('" << (*body)[bodyidx] << "') for range with start: " << (*ranges)[rangesidx].start << std::flush; int charsizeinbytes = bytesToUtf8CharSize(*body, bodyidx); int charsizeinutf16entities = utf16CharSize(*body, bodyidx); //std::cout << ", charsize: " << charsizeinbytes << " codepoints: " << charsizeinutf16entities << std::endl; if (bodyidx == (*ranges)[rangesidx].start) { //int length = bytesToUtf8CharSize(*body, bodyidx, (*ranges)[rangesidx].length); int length = numBytesInUtf16Substring(*body, bodyidx, (*ranges)[rangesidx].length); std::string pre = (*ranges)[rangesidx].pre; std::string replacement = (*ranges)[rangesidx].replacement; std::string post = (*ranges)[rangesidx].post; if (replacement.empty()) replacement = body->substr(bodyidx, length); body->replace(bodyidx, length, pre + replacement + post); for (unsigned int i = 0; i < pre.size(); ++i) if (positions_excluded_from_escape) positions_excluded_from_escape->insert(i + bodyidx); for (unsigned int i = 0; i < post.size(); ++i) if (positions_excluded_from_escape) positions_excluded_from_escape->insert(i + bodyidx + pre.size() + replacement.size()); //std::cout << "BODY: " << *body << std::endl; for (unsigned int i = rangesidx + 1; i < ranges->size(); ++i) (*ranges)[i].start += pre.size() + post.size() + (replacement.size() - (*ranges)[rangesidx].length); // skip the just inserted string bodyidx += pre.size() + post.size() + replacement.size(); // look for next range // while the prepwork should make sure it is the first one, // interactions with mention replacements might throw it off? // just to be sure, lets not just `++rangesidx' while (++rangesidx < ranges->size() && (*ranges)[rangesidx].start < bodyidx) ; continue; } // update all following ranges for multibyte char for (unsigned int i = rangesidx; i < ranges->size(); ++i) (*ranges)[i].start += (charsizeinbytes - charsizeinutf16entities); // next char... bodyidx += charsizeinbytes; } } /* This should work for (possible) overlapping ranges depending on how html renders certain things. */ void SignalBackup::prepRanges(std::vector *ranges) const { std::sort(ranges->begin(), ranges->end()); // if (ranges->size()) // { // std::cout << "got range:" << std::endl; // for (auto const &r : *ranges) // std::cout << "[" << r.start << "-" << r.start + r.length << "] '" << r.pre << "' '" << r.post << "'" << std::endl; // } /* After sorting, there are the following possible overlaps between range_i-1 and range_i: (1) O1: <1> O1: <2> -> N1: <1><2> ! NOTE only one of them can have replacement, not both * ! copies replacement from 1/2 (2) O1: <1> O2: <2> -> N1: <2><1> N2: "" ! NOTE <2> cannot have replacement (<1> would fall in the middle of it) ** ! IF <1> has replacement has replacement (3) O1: <1> O2: <2> -> N1: <1> "" N2: <2> ! NOTE <1> cannot have replacement (<2> would fall in the middle of it) ** ! IF <2> has replacement has replacement (4a) O1: <1> O2: <2> -> N1: <1> "" N2: <2> N3: <2> ! NOTE: <1> is unbroken! ! NOTE neither <1> or <2> can have replacement ** (4b) ??? O1: <1> O2: <2> -> N1: <1> <2> N2: <1> N3: "" ! NOTE : <2> is unbroken! ! NOTE neither <1> or <2> can have replacement ** !! NOTE CASE 4 (a&b) DO NOT SEEM TO BE ALLOWED IN SIGNAL !!! NOTE IN CASE 4ab, ONE OF THE RANGES MUST BE BROKEN (IF BOTH HAVE nobreak == true, ONE WILL BREAK ANYWAY) (5) O1: <1> O2: <2> -> N1: <1> "" N2: (O2) N3: "" ! NOTE <1> cannot have replacement (<2> would fall in the middle of it) ** ! IF <2> has replacement has replacement *) This is hypothetical, no action can currently cause the same string location to be replaced by multiple substrings **) This is also hypothetical, all replacement-ranges (mentions) currently have length == 1, which makes it impossible for another range to start/end in the middle of it */ for (unsigned int i = 1; i < ranges->size(); ++i) { if ((*ranges)[i].start == (*ranges)[i - 1].start) { if ((*ranges)[i].length == (*ranges)[i -1].length) { // (1) SAME START, SAME FINISH if (!(*ranges)[i - 1].replacement.empty() && !(*ranges)[i].replacement.empty()) { Logger::warning("Illegal range-set (overlapping ranges both have replacement)."); continue; } //std::cout << "CASE 1" << std::endl; (*ranges)[i - 1].pre += (*ranges)[i].pre; (*ranges)[i - 1].post = (*ranges)[i].post + (*ranges)[i - 1].post; if (!(*ranges)[i].replacement.empty()) // only one can have replacement, if it's i-1 its already kept... (*ranges)[i - 1].replacement = (*ranges)[i].replacement; ranges->erase(ranges->begin() + i); return prepRanges(ranges); } // (2) SAME START, LATER FINISH // else -> i.length > (i - 1).length if (!(*ranges)[i].replacement.empty()) { Logger::warning("Illegal range-set (overlapping ranges both have replacement)."); continue; } //std::cout << "CASE 2" << std::endl; // (*ranges)[i - 1].replacement is automatically kept if it has one (*ranges)[i - 1].pre = (*ranges)[i].pre + (*ranges)[i - 1].pre; (*ranges)[i].pre = ""; (*ranges)[i].start = (*ranges)[i - 1].start + (*ranges)[i - 1].length; (*ranges)[i].length -= (*ranges)[i - 1].length; return prepRanges(ranges); } // else (i].start > (i - 1).start) if ((*ranges)[i].start + (*ranges)[i].length == (*ranges)[i - 1].start + (*ranges)[i - 1].length) // same end pos { // (3) LATER START, SAME FINISH if (!(*ranges)[i - 1].replacement.empty()) { Logger::warning("Warning. Illegal range-set (overlapping ranges both have replacement)."); continue; } //std::cout << "CASE 3" << std::endl; // (*ranges)[i].replacement is automatically kept if it has one (*ranges)[i].post = (*ranges)[i].post + (*ranges)[i - 1].post; (*ranges)[i - 1].post = ""; (*ranges)[i - 1].length = (*ranges)[i].start - (*ranges)[i - 1].start; return prepRanges(ranges); } if ((*ranges)[i].start < (*ranges)[i - 1].start + (*ranges)[i - 1].length) { if ((*ranges)[i].start + (*ranges)[i].length > (*ranges)[i - 1].start + (*ranges)[i - 1].length) // end later { // (4) LATER START, LATER FINISH" if (!(*ranges)[i - 1].replacement.empty() || !(*ranges)[i].replacement.empty()) { Logger::warning("Warning. Illegal range-set (overlapping ranges both have replacement)."); continue; } if ((*ranges)[i].nobreak && !(*ranges)[i - 1].nobreak) { //std::cout << "CASE 4b" << std::endl; Range newrange = {(*ranges)[i - 1].start, (*ranges)[i].start - (*ranges)[i - 1].start, (*ranges)[i - 1].pre, "", (*ranges)[i - 1].post + (*ranges)[i].pre, false}; // N1 ranges->emplace_back(newrange); (*ranges)[i - 1].start = (*ranges)[i].start; (*ranges)[i - 1].length = (*ranges)[i - 1].length - newrange.length; // N2 (*ranges)[i].start = (*ranges)[i - 1].start + (*ranges)[i - 1].length; (*ranges)[i].pre = ""; (*ranges)[i].length = (*ranges)[i].length - (*ranges)[i - 1].length; // N3 } else { if ((*ranges)[i].nobreak && (*ranges)[i - 1].nobreak) [[unlikely]] { Logger::warning("Illegal ranges: overlapping but unbreakable"); // std::cout << i << std::endl; // std::cout << (*ranges)[i].start << std::endl; // std::cout << (*ranges)[i].length << std::endl; // std::cout << (*ranges)[i].pre << std::endl; // std::cout << (*ranges)[i].post << std::endl; // std::cout << i - 1 << std::endl; // std::cout << (*ranges)[i-1].start << std::endl; // std::cout << (*ranges)[i-1].length << std::endl; // std::cout << (*ranges)[i-1].pre << std::endl; // std::cout << (*ranges)[i-1].post << std::endl; } //std::cout << "CASE 4a" << std::endl; Range newrange = {(*ranges)[i].start, (*ranges)[i - 1].length - ((*ranges)[i].start - (*ranges)[i - 1].start), (*ranges)[i].pre, "", (*ranges)[i].post + (*ranges)[i - 1].post, false}; // N2 ranges->emplace_back(newrange); (*ranges)[i - 1].length = newrange.start - (*ranges)[i - 1].start; // N1 (*ranges)[i - 1].post = ""; (*ranges)[i].start = newrange.start + newrange.length; // N3 (*ranges)[i].length -= newrange.length; } return prepRanges(ranges); } //if ((*ranges)[i].start + (*ranges)[i].length < (*ranges)[i - 1].start + (*ranges)[i - 1].length) // end sooner // (5) LATER START, EARLIER FINISH if (!(*ranges)[i - 1].replacement.empty()) { Logger::warning("Warning. Illegal range-set (overlapping ranges both have replacement)."); continue; } //std::cout << "CASE 5" << std::endl; Range newrange = {(*ranges)[i].start + (*ranges)[i].length, (*ranges)[i - 1].start + (*ranges)[i - 1].length - ((*ranges)[i].start + (*ranges)[i].length), "", "", (*ranges)[i - 1].post, false}; ranges->emplace_back(newrange); // -> N3 (*ranges)[i - 1].post = ""; (*ranges)[i - 1].length = (*ranges)[i].start - (*ranges)[i - 1].start; // O1 -> N1 // (*ranges)[i].replacement is automatically kept if it has one // O2 == N2 return prepRanges(ranges); } } // std::cout << "DONE!" << std::endl; } signalbackup-tools-20250313-1/signalbackup/buildsqlstatementframe.cc000066400000000000000000000071571476450434500254710ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" SqlStatementFrame SignalBackup::buildSqlStatementFrame(std::string const &table, std::vector const &headers, std::vector const &result) const { //std::cout << "Building new frame:" << std::endl; SqlStatementFrame newframe; std::string newstatement = "INSERT INTO " + table + " ("; for (unsigned int i = 0; i < headers.size(); ++i) { newstatement.append(headers[i]); if (i < headers.size() - 1) newstatement.append(","); else newstatement.append(")"); } newstatement += " VALUES ("; for (unsigned int j = 0; j < result.size(); ++j) { if (j < result.size() - 1) newstatement.append("?,"); else newstatement.append("?)"); if (result[j].type() == typeid(long long int)) newframe.addIntParameter(std::any_cast(result[j])); else if (result[j].type() == typeid(nullptr)) newframe.addNullParameter(); else if (result[j].type() == typeid(std::string)) newframe.addStringParameter(std::any_cast(result[j])); else if (result[j].type() == typeid(std::pair, size_t>)) newframe.addBlobParameter(std::any_cast, size_t>>(result[j])); else if (result[j].type() == typeid(double)) newframe.addDoubleParameter(std::any_cast(result[j])); else Logger::warning("UNHANDLED PARAMETER TYPE = ", result[j].type().name()); } newframe.setStatementField(newstatement); //newframe.printInfo(); return newframe; } SqlStatementFrame SignalBackup::buildSqlStatementFrame(std::string const &table, std::vector const &result) const { //std::cout << "Building new frame:" << std::endl; SqlStatementFrame newframe; std::string newstatement = "INSERT INTO " + table + " VALUES ("; for (unsigned int j = 0; j < result.size(); ++j) { if (j < result.size() - 1) newstatement.append("?,"); else newstatement.append("?)"); if (result[j].type() == typeid(long long int)) newframe.addIntParameter(std::any_cast(result[j])); else if (result[j].type() == typeid(nullptr)) newframe.addNullParameter(); else if (result[j].type() == typeid(std::string)) newframe.addStringParameter(std::any_cast(result[j])); else if (result[j].type() == typeid(std::pair, size_t>)) newframe.addBlobParameter(std::any_cast, size_t>>(result[j])); else if (result[j].type() == typeid(double)) newframe.addDoubleParameter(std::any_cast(result[j])); else Logger::warning("UNHANDLED PARAMETER TYPE = ", result[j].type().name()); } newframe.setStatementField(newstatement); //newframe.printInfo(); //std::exit(0); return newframe; } signalbackup-tools-20250313-1/signalbackup/checkdbintegrity.cc000066400000000000000000000053241476450434500242260ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::checkDbIntegrity(bool warn) const { SqliteDB::QueryResults results; // CHECKING FOREIGN KEY CONSTRAINTS if (!warn) Logger::message_start("Checking foreign key constraints..."); d_database.exec("SELECT DISTINCT [table],[parent],[fkid] FROM pragma_foreign_key_check", &results); if (results.rows()) { if (!warn) Logger::error("Foreign key constraint violated. This will not end well, aborting." "\n\n" "Please report this error to the program author."); else Logger::warning("Foreign key constraint violated."); results.prettyPrint(d_truncate); return false; } if (!warn) Logger::message_end(" ok"); // std::cout << "Checking database integrity (quick)..." << std::flush; // d_database.exec("SELECT * FROM pragma_quick_check", &results); // if (results.rows() && results.valueAsString(0, "quick_check") != "ok") // { // std::cout << std::endl << bepaald::bold_on << "ERROR" << bepaald::bold_off << " Database integrity check failed. This will not end well, aborting." << std::endl // << " " " Please report this error to the program author." << std::endl; // results.prettyPrint(); // return false; // } // std::cout << " ok" << std::endl; // CHECKING DATABASE if (!warn) Logger::message_start("Checking database integrity (full)..."); d_database.exec("SELECT * FROM pragma_integrity_check", &results); if (results.rows() && results.valueAsString(0, "integrity_check") != "ok") { if (!warn) Logger::error("Database integrity check failed. This will not end well, aborting." "\n\n" "Please report this error to the program author."); else Logger::warning("Foreign key constraint violated."); results.prettyPrint(d_truncate); return false; } if (!warn) Logger::message_end(" ok"); return true; } signalbackup-tools-20250313-1/signalbackup/cleanattachments.cc000066400000000000000000000041071476450434500242200ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::cleanAttachments() { //std::map, std::unique_ptr> d_attachments; //maps to attachment // remove unused attachments Logger::message(" Deleting unused attachments..."); SqliteDB::QueryResults results; int constexpr INVALID_ID = -10; d_database.exec("SELECT _id," + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id"s : "-1 AS unique_id"s) + " FROM " + d_part_table, &results); for (auto it = d_attachments.begin(); it != d_attachments.end();) { bool found = false; for (unsigned int i = 0; i < results.rows(); ++i) { long long int rowid = INVALID_ID; if (results.valueHasType(i, "_id")) rowid = results.getValueAs(i, "_id"); long long int uniqueid = INVALID_ID; if (results.valueHasType(i, "unique_id")) uniqueid = results.getValueAs(i, "unique_id"); if (rowid != INVALID_ID && uniqueid != INVALID_ID && it->first.first == static_cast(rowid) && it->first.second == static_cast(uniqueid)) { found = true; break; } } if (!found) it = d_attachments.erase(it); else ++it; } return true; } signalbackup-tools-20250313-1/signalbackup/cleandatabasebymessages.cc000066400000000000000000000547701476450434500255470ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::cleanDatabaseByMessages() { Logger::message(__FUNCTION__); Logger::message(" Deleting attachment entries from '", d_part_table, "' not belonging to remaining ", d_mms_table, " entries"); d_database.exec("DELETE FROM " + d_part_table + " WHERE " + d_part_mid + " NOT IN (SELECT DISTINCT _id FROM " + d_mms_table + ")"); Logger::message(" Deleting other threads from 'thread'..."); d_database.exec("DELETE FROM thread WHERE _id NOT IN (SELECT DISTINCT thread_id FROM " + d_mms_table + ")" + (d_database.containsTable("sms") ? " AND _id NOT IN (SELECT DISTINCT thread_id FROM sms)" : "")); updateThreadsEntries(); if (d_database.containsTable("mention")) { Logger::message(" Deleting entries from 'mention' not belonging to remaining mms entries"); d_database.exec("DELETE FROM mention WHERE message_id NOT IN (SELECT DISTINCT _id FROM " + d_mms_table + ") OR thread_id NOT IN (SELECT DISTINCT _id FROM thread)"); } //Logger::message("Groups left:"); //runSimpleQuery("SELECT group_id,title,members FROM groups"); Logger::message_start(" Deleting removed groups..."); if (d_databaseversion < 27) { d_database.exec("DELETE FROM groups WHERE group_id NOT IN (SELECT DISTINCT " + d_thread_recipient_id + " FROM thread)"); Logger::message_end(" (", d_database.changed(), ")"); } else { d_database.exec("DELETE FROM groups WHERE recipient_id NOT IN (SELECT DISTINCT " + d_thread_recipient_id + " FROM thread) RETURNING group_id"); Logger::message_end(" (", d_database.changed(), ")"); if (d_database.containsTable("group_membership")) d_database.exec("DELETE FROM group_membership WHERE group_id NOT IN (SELECT DISTINCT group_id FROM groups)"); } //Logger::message("Groups left:"); //runSimpleQuery("SELECT group_id,title,members FROM groups"); //runSimpleQuery("SELECT _id, recipient_ids, system_display_name FROM recipient_preferences"); // remove all call_link's. These are special group types, // which cant have messages? So cleanByMessages, should drop // all these? if (d_database.containsTable("call_link")) { d_database.exec("DELETE FROM call_link"); } if (d_database.containsTable("msl_message") && d_database.containsTable("msl_recipient") && d_database.containsTable("msl_payload")) { Logger::message_start(" Deleting unneeded MessageSendLog entries..."); long long int count_msg = 0, count_payl = 0, count_rec = 0; // note this function is generally called because messages (and/or attachments) have been deleted // the msl_payload table has triggers that delete its entries: // (delete from msl_payload where _id in (select payload_id from message where message_id = (message.deleted_id/part.deletedmid))) // apparently these triggers even when editing within this program, even though the 'ON DELETE CASCADE' stuff does not and // foreign key constraints are not enforced... This causes a sort of circular thing here but I think we can just clean up the // msl_message table according to still-existing msl_payloads first d_database.exec("DELETE FROM msl_message WHERE payload_id NOT IN (SELECT DISTINCT _id FROM msl_payload)"); count_msg += d_database.changed(); // delete from msl_message table if message does not exist anymore if (d_database.containsTable("sms")) { d_database.exec("DELETE FROM msl_message WHERE is_mms IS NOT 1 AND message_id NOT IN (SELECT _id FROM sms)"); count_msg += d_database.changed(); d_database.exec("DELETE FROM msl_message WHERE is_mms IS 1 AND message_id NOT IN (SELECT _id FROM " + d_mms_table + ")"); count_msg += d_database.changed(); } else { d_database.exec("DELETE FROM msl_message WHERE message_id NOT IN (SELECT _id FROM " + d_mms_table + ")"); count_msg += d_database.changed(); } // now delete all msl_payloads for non existing msl_messages d_database.exec("DELETE FROM msl_payload WHERE _id NOT IN (SELECT DISTINCT payload_id FROM msl_message)"); count_payl = d_database.changed(); // lastly delete recipient for non existing payloads d_database.exec("DELETE FROM msl_recipient WHERE payload_id NOT IN (SELECT DISTINCT _id FROM msl_payload)"); count_rec = d_database.changed(); Logger::message_end(" (", count_msg, ", ", count_payl, ", ", count_rec, ")"); } if (d_database.containsTable("reaction")) // dbv >= 121 { // delete reactions for messages that do not exist Logger::message_start(" Deleting reactions to non-existing messages..."); long long int count = 0; if (d_database.containsTable("sms")) { d_database.exec("DELETE FROM reaction WHERE is_mms IS NOT 1 AND message_id NOT IN (SELECT _id FROM sms)"); count += d_database.changed(); d_database.exec("DELETE FROM reaction WHERE is_mms IS 1 AND message_id NOT IN (SELECT _id FROM " + d_mms_table + ")"); count += d_database.changed(); } else { d_database.exec("DELETE FROM reaction WHERE message_id NOT IN (SELECT _id FROM " + d_mms_table + ")"); count += d_database.changed(); } Logger::message_end(" (", count, ")"); } if (d_database.containsTable("call")) // dbv >= ~170? { Logger::message_start(" Deleting call details from non-existing messages..."); d_database.exec("DELETE FROM call WHERE message_id NOT IN (SELECT _id FROM " + d_mms_table + ")"); Logger::message_end(" (", d_database.changed(), ")"); } // delete story_sends entries that no longer refer to an existing message? if (d_database.tableContainsColumn("story_sends", "message_id")) { d_database.exec("DELETE FROM story_sends WHERE message_id NOT IN (SELECT _id FROM " + d_mms_table + ")"); } // clean up distribution_lists... if (d_database.containsTable("distribution_list")) { //d_database.prettyPrint(true, "SELECT * FROM distribution_list"); d_database.exec("DELETE FROM distribution_list WHERE recipient_id NOT IN (SELECT DISTINCT " + d_thread_recipient_id + " FROM thread) AND _id != 1"); d_database.exec("DELETE FROM distribution_list_member WHERE list_id NOT IN (SELECT DISTINCT _id FROm distribution_list)"); //d_database.prettyPrint(true, "SELECT * FROM distribution_list"); } // delete name_collision for non existing thread if(d_database.containsTable("name_collision")) { d_database.exec("DELETE FROM name_collision WHERE thread_id NOT IN (SELECT _id FROM thread)"); // delete name_collision_membership if name_collision was deleted d_database.exec("DELETE FROM name_collision_membership WHERE collision_id NOT IN (SELECT _id FROM name_collision)"); } if (d_databaseversion < 24) { Logger::message(" Deleting unreferenced recipient_preferences entries..."); //runSimpleQuery("WITH RECURSIVE split(word, str) AS (SELECT '', members||',' FROM groups UNION ALL SELECT substr(str, 0, instr(str, ',')), substr(str, instr(str, ',')+1) FROM split WHERE str!='') SELECT DISTINCT split.word FROM split WHERE word!='' UNION SELECT DISTINCT " + d_sms_recipient_id + " FROM sms UNION SELECT DISTINCT " + d_mms_recipient_id + " FROM mms"); // this gets all recipient_ids/addresses ('+31612345678') from still existing groups and sms/mms d_database.exec("DELETE FROM recipient_preferences WHERE recipient_ids NOT IN (WITH RECURSIVE split(word, str) AS (SELECT '', members||',' FROM groups UNION ALL SELECT substr(str, 0, instr(str, ',')), substr(str, instr(str, ',')+1) FROM split WHERE str!='') SELECT DISTINCT split.word FROM split WHERE word!='' UNION SELECT DISTINCT " + d_sms_recipient_id + " FROM sms UNION SELECT DISTINCT " + d_mms_recipient_id + " FROM " + d_mms_table + ")"); } else { Logger::message(" Deleting unreferenced recipient entries..."); //runSimpleQuery("SELECT group_concat(_id,',') FROM recipient"); //runSimpleQuery("WITH RECURSIVE split(word, str) AS (SELECT '', members||',' FROM groups UNION ALL SELECT substr(str, 0, instr(str, ',')), substr(str, instr(str, ',')+1) FROM split WHERE str!='') SELECT DISTINCT split.word FROM split WHERE word!='' UNION SELECT DISTINCT " + d_sms_recipient_id + " FROM sms UNION SELECT DISTINCT " + d_mms_recipient_id FROM mms UNION SELECT DISTINCT " + d_thread_recipient_id + " FROM thread"); // this gets all recipient_ids/addresses ('+31612345678') from still existing groups and sms/mms // KEEP recipients WITH _id IN remapped_recipients.old_id!?!? std::set referenced_recipients; if (d_database.tableContainsColumn("sms", "reactions")) { SqliteDB::QueryResults reactionresults; d_database.exec("SELECT DISTINCT reactions FROM sms WHERE reactions IS NOT NULL", &reactionresults); for (unsigned int i = 0; i < reactionresults.rows(); ++i) { ReactionList reactions(reactionresults.getValueAs, size_t>>(i, "reactions")); for (unsigned int j = 0; j < reactions.numReactions(); ++j) referenced_recipients.insert(reactions.getAuthor(j)); } } if (d_database.tableContainsColumn(d_mms_table, "reactions")) { SqliteDB::QueryResults reactionresults; d_database.exec("SELECT DISTINCT reactions FROM " + d_mms_table + " WHERE reactions IS NOT NULL", &reactionresults); for (unsigned int i = 0; i < reactionresults.rows(); ++i) { ReactionList reactions(reactionresults.getValueAs, size_t>>(i, "reactions")); for (unsigned int j = 0; j < reactions.numReactions(); ++j) referenced_recipients.insert(reactions.getAuthor(j)); } } if (d_verbose) [[unlikely]] if (d_database.tableContainsColumn("sms", "reactions") || d_database.tableContainsColumn(d_mms_table, "reactions")) Logger::message("Got recipients from reactions. List now: ", std::vector(referenced_recipients.begin(), referenced_recipients.end())); getGroupV1MigrationRecipients(&referenced_recipients); if (d_verbose) [[unlikely]] Logger::message("Got recipients from gv1migration. List now: ", std::vector(referenced_recipients.begin(), referenced_recipients.end())); // get (former)group members SqliteDB::QueryResults results; for (auto const &members : {"members"s, d_groups_v1_members}) { if (!d_database.tableContainsColumn("groups", members)) continue; d_database.exec("SELECT "s + members + " FROM groups WHERE " + members + " IS NOT NULL", &results); for (unsigned int i = 0; i < results.rows(); ++i) { std::string membersstr = results.getValueAs(i, members); std::stringstream ss(membersstr); while (ss.good()) { std::string substr; std::getline(ss, substr, ','); //Logger::message("ADDING ", members, " MEMBER: ", substr); referenced_recipients.insert(bepaald::toNumber(substr)); } } } if (d_database.containsTable("group_membership")) if (d_database.exec("SELECT DISTINCT recipient_id FROM group_membership", &results)) for (unsigned int i = 0; i < results.rows(); ++i) referenced_recipients.insert(results.getValueAs(i, "recipient_id")); if (d_verbose) [[unlikely]] Logger::message("Got recipients from groupmemberships. List now: ", std::vector(referenced_recipients.begin(), referenced_recipients.end())); // get recipients mentioned in group updates (by uuid) std::vector mentioned_in_group_updates(getGroupUpdateRecipients()); for (long long int id : mentioned_in_group_updates) referenced_recipients.insert(id); if (d_verbose) [[unlikely]] Logger::message("Got recipients from mentions. List now: ", std::vector(referenced_recipients.begin(), referenced_recipients.end())); // get recipient_id of releasechannel for (auto const &kv : d_keyvalueframes) if (kv->key() == "releasechannel.recipient_id") referenced_recipients.insert(bepaald::toNumber(kv->value())); if (d_verbose) [[unlikely]] Logger::message("Got recipients from releasechannel. List now: ", std::vector(referenced_recipients.begin(), referenced_recipients.end())); // get recipient for MY_STORY distribution_list, make sure it is referenced (it must exist) long long int my_story_recipient = -1; if (d_database.containsTable("distribution_list")) { my_story_recipient = d_database.getSingleResultAs("SELECT recipient_id FROM distribution_list WHERE _id = 1", -1); if (my_story_recipient != -1) referenced_recipients.insert(my_story_recipient); } if (d_verbose) [[unlikely]] Logger::message("Got recipients from MY_STORY. List now: ", std::vector(referenced_recipients.begin(), referenced_recipients.end())); // get recipients referenced in call-links if (d_database.containsTable("call_link")) { SqliteDB::QueryResults call_link_recipients; if (d_database.exec("SELECT DISTINCT recipient_id FROM call_link", &call_link_recipients)) for (unsigned int clr = 0; clr < call_link_recipients.rows(); ++clr) referenced_recipients.insert(call_link_recipients.valueAsInt(clr, "recipient_id")); } if (d_verbose) [[unlikely]] Logger::message("Got recipients from call_link table. List now: ", std::vector(referenced_recipients.begin(), referenced_recipients.end())); std::string referenced_recipients_query; if (!referenced_recipients.empty()) { referenced_recipients_query = " UNION SELECT * FROM (VALUES"; for (auto const &r : referenced_recipients) { //Logger::message("AUTHOR : ", r); referenced_recipients_query += "(" + bepaald::toString(r) + "),"; } referenced_recipients_query.pop_back(); referenced_recipients_query += ")"; } //Logger::message("QUERY: ", reaction_authors_query); SqliteDB::QueryResults deleted_recipients; d_database.exec("DELETE FROM recipient WHERE _id NOT IN" " (SELECT DISTINCT " + d_mms_recipient_id + " FROM " + d_mms_table + (d_database.containsTable("sms") ? " UNION SELECT DISTINCT " + d_sms_recipient_id + " FROM sms"s : "") + (d_database.tableContainsColumn(d_mms_table, "quote_author") ? " UNION SELECT DISTINCT quote_author FROM " + d_mms_table + " WHERE quote_author IS NOT NULL"s : ""s) + (d_database.tableContainsColumn(d_mms_table, "to_recipient_id") ? " UNION SELECT DISTINCT to_recipient_id FROM " + d_mms_table : ""s) + (d_database.containsTable("mention") ? " UNION SELECT DISTINCT recipient_id FROM mention"s : ""s) + (d_database.containsTable("reaction") ? " UNION SELECT DISTINCT author_id FROM reaction"s : ""s) + (d_database.containsTable("story_sends") ? " UNION SELECT DISTINCT recipient_id FROM story_sends"s : ""s) + (d_database.containsTable("distribution_list_member") ? " UNION SELECT DISTINCT recipient_id FROM distribution_list_member"s : ""s) + referenced_recipients_query + " UNION SELECT DISTINCT " + d_thread_recipient_id + " FROM thread) RETURNING _id"s + //",COALESCE(NULLIF(" + d_recipient_system_joined_name + ", ''), NULLIF(profile_joined_name, ''), NULLIF(" + d_recipient_profile_given_name + ", ''), NULLIF(recipient." + d_recipient_e164 + ", ''), NULLIF(recipient." + d_recipient_aci + ", ''), recipient._id) AS 'display_name'," + d_recipient_e164 + (d_database.containsTable("distribution_list") ? ",distribution_list_id"s : ""s), &deleted_recipients, d_verbose); if (deleted_recipients.rows()) { //deleted_recipients.prettyPrint(); Logger::message(" Deleted ", deleted_recipients.rows(), " unreferenced recipients"); // if story recipients were deleted, delete corresponding distribution_list if (d_database.containsTable("distribution_list")) { int count = 0; for (unsigned int i = 0; i < deleted_recipients.rows(); ++i) { if (!deleted_recipients.isNull(i, "distribution_list_id")) { d_database.exec("DELETE FROM distribution_list WHERE _id = ?", deleted_recipients.getValueAs(i, "distribution_list_id")); count += d_database.changed(); } } if (count) Logger::message(" Deleted ", count, " unneeded distribution_lists"); // clean up the member table d_database.exec("DELETE FROM distribution_list_member WHERE list_id NOT IN (SELECT DISTINCT _id FROM distribution_list)"); } // delete name_collision_memberships with non-existing recipients? // UNTESTED if (d_database.containsTable("name_collision")) { d_database.exec("DELETE FROM name_collision_membership WHERE recipient_id NOT IN (SELECT _id FROM recipient)"); // delete corresponding name_collisions d_database.exec("DELETE FROM name_collision WHERE _id NOT IN (SELECT collision_id FROM name_collision_membership)"); } } } if (d_database.containsTable("notification_profile_allowed_members")) { Logger::message(" Deleting unneeded notification profiles entries..."); // delete from notification profile where recipient no longer in database d_database.exec("DELETE FROM notification_profile_allowed_members WHERE recipient_id NOT IN (SELECT DISTINCT _id FROM recipient)"); } if (d_database.containsTable("pending_pni_signature_message")) { Logger::message(" Deleting pending_pni_signature_messages not belonging to existing recipients..."); d_database.exec("DELETE FROM pending_pni_signature_message WHERE recipient_id NOT IN (SELECT DISTINCT _id FROM recipient)"); } //runSimpleQuery((d_databaseversion < 27) ? "SELECT _id, recipient_ids, system_display_name FROM recipient_preferences" : "SELECT _id, COALESCE(system_display_name,group_id,signal_profile_name) FROM recipient"); // remove avatars not belonging to existing recipients Logger::message(" Deleting unused avatars..."); SqliteDB::QueryResults results; if (d_databaseversion < 24) d_database.exec("SELECT recipient_ids FROM recipient_preferences", &results); else if (d_databaseversion < 33) // 'recipient_preferences' does not exist anymore, but d_avatars are still linked to "+316xxxxxxxx" strings d_database.exec("SELECT COALESCE(phone,group_id) FROM recipient", &results); else d_database.exec("SELECT _id FROM recipient", &results); // NOTE! _id is not a string! bool erased = true; while (erased) { erased = false; for (std::vector>>::iterator avit = d_avatars.begin(); avit != d_avatars.end(); ++avit) if ((d_databaseversion < 33) ? !results.contains(avit->first) : !results.contains(bepaald::toNumber(avit->first))) // avit first == "+316xxxxxxxx" on d_database < 33, recipient._id if > 33; { avit = d_avatars.erase(avit); erased = true; break; } //else // ++avit; } cleanAttachments(); Logger::message(" Delete others from 'identities'"); if (d_databaseversion < 24) d_database.exec("DELETE FROM identities WHERE address NOT IN (SELECT DISTINCT recipient_ids FROM recipient_preferences)"); else d_database.exec("DELETE FROM identities WHERE address NOT IN (SELECT DISTINCT _id FROM recipient)"); Logger::message(" Deleting group_receipts entries from deleted messages..."); d_database.exec("DELETE FROM group_receipts WHERE mms_id NOT IN (SELECT DISTINCT _id FROM " + d_mms_table + ")"); Logger::message(" Deleting group_receipts from non-existing recipients"); if (d_databaseversion < 24) d_database.exec("DELETE FROM group_receipts WHERE address NOT IN (SELECT DISTINCT recipient_ids FROM recipient_preferences)"); else d_database.exec("DELETE FROM group_receipts WHERE address NOT IN (SELECT DISTINCT _id FROM recipient)"); Logger::message(" Deleting drafts from deleted threads..."); d_database.exec("DELETE FROM drafts WHERE "s + (d_database.containsTable("sms") ? "thread_id NOT IN (SELECT DISTINCT thread_id FROM sms) AND " : "") + "thread_id NOT IN (SELECT DISTINCT thread_id FROM " + d_mms_table + ")"); if (d_database.containsTable("remapped_recipients")) { Logger::message(" Deleting remapped recipients for non existing recipients"); //d_database.exec("DELETE FROM remapped_recipients WHERE old_id NOT IN (SELECT DISTINCT _id FROM recipient) AND new_id NOT IN (SELECT DISTINCT _id FROM recipient)"); d_database.exec("DELETE FROM remapped_recipients WHERE new_id NOT IN (SELECT DISTINCT _id FROM recipient)"); } if (d_database.containsTable("chat_folder_membership")) { Logger::message_start(" Deleting non-existent chat_folder_memberships"); d_database.exec("DELETE FROM chat_folder_membership WHERE thread_id NOT IN (SELECT _id FROM thread)"); Logger::message_end(" (", d_database.changed(), ")"); } if (d_database.containsTable("chat_folder")) { Logger::message_start(" Deleting empty chat_folders"); d_database.exec("DELETE FROM chat_folder WHERE name IS NOT NULL AND _id NOT IN (SELECT chat_folder_id FROM chat_folder_membership)"); Logger::message_end(" (", d_database.changed(), ")"); } Logger::message(" Vacuuming database"); d_database.exec("VACUUM"); d_database.freeMemory(); // maybe remap recipients? //runSimpleQuery("SELECT _id, recipient_ids, system_display_name FROM recipient_preferences"); // #warning REMOVE ME // d_database.prettyPrint(d_truncate, "SELECT _id FROM recipient"); // d_database.prettyPrint(d_truncate, "SELECT * FROM distribution_list"); // d_database.prettyPrint(d_truncate, "SELECT * FROM distribution_list_member"); //d_database.exec("DELETE FROM recipient WHERE _id = 8"); } signalbackup-tools-20250313-1/signalbackup/compactids.cc000066400000000000000000000115051476450434500230300ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::compactIds(std::string const &table, std::string const &col) { //Logger::message(__FUNCTION__); if (d_database.getSingleResultAs("SELECT COUNT(*) FROM " + table, -1) == 0) // table is empty return; Logger::message(" Compacting table: ", table, " (", col, ")"); SqliteDB::QueryResults results; // d_database.exec("SELECT " + col + " FROM " + table, &results); // results.prettyPrint(); // gets first available _id in table d_database.exec("SELECT t1." + col + "+1 FROM " + table + " t1 LEFT OUTER JOIN " + table + " t2 ON t2." + col + "=t1." + col + "+1 WHERE t2." + col + " IS NULL AND t1." + col + " > 0 ORDER BY t1." + col + " LIMIT 1", &results); while (results.rows() > 0 && results.valueHasType(0, 0)) { long long int nid = results.getValueAs(0, 0); d_database.exec("SELECT MIN(" + col + ") FROM " + table + " WHERE " + col + " > ?", nid, &results); if (results.rows() == 0 || !results.valueHasType(0, 0)) break; long long int valuetochange = results.getValueAs(0, 0); //std::cout << "Changing _id : " << valuetochange << " -> " << nid << std::endl; d_database.exec("UPDATE " + table + " SET " + col + " = ? WHERE " + col + " = ?", {nid, valuetochange}); if (col == "_id") [[likely]] { for (auto const &dbl : s_databaselinks) { if (dbl.flags & SKIP) continue; if (!d_database.containsTable(dbl.table)) [[unlikely]] continue; if (table == dbl.table) { for (auto const &c : dbl.connections) { if (d_databaseversion >= c.mindbvversion && d_databaseversion <= c.maxdbvversion && d_database.containsTable(c.table) && d_database.tableContainsColumn(c.table, c.column)) { if (!c.json_path.empty()) { if (!d_database.exec("UPDATE " + c.table + " SET " + c.column + " = json_replace(" + c.column + ", " + c.json_path + ", ?) " "WHERE json_extract(" + c.column + ", " + c.json_path + ") = ?", {nid, valuetochange})) Logger::error("Compacting table '", table, "'"); } else if (!d_database.exec("UPDATE " + c.table + " SET " + c.column + " = ? WHERE " + c.column + " = ?" + (c.whereclause.empty() ? "" : " AND " + c.whereclause), {nid, valuetochange})) Logger::error("Compacting table '", table, "'"); } } } } if (table == d_part_table) { for (auto att = d_attachments.begin(); att != d_attachments.end(); ) { if (reinterpret_cast(att->second.get())->rowId() == static_cast(valuetochange)) { AttachmentFrame *af = reinterpret_cast(att->second.release()); att = d_attachments.erase(att); af->setRowId(nid); int64_t uniqueid = af->attachmentId(); if (uniqueid == 0) uniqueid = -1; d_attachments.emplace(std::make_pair(af->rowId(), uniqueid), af); } else ++att; } } else if (table == "sticker") { for (auto s = d_stickers.begin(); s != d_stickers.end(); ) { if (reinterpret_cast(s->second.get())->rowId() == static_cast(valuetochange)) { StickerFrame *sf = reinterpret_cast(s->second.release()); s = d_stickers.erase(s); sf->setRowId(nid); d_stickers.emplace(std::make_pair(sf->rowId(), sf)); } else ++s; } } } // gets first available _id in table d_database.exec("SELECT t1." + col + "+1 FROM " + table + " t1 LEFT OUTER JOIN " + table + " t2 ON t2." + col + "=t1." + col + "+1 WHERE t2." + col + " IS NULL AND t1." + col + " > 0 ORDER BY t1." + col + " LIMIT 1", &results); } // d_database.exec("SELECT _id FROM " + table, &results); // results.prettyPrint(); } signalbackup-tools-20250313-1/signalbackup/croptodates.cc000066400000000000000000000055751476450434500232430ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::cropToDates(std::vector> const &dateranges) { Logger::message(__FUNCTION__); std::string smsq; std::string mmsq; std::string megaphoneq; std::vector params; std::vector params2; for (unsigned int i = 0; i < dateranges.size(); ++i) { bool needrounding = false; long long int startrange = dateToMSecsSinceEpoch(dateranges[i].first); long long int endrange = dateToMSecsSinceEpoch(dateranges[i].second, &needrounding); if (startrange == -1 || endrange == -1 || endrange < startrange) { Logger::warning("Skipping range: '", dateranges[i].first, " - ", dateranges[i].second, "'. Failed to parse or invalid range."); continue; } Logger::message(" Using range: ", dateranges[i].first, " - ", dateranges[i].second, "\n", " ", startrange, " - ", endrange); if (needrounding)// if called with "YYYY-MM-DD HH:MM:SS" endrange += 999; // to get everything in the second specified... if (i == 0) { smsq = "DELETE FROM sms WHERE "; mmsq = "DELETE FROM " + d_mms_table + " WHERE "; megaphoneq = "DELETE FROM megaphone WHERE "; } else { smsq += "AND "; mmsq += "AND "; megaphoneq += "AND "; } smsq += d_sms_date_received + " NOT BETWEEN ? AND ?"; mmsq += "date_received NOT BETWEEN ? AND ?"; megaphoneq += "first_visible >= ?"; if (i < dateranges.size() - 1) { smsq += " "; mmsq += " "; megaphoneq += " "; } params.emplace_back(startrange); params.emplace_back(endrange); params2.emplace_back(endrange / 10); // the timestamp in megaphone is not in msecs (one digit less) } if (smsq.empty() || mmsq.empty()) { Logger::error("Failed to get any date ranges."); return; } if (d_database.containsTable("sms")) d_database.exec(smsq, params); d_database.exec(mmsq, params); if (d_database.containsTable("megaphone")) { d_database.exec(megaphoneq, params2); //std::cout << "changed: " << d_database.changed() << std::endl; } cleanDatabaseByMessages(); } signalbackup-tools-20250313-1/signalbackup/croptothread.cc000066400000000000000000000037031476450434500234010ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::cropToThread(long long int threadid) { cropToThread(std::vector{threadid}); } void SignalBackup::cropToThread(std::vector const &threadids) { Logger::message(__FUNCTION__); std::string smsq; std::string mmsq; std::vector tids; for (unsigned int i = 0; i < threadids.size(); ++i) { if (i == 0) { smsq = "DELETE FROM sms WHERE "; mmsq = "DELETE FROM " + d_mms_table + " WHERE "; } else { smsq += "AND "; mmsq += "AND "; } smsq += "thread_id != ?"; mmsq += "thread_id != ?"; if (i < threadids.size() - 1) { smsq += " "; mmsq += " "; } tids.emplace_back(threadids[i]); } if (smsq.empty() || mmsq.empty() || tids.empty()) { Logger::error("building crop-to-thread statement resulted in invalid statement"); return; } if (d_database.containsTable("sms")) { Logger::message(" Deleting messages not belonging to requested thread(s) from 'sms'"); d_database.exec(smsq, tids); } Logger::message(" Deleting messages not belonging to requested thread(s) from 'mms'"); d_database.exec(mmsq, tids); cleanDatabaseByMessages(); } signalbackup-tools-20250313-1/signalbackup/customs.cc000066400000000000000000002054241476450434500224040ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ /* NOTE: THESE ARE CUSTOM FUNCTIONS AND WILL BE REMOVED WITHOUT NOTIFICATION IN THE NEAR FUTURE */ #include "signalbackup.ih" // #include // #include "../sqlcipherdecryptor/sqlcipherdecryptor.h" /* move messages froma given thread to the note-to-self thread, adjusting recipienst */ bool SignalBackup::arc(long long int tid, std::string const &selfphone) { // check and warn about selfid & note-to-self thread long long int note_to_self_thread_id = -1; d_selfid = selfphone.empty() ? scanSelf() : d_database.getSingleResultAs("SELECT _id FROM recipient WHERE " + d_recipient_e164 + " = ?", selfphone, -1); if (d_selfid == -1) { if (!selfphone.empty()) Logger::error("Failed to determine id of 'self'."); else // if (selfphone.empty()) Logger::error("Failed to determine Note-to-self thread. Consider passing `--setselfid \"[phone]\"' to set it manually"); return false; } else note_to_self_thread_id = d_database.getSingleResultAs("SELECT _id FROM thread WHERE " + d_thread_recipient_id + " = ?", d_selfid, -1); Logger::message("Using self-id: ", d_selfid, ", with thread ", note_to_self_thread_id); Logger::message("Initially in note-to-self-thread:"); d_database.prettyPrint(d_truncate, "SELECT DISTINCT thread_id, from_recipient_id, to_recipient_id, COUNT(*) AS nmessages FROM message " "WHERE thread_id = ? GROUP BY thread_id, from_recipient_id, to_recipient_id", note_to_self_thread_id); Logger::message("Initially in thread ", tid, ":"); d_database.prettyPrint(d_truncate, "SELECT DISTINCT thread_id, from_recipient_id, to_recipient_id, COUNT(*) AS nmessages FROM message " "WHERE thread_id = ? GROUP BY thread_id, from_recipient_id, to_recipient_id", tid); // check if any of the date_sents in tid match any of those in nts-thread long long int doublemsgs = d_database.getSingleResultAs("SELECT COUNT(*) FROM message WHERE thread_id = ? AND date_sent IN (SELECT date_sent FROM message WHERE thread_id = ?)", {tid, note_to_self_thread_id}, -1); if (doublemsgs == -1) Logger::warning("Failed to check timestamps shared between threads."); else if (doublemsgs > 0) Logger::warning("Found duplicate timestamps between threads. Expecting at least ", doublemsgs, " will fail to import..."); else // doublemsg == 0 Logger::message("Found no duplicate timestamps between thread."); long long int non_outgoing_messages = d_database.getSingleResultAs("SELECT COUNT(*) FROM message WHERE thread_id = ? AND (type & 0x1f) != 23", tid, 1); if (non_outgoing_messages > 0) { Logger::warning("Found ", non_outgoing_messages, " messages in thread ", tid, " that have a type not normally found in note-to-self"); Logger::warning_indent("threads (for example, incoming message, calls, or profile changes). Summary of"); Logger::warning_indent("message types found:"); d_database.prettyPrint(d_truncate, "SELECT DISTINCT type, (type & 0x1f) FROM message WHERE thread_id = ? ORDER BY (type & 0x1f)", tid); } SqliteDB::QueryResults message_ids; if (!d_database.exec("SELECT _id FROM message WHERE thread_id = ?", tid, &message_ids)) return false; Logger::message("Attempting move of ", message_ids.rows(), " messages..."); long long int moved = 0; for (unsigned int i = 0; i < message_ids.rows(); ++i) { //Logger::message(message_ids.valueAsInt(i, "_id", -1)); if (!d_database.exec("UPDATE message SET from_recipient_id = ?, to_recipient_id = ?, thread_id = ? WHERE _id = ?", {d_selfid, d_selfid, note_to_self_thread_id, message_ids.value(i, "_id")})) { Logger::warning("Failed to move message id: ", message_ids.valueAsInt(i, "_id", -1)); Logger::warning_indent("Some info on this message:"); d_database.printLineMode("SELECT _id, date_sent, date_received, date_server, from_recipient_id, to_recipient_id, type, read, m_type, receipt_timestamp, has_delivery_receipt, has_read_receipt, viewed, mismatched_identities, network_failures, expires_in, expire_started, notified, quote_id, quote_author, quote_missing, quote_body, quote_mentions, quote_type, shared_contacts, unidentified, link_previews ,view_once, reactions_unread, reactions_last_seen, remote_deleted, mentions_self, notified_timestamp, server_guid, message_ranges, story_type, parent_story_id, message_extras, latest_revision_id, original_message_id, revision_number FROM message WHERE _id = ?", message_ids.value(i, "_id")); } else { ++moved; } } Logger::message("Moved ", moved, " messages to note-to-self thread"); // if old thread empty -> mark inactive if (d_database.getSingleResultAs("SELECT COUNT(*) FROM message WHERE thread_id = ?", tid, -1) == 0) { Logger::message("Old thread is empty, marking as inactive"); if (!d_database.exec("UPDATE thread SET meaningful_messages = 0, active = 0 WHERE _id = ?", tid)) Logger::error("Failed to update old thread status"); } // mark new thread as active if (moved) if (!d_database.exec("UPDATE thread SET meaningful_messages = 1, active = 1 WHERE _id = ?", note_to_self_thread_id)) Logger::error("Failed to update note-to-self thread"); Logger::message("Finally in note-to-self-thread:"); d_database.prettyPrint(d_truncate, "SELECT DISTINCT thread_id, from_recipient_id, to_recipient_id, COUNT(*) AS nmessages FROM message " "WHERE thread_id = ? GROUP BY thread_id, from_recipient_id, to_recipient_id", note_to_self_thread_id); Logger::message("Finally in thread ", tid, ":"); d_database.prettyPrint(d_truncate, "SELECT DISTINCT thread_id, from_recipient_id, to_recipient_id, COUNT(*) AS nmessages FROM message " "WHERE thread_id = ? GROUP BY thread_id, from_recipient_id, to_recipient_id", tid); return true; } /* alter a version 214 database so it is compatiible enough with 215 to be imported into a 215 db as source */ bool SignalBackup::custom_hugogithubs() { if (d_databaseversion != 214) { Logger::error("Database version: ", d_databaseversion, " (needs to be 214)"); return false; } // alter part table if (!d_database.exec("ALTER TABLE part RENAME COLUMN mid TO message_id") || !d_database.exec("ALTER TABLE part RENAME COLUMN ct TO content_type") || !d_database.exec("ALTER TABLE part RENAME COLUMN cd TO remote_key") || !d_database.exec("ALTER TABLE part RENAME COLUMN cl TO remote_location") || !d_database.exec("ALTER TABLE part RENAME COLUMN digest TO remote_digest") || !d_database.exec("ALTER TABLE part RENAME COLUMN incremental_mac_digest TO remote_incremental_digest") || !d_database.exec("ALTER TABLE part RENAME COLUMN incremental_mac_chunk_size TO remote_incremental_digest_chunk_size") || !d_database.exec("ALTER TABLE part RENAME COLUMN pending_push TO transfer_state") || !d_database.exec("ALTER TABLE part RENAME COLUMN _data TO data_file") || !d_database.exec("ALTER TABLE part DROP COLUMN unique_id") || !d_database.exec("ALTER TABLE part RENAME TO attachment")) { Logger::error("Altering part table"); return false; } d_part_table = "attachment"; d_part_mid = "message_id"; // dbv 215 d_part_ct = "content_type"; // dbv 215 d_part_pending = "transfer_state"; // dbv 215 d_part_cd = "remote_key"; // dbv 215 d_part_cl = "remote_location"; // dbv 215 // remove unqiue ids from AttachmentFrames std::map, DeepCopyingUniquePtr> d_new_attachments; for (auto const &a : d_attachments) { AttachmentFrame const *af = a.second.get(); // std::cout << "OLD FRAME:" << std::endl; // std::cout << a.first.first << " " << a.first.second << std::endl; // af->printInfo(); std::istringstream old_attachment_frame(af->getHumanData()); std::vector new_attachment_frame_strings; std::string line; while (std::getline(old_attachment_frame, line)) { if (STRING_STARTS_WITH(line, "ATTACHMENTID")) // = uniqueid continue; new_attachment_frame_strings.emplace_back(line); } uint64_t rowid = a.first.first; DeepCopyingUniquePtr new_attachment_frame; // new_attachment_frame->printInfo(); if (!setFrameFromStrings(&new_attachment_frame, new_attachment_frame_strings)) { Logger::error("Failed to create new attachmentframe"); return false; } // new_attachment_frame->setLazyData(af->iv(), af->iv_size(), // af->mackey(), af->mackey_size(), // af->cipherkey(), af->cipherkey_size(), // af->length(), af->filename(), af->filepos()); new_attachment_frame->setReader(af->reader()->clone()); // UNTESTED! d_new_attachments.emplace(std::make_pair(rowid, -1), new_attachment_frame.release()); // auto const &rit = d_new_attachments.rbegin(); // std::cout << rit->first.first << " " << rit->first.second << std::endl; // rit->second->printInfo(); } d_attachments = d_new_attachments; // adjust DatabaseVersionFrame DeepCopyingUniquePtr d_new_dbvframe; if (!setFrameFromStrings(&d_new_dbvframe, std::vector{"VERSION:uint32:215"})) { Logger::error("Failed to create new databaseversionframe"); return false; } d_databaseversionframe.reset(d_new_dbvframe.release()); //d_databaseversionframe->printInfo(); Logger::message(Logger::Control::BOLD, "BACKUP UPDATED TO SEMI-215. IT SHOULD ONLY BE USED AS A SOURCE FOR IMPORTING INTO A v215 BACKUP!", Logger::Control::NORMAL); return true; } // bool SignalBackup::carowit(std::string const &sourcefile, std::string const &sourcepw) const // { // SignalBackup source(sourcefile, sourcepw, false, true, false); // if (!source.ok()) // { // std::cout << "Failed to open source" << std::endl; // return false; // } // SqliteDB::QueryResults r; // source.d_database.exec("SELECT _id, uuid, phone, group_id, COALESCE(uuid, phone, group_id) AS coalesce FROM recipient " // "WHERE _id IS (SELECT " + source.d_thread_recipient_id + " FROM thread WHERE _id IS ?)", 31, &r); // r.prettyPrint(); // d_database.prettyPrint("SELECT _id, uuid, phone, group_id, COALESCE(uuid, phone, group_id) AS coalesce FROM recipient " // "WHERE phone IS ?", r.value(0, "phone")); // return true; // } // bool SignalBackup::hhenkel(std::string const &signaldesktoplocation) // { // /* // DEPRECATED // */ // using namespace std::string_literals; // if (d_databaseversion >= 168) // return false; // // args // // open signal desktop database // SqlCipherDecryptor db(signaldesktoplocation, signaldesktoplocation + "/sql", 4 /* desktop sqlcipher version */); // if (!db.ok()) // { // std::cout << "Error reading signal desktop database" << std::endl; // return false; // } // auto [data, size] = db.data(); // std::pair tmp = std::make_pair(data, size); // SqliteDB desktopdb(&tmp); // //SqliteDB::QueryResults r; // //desktopdb.exec("SELECT hex(groupid) FROM conversations", &r); // //r.prettyPrint(); // // all threads in messages // SqliteDB::QueryResults list_of_threads; // d_database.exec("SELECT DISTINCT " + d_mms_table + ".thread_id FROM " + d_mms_table + (d_database.containsTable("sms") ? " UNION SELECT DISTINCT sms.thread_id FROM sms" : ""), &list_of_threads); // list_of_threads.prettyPrint(); // std::cout << std::endl; // // for each thread, find a corresponding conversation in desktop-db // std::vector> matches; // thread_id, recipient_id/address, -> desktop.converationId // for (long long int i = 0; i < static_cast(list_of_threads.rows()); ++i) // { // SqliteDB::QueryResults message_data; // d_database.exec("SELECT body," + d_mms_date_sent + "," + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE thread_id = ?", // list_of_threads.value(i, 0), &message_data); // //message_data.prettyPrint(); // bool matched = false; // for (unsigned int j = 0; j < message_data.rows(); ++j) // { // SqliteDB::QueryResults r2; // desktopdb.exec("SELECT conversationId FROM messages WHERE sent_at == ? AND body == ?", {message_data.value(j, d_mms_date_sent), message_data.value(j, "body")}, &r2); // if (r2.rows() == 1) // { // matched = true; // matches.emplace_back(std::make_tuple(list_of_threads.getValueAs(i, 0), message_data.valueAsString(j, d_mms_recipient_id), r2.getValueAs(0, "conversationId"))); // } // if (matched) // break; // } // if (!matched) // { // d_database.exec("SELECT body,date_sent," + d_sms_recipient_id + " FROM sms WHERE thread_id = ?", // list_of_threads.value(i, 0), &message_data); // //message_data.prettyPrint(); // for (unsigned int j = 0; j < static_cast(message_data.rows()); ++j) // { // SqliteDB::QueryResults r2; // desktopdb.exec("SELECT conversationId FROM messages WHERE sent_at == ? AND body == ?", {message_data.value(j, "date_sent"), message_data.value(j, "body")}, &r2); // if (r2.rows() == 1) // { // matched = true; // matches.emplace_back(std::make_tuple(list_of_threads.getValueAs(i, 0), message_data.valueAsString(j, d_sms_recipient_id), // r2.getValueAs(0, "conversationId"))); // } // if (matched) // break; // } // } // if (!matched) // { // std::cout << " - Failed to match thread " << list_of_threads.valueAsString(i, 0) << " to any conversation in Signal Desktop database" << std::endl; // std::cout << " Last 10 messages from this thread:" << std::endl; // // todo // std::string q = // "SELECT " // "sms." + d_sms_date_received + " AS union_date, " // "sms.date_sent AS union_display_date, " // "sms.type AS union_type, " // "sms.body AS union_body " // "FROM sms WHERE " + list_of_threads.valueAsString(i, 0) + " = sms.thread_id " // "UNION " // "SELECT " + // d_mms_table + ".date_received AS union_date, " + // not sure for outgoing // d_mms_table + "." + d_mms_date_sent + " AS union_display_date, " + // d_mms_table + "." + d_mms_type + " AS union_type, " + // d_mms_table + ".body AS union_body " // "FROM " + d_mms_table + " WHERE " + list_of_threads.valueAsString(i, 0) + " = " + d_mms_table + ".thread_id " // "ORDER BY union_date DESC, union_display_date ASC LIMIT 10"; // d_database.prettyPrint(q); // std::cout << std::endl; // } // } // /* // mms: desktop.messages.sent_at == android.mms.date // sms: desktop.messages.sent_at == android.sms.date_sent // */ // // desktop messages: // // SELECT id,unread,expires_at,sent_at,schemaVersion,conversationId,received_at,source,sourceDevice,hasAttachments,hasFileAttachments,hasVisualMediaAttachments,expireTimer,expirationStartTimestamp,type,body,messageTimer,messageTimerStart,messageTimerExpiresAt,isErased,isViewOnce,sourceUuid FROM messages WHERE body == "Test terug"; // // id|unread|expires_at|sent_at|schemaVersion|conversationId|received_at|source|sourceDevice|hasAttachments|hasFileAttachments|hasVisualMediaAttachments|expireTimer|expirationStartTimestamp|type|body|messageTimer|messageTimerStart|messageTimerExpiresAt|isErased|isViewOnce|sourceUuid // // 13c39ffe-67e2-43a5-911b-9537e485b75d|||1597327208579|10|82184df5-c89b-4c5c-a0c6-b7c9449b3818|1598179687083|31683616099|1|0|||||incoming|Test terug|||||0|6b7e6b80-c3d9-4701-8f36-bf5e22ebd62c // d_database.exec("DELETE FROM constraint_spec"); // d_database.exec("DELETE FROM megaphone"); // d_database.exec("DELETE FROM dependency_spec"); // d_database.exec("DELETE FROM push"); // d_database.exec("DELETE FROM drafts"); // d_database.exec("DELETE FROM " + d_mms_table + "_fts"); // d_database.exec("DELETE FROM recipient"); // d_database.exec("DELETE FROM group_receipts"); // d_database.exec("DELETE FROM " + d_mms_table + "_fts_config"); // d_database.exec("DELETE FROM sessions"); // d_database.exec("DELETE FROM sticker"); // d_database.exec("DELETE FROM groups"); // d_database.exec("DELETE FROM " + d_mms_table + "_fts_data"); // d_database.exec("DELETE FROM signed_prekeys"); // d_database.exec("DELETE FROM storage_key"); // d_database.exec("DELETE FROM identities"); // d_database.exec("DELETE FROM " + d_mms_table + "_fts_docsize"); // d_database.exec("DELETE FROM thread"); // d_database.exec("DELETE FROM job_spec"); // d_database.exec("DELETE FROM " + d_mms_table + "_fts_idx"); // d_database.exec("DELETE FROM sms_fts"); // d_database.exec("DELETE FROM key_value"); // d_database.exec("DELETE FROM one_time_prekeys"); // if (d_database.containsTable("sms")) // { // d_database.exec("DELETE FROM sms_fts_config"); // d_database.exec("DELETE FROM sms_fts_data"); // d_database.exec("DELETE FROM sms_fts_docsize"); // d_database.exec("DELETE FROM sms_fts_idx"); // } // /* // recipient._id := [s|m]ms.address // thread._id := [s|m]ms.thread_id // thread.recipient_ids/thread.thread_recipient_id := [s|m]ms.address // recipient.uuid := conversations.uuid // .phone := conversations.e164 // .group_id := decode(conversations.groupId) // .system_display_name := IF (!group) conversations.name ELSE NULL // .signal_profile_name := conversations.profileName // .profile_family_name := .profileFamilyName // .progile_joined_name := .profileFullName // IF GROUP // group.group_id := recipient.group_id ( == conversations.groupId) // .title := conversations.name // .members := CONVERT(conversations.members FROM conversation.id -> recipient._id) // .recipient_ids := [s|m]ms.recipient_ids // */ // // SKIP GROUPS FOR SECOND PASS // for (unsigned int t = 0; t < matches.size(); ++t) // { // SqliteDB::QueryResults r; // desktopdb.exec("SELECT id,uuid,e164,groupId,type,name,profileName,profileFamilyName,profileFullName FROM conversations WHERE id == ?", std::get<2>(matches[t]), &r); // if (r.valueAsString(0, "type") == "group") // continue; // std::cout << " - Got match for thread " << std::get<0>(matches[t]) << std::endl; // r.prettyPrint(); // std::cout << std::endl; // // add entry in 'thread' database // //std::cout << "INSERTING : " << std::get<0>(matches[t]) << " " << std::get<1>(matches[t]) << std::endl; // d_database.exec("INSERT INTO thread (_id, " + d_thread_recipient_id + ") VALUES (?, ?)", {std::get<0>(matches[t]), std::get<1>(matches[t])}); // // add entry in 'recipient' database // d_database.exec("INSERT INTO recipient (_id, uuid, phone, group_id, system_display_name, signal_profile_name, profile_family_name, profile_joined_name) " // "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", // {std::get<1>(matches[t]), r.value(0, "uuid"), r.value(0, "e164"), r.value(0, "groupId"), // r.value(0, "name"), r.value(0, "profileName"), r.value(0, "profileFamilyName"), r.value(0, "profileFullName")}); // } // // GROUPS // for (unsigned int t = 0; t < matches.size(); ++t) // { // SqliteDB::QueryResults r; // desktopdb.exec("SELECT id,uuid,e164,groupId,type,name,profileName,profileFamilyName,profileFullName FROM conversations WHERE id == ?", std::get<2>(matches[t]), &r); // if (r.valueAsString(0, "type") != "group") // continue; // std::cout << " - Got match for thread " << std::get<0>(matches[t]) << std::endl; // r.prettyPrint(); // std::cout << std::endl; // // NOTE !!!! // // recipient_id (address) COULD BE WRONG! IT ONLY REPRESENTS THE GROUPS ID ON OUTGOING MESSAGES! (& outgoing message are always in mms table (nothing in sms)) // // on incoming message it is the recip_id of the specific group member sending the message // SqliteDB::QueryResults group_rec; // std::string group_recipient_id; // could be string or int, depending on age of database // if (!d_database.exec("SELECT DISTINCT " + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE (" + d_mms_type + " & " + bepaald::toString(Types::BASE_TYPE_MASK) + // ") BETWEEN " + bepaald::toString(Types::BASE_OUTBOX_TYPE) + " AND " + // bepaald::toString(Types::BASE_PENDING_INSECURE_SMS_FALLBACK) + // " AND thread_id == ?", std::get<0>(matches[t]), &group_rec)) // { // std::cout << "ERROR" << std::endl; // break; // } // if (group_rec.rows() == 0) // { // std::cout << "WARNING : No outgoing messages found in this group, this is unusual but I'm guessing i can just use any unused recipient_id" << std::endl; // SqliteDB::QueryResults list_of_addresses; // d_database.exec("SELECT DISTINCT " + d_mms_table + "." + d_mms_recipient_id + " AS union_rec_id FROM " + d_mms_table + " " // "UNION SELECT DISTINCT sms." + d_sms_recipient_id + " AS union_rec_id FROM sms", &list_of_addresses); // // this is a stupid and naive way of looking for a free id, but it's quick to write :P // long long int free_address = 1; // bool done = false; // while (!done) // { // bool found = false; // for (unsigned int i = 0; i < list_of_addresses.rows(); ++i) // { // if (list_of_addresses.valueAsString(i, "union_rec_id") == bepaald::toString(free_address)) // { // ++free_address; // found = true; // break; // } // } // if (!found) // done = true; // } // //std::cout << "Got free address : " << free_address << std::endl; // group_recipient_id = bepaald::toString(free_address); // } // else if (group_rec.rows() > 1) // { // std::cout << "Unexpectedly got multiple group ids.... this shouldn't happen. skipping" << std::endl; // continue; // } // else // group_recipient_id = group_rec.valueAsString(0, d_mms_recipient_id); // // add entry in 'thread' database // d_database.exec("INSERT INTO thread (_id, " + d_thread_recipient_id + ") VALUES (?, ?)", {std::get<0>(matches[t]), group_recipient_id}); // // add entry in 'recipient' database (Skip 'name' for groups, in desktop it is group name, in app it's NULL // d_database.exec("INSERT INTO recipient (_id, uuid, phone, group_id, signal_profile_name, profile_family_name, profile_joined_name) " // "VALUES (?, ?, ?, ?, ?, ?, ?)", // {group_recipient_id, r.value(0, "uuid"), r.value(0, "e164"), r.value(0, "groupId"), // r.value(0, "profileName"), r.value(0, "profileFamilyName"), r.value(0, "profileFullName")}); // // add group entry // std::string groupdatastr = r.valueAsString(0, "groupId"); // unsigned char const *groupdata = reinterpret_cast(groupdatastr.c_str()); // unsigned int groupdatasize = groupdatastr.size(); // std::stringstream decoded_group_id; // decoded_group_id << "__textsecure_group__!"; // unsigned int pos = 0; // while (pos < groupdatasize) // { // if ((static_cast(groupdata[pos]) & 0xff) <= 0x7f) // decoded_group_id << std::hex << std::setw(2) << std::setfill('0') << (groupdata[pos] & 0xff); // else if ((static_cast(groupdata[pos]) & 0xff) >= 0xc0) // { // decoded_group_id << std::hex << std::setw(2) << std::setfill('0') << ((((groupdata[pos] << 6) & 0b11000000) | (groupdata[pos + 1] & 0b00111111)) & 0xff); // ++pos; // } // ++pos; // } // std::string members; // d_database.exec("INSERT INTO groups (group_id, title, members, recipient_id) VALUES(?, ?, ?, ?)", {decoded_group_id.str(), r.value(0, "name"), members, std::get<1>(matches[t])}); // } // updateThreadsEntries(); // return true; // } // /* // SqliteDB::QueryResults t2; // if (d_database.exec("SELECT * FROM thread WHERE _id == 1", &t2)) // t2.print(); // if (d_database.exec("SELECT * FROM recipient WHERE _id == 2", &t2)) // t2.print(); // if (desktopdb.exec("SELECT id,active_at,type,members,name,profileName,profileFamilyName,profileFullName,e164,uuid,groupId FROM conversations WHERE id == \"4bfad53f-540c-4a54-be37-ce8eb7bc3440\"", &t2)) // t2.print(); // std::cout << "" << std::endl; // if (d_database.exec("SELECT * FROM thread WHERE _id == 9", &t2)) // t2.print(); // if (d_database.exec("SELECT * FROM recipient WHERE _id == 1", &t2)) // t2.print(); // if (desktopdb.exec("SELECT id,active_at,type,members,name,profileName,profileFamilyName,profileFullName,e164,uuid,groupId FROM conversations WHERE id == \"e6e4d01d-5607-4749-8c95-cde384547a8c\"", &t2)) // t2.print(); // std::cout << "" << std::endl; // if (d_database.exec("SELECT * FROM thread WHERE _id == 13", &t2)) // t2.print(); // if (d_database.exec("SELECT * FROM recipient WHERE _id == 17", &t2)) // t2.print(); // if (d_database.exec("SELECT * FROM groups WHERE group_id == \"__textsecure_group__!7b8072dc2aa63a7e34dde2d0c5e315a0\"", &t2)) // t2.print(); // if (desktopdb.exec("SELECT id,active_at,type,members,name,profileName,profileFamilyName,profileFullName,e164,uuid,groupId FROM conversations WHERE id == \"90e6212f-01ca-473b-8001-36876fa50146\"", &t2)) // t2.print(); // std::cout << "" << std::endl; // if (d_database.exec("SELECT * FROM thread WHERE _id == 15", &t2)) // t2.print(); // if (d_database.exec("SELECT * FROM recipient WHERE _id == 6", &t2)) // t2.print(); // if (desktopdb.exec("SELECT id,active_at,type,members,name,profileName,profileFamilyName,profileFullName,e164,uuid,groupId FROM conversations WHERE id == \"b89aaadc-293f-4d67-a17a-8b51f04a4de1\"", &t2)) // t2.print(); // std::cout << "" << std::endl; // */ // /* // void SignalBackup::esokrates() // { // SqliteDB::QueryResults res; // d_database.exec("SELECT _id,body,address,date,type " // "FROM sms " // "WHERE (type & " + bepaald::toString(Types::SECURE_MESSAGE_BIT) + ") IS 0", &res); // std::cout << "Searching for possible duplicates of " << res.rows() << " unsecured messages" << std::endl; // std::vector ids_to_remove; // for (unsigned int i = 0; i < res.rows(); ++i) // { // long long int msgid = std::any_cast(res.value(i, "_id")); // std::string body; // bool body_is_null = false; // if (res.valueHasType(i, "body")) // body = std::any_cast(res.value(i, "body")); // else if (res.isNull(i, "body")) // body_is_null = true; // std::string address = std::any_cast(res.value(i, "address")); // long long int date = std::any_cast(res.value(i, "date")); // long long int type = std::any_cast(res.value(i, "type")); // SqliteDB::QueryResults res2; // if (body_is_null) // d_database.exec("SELECT _id " // "FROM sms " // "WHERE (type & " + bepaald::toString(Types::SECURE_MESSAGE_BIT) + ") IS NOT 0 " // "AND date = ? " // "AND body IS NULL " // "AND address = ?", {date, address}, &res2); // else // !body_is_null // d_database.exec("SELECT _id " // "FROM sms " // "WHERE (type & " + bepaald::toString(Types::SECURE_MESSAGE_BIT) + ") IS NOT 0 " // "AND date = ? " // "AND body = ? " // "AND address = ?", {date, body, address}, &res2); // if (res2.rows() > 1) // { // std::cout << "Unexpectedley got multiple results when searching for duplicates... ignoring" << std::endl; // continue; // } // else if (res2.rows() == 1) // { // std::time_t epoch = date / 1000; // std::cout << " * Found duplicate of message: " << std::endl // << " " << msgid << "|" << body << "|" << address << "|" // << std::put_time(std::localtime(&epoch), "%F %T %z") << "|" << "|" << type << std::endl // << " in 'sms' table. Marking for deletion." << std::endl; // ids_to_remove.push_back(msgid); // continue; // } // if (body_is_null) // d_database.exec("SELECT _id " // "FROM " + d_mms_table + " " // "WHERE (msg_box & " + bepaald::toString(Types::SECURE_MESSAGE_BIT) + ") IS NOT 0 " // "AND date = ? " // "AND body IS NULL " // "AND address = ?", {date, address}, &res2); // else // !body_is_null // d_database.exec("SELECT _id " // "FROM " + d_mms_table + " " // "WHERE (msg_box & " + bepaald::toString(Types::SECURE_MESSAGE_BIT) + ") IS NOT 0 " // "AND date = ? " // "AND body = ? " // "AND address = ?", {date, body, address}, &res2); // if (res2.rows() > 1) // { // std::cout << "Unexpectedley got multiple results when searching for duplicates... ignoring" << std::endl; // continue; // } // else if (res2.rows() == 1) // { // std::time_t epoch = date / 1000; // std::cout << " * Found duplicate of message: " << std::endl // << " " << msgid << "|" << body << "|" << address << "|" // << std::put_time(std::localtime(&epoch), "%F %T %z") << "|" << "|" << type << std::endl // << " in 'mms' table. Marking for deletion." << std::endl; // ids_to_remove.push_back(msgid); // continue; // } // } // std::string ids_to_remove_str; // for (unsigned int i = 0; i < ids_to_remove.size(); ++i) // ids_to_remove_str += bepaald::toString(ids_to_remove[i]) + ((i < ids_to_remove.size() - 1) ? "," : ""); // std::cout << std::endl << std::endl << "About to remove messages from 'sms' table with _id's = " << std::endl; // std::cout << ids_to_remove_str << std::endl << std::endl; // std::cout << "Deleting " << ids_to_remove.size() << " duplicates..." << std::endl; // d_database.exec("DELETE FROM sms WHERE _id IN (" + ids_to_remove_str + ")"); // std::cout << "Deleted " << d_database.changed() << " entries" << std::endl; // } // */ // bool SignalBackup::sleepyh34d(std::string const &truncatedbackup, std::string const &pwd) // { // // open truncated // std::cout << "Opening truncated backup..." << std::endl; // std::unique_ptr tf(new SignalBackup(truncatedbackup, pwd, false, false, false)); // if (!tf->ok()) // { // std::cout << "Failed to read truncated backup file" << std::endl; // return false; // } // std::cout << "Deleting sms/mms tables from complete backup" << std::endl; // if ((!d_database.containsTable("sms") || !d_database.exec("DELETE FROM sms")) || // !d_database.exec("DELETE FROM " + d_mms_table)/* || // !d_database.exec("DELETE FROM part")*/) // { // std::cout << "Error deleting contents of sms/mms tables" << std::endl; // return false; // } // //d_attachments.clear(); // // delete part entries from truncated which are already in target // std::cout << "Deleting doubled part entries..." << std::endl; // SqliteDB::QueryResults r; // d_database.exec("SELECT _id FROM part", &r); // std::string q = "DELETE FROM part WHERE _id IN ("; // for (unsigned int i = 0; i < r.rows(); ++i) // q += bepaald::toString(r.getValueAs(i, 0)) + ((i == r.rows() - 1) ? ")" : ","); // if (!tf->d_database.exec(q)) // { // std::cout << "Error deleting part entries" << std::endl; // return false; // } // // in truncated: remove part entries (and d_attachments[i]), that are // // missing or are already present in target (complete) db // std::cout << "Cleaning up part table/attachments..." << std::endl; // SqliteDB::QueryResults results; // tf->d_database.exec("SELECT _id,unique_id FROM part", &results); // std::vector> missingdata; // for (unsigned int i = 0; i < results.rows(); ++i) // { // uint64_t rowid = results.getValueAs(i, "_id"); // uint64_t uniqid = results.getValueAs(i, "unique_id"); // if (tf->d_attachments.find({rowid, uniqid}) == tf->d_attachments.end()/* || // d_attachments.find({rowid, uniqid}) != d_attachments.end()*/) // missingdata.emplace_back(std::make_pair(rowid, uniqid)); // } // for (auto const &a : missingdata) // { // if (!tf->d_database.exec("DELETE FROM part WHERE _id = ? AND unique_id = ?", {a.first, a.second})) // std::cout << "Warning failed to remove part entry with missing data" << std::endl; // } // std::vector tables{d_mms_table, "part"}; // if (d_database.containsTable("sms")) // tables.emplace_back("sms"); // for (std::string const &table : tables) // { // std::cout << "Importing " << table << " entries from truncated file "; // //SqliteDB::QueryResults results; // tf->d_database.exec("SELECT * FROM " + table, &results); // // check if tf (which is newer) contains columns not existing in target // unsigned int idx = 0; // while (idx < results.headers().size()) // { // if (!d_database.tableContainsColumn(table, results.headers()[idx])) // { // std::cout << " NOTE: Dropping column '" << table << "." << results.headers()[idx] << "' from truncated : Column not present in target database" << std::endl; // if (results.removeColumn(idx)) // continue; // } // // else // ++idx; // } // // import // std::cout << " " << results.rows() << " entries." << std::endl; // for (unsigned int i = 0; i < results.rows(); ++i) // { // SqlStatementFrame newframe = buildSqlStatementFrame(table, results.headers(), results.row(i)); // if (!d_database.exec(newframe.bindStatement(), newframe.parameters())) // std::cout << "Warning: Failed to import sqlstatement (" << table << ")" << std::endl; // } // } // // check for unreferenced threads // if (d_database.containsTable("sms")) // { // d_database.exec("SELECT DISTINCT thread_id FROM sms WHERE thread_id NOT IN (SELECT DISTINCT _id FROM thread)", &results); // if (results.rows() > 0) // { // std::cout << "WARNING:" << " Found messages in thread not present in old (complete) database... dropping them!" << std::endl; // d_database.exec("DELETE FROM sms WHERE thread_id NOT IN (SELECT DISTINCT _id FROM thread)"); // } // } // d_database.exec("SELECT DISTINCT thread_id FROM " + d_mms_table + " WHERE thread_id NOT IN (SELECT DISTINCT _id FROM thread)", &results); // if (results.rows() > 0) // { // std::cout << "WARNING:" << " Found messages in thread not present in old (complete) database... dropping them!" << std::endl; // d_database.exec("DELETE FROM " + d_mms_table + " WHERE thread_id NOT IN (SELECT DISTINCT _id FROM thread)"); // } // // check for unreferenced recipients // if (d_database.containsTable("sms")) // { // d_database.exec("SELECT DISTINCT " + d_sms_recipient_id + " FROM sms WHERE " + d_sms_recipient_id + " NOT IN (SELECT DISTINCT _id FROM recipient)", &results); // if (results.rows() > 0) // { // std::cout << "WARNING:" << " Found messages referencing recipient not present in old (complete) database... dropping them!" << std::endl; // d_database.exec("DELETE FROM sms WHERE " + d_sms_recipient_id + " NOT IN (SELECT DISTINCT _id FROM recipient)"); // } // } // d_database.exec("SELECT DISTINCT " + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE " + d_mms_recipient_id + " NOT IN (SELECT DISTINCT _id FROM recipient)", &results); // if (results.rows() > 0) // { // std::cout << "WARNING:" << " Found messages referencing recipient not present in old (complete) database... dropping them!" << std::endl; // d_database.exec("DELETE FROM " + d_mms_table + " WHERE " + d_mms_recipient_id + " NOT IN (SELECT DISTINCT _id FROM recipient)"); // } // // CHECK AND WARN FOR MENTIONS // d_database.exec("SELECT " + d_mms_table + "." + d_mms_recipient_id + ",DATETIME(ROUND(" + d_mms_table + "." + d_mms_date_sent + " / 1000), 'unixepoch', 'localtime') AS 'date_sent'," + d_mms_table + ".thread_id,groups.title FROM " + d_mms_table + " LEFT JOIN thread ON thread._id == " + d_mms_table + ".thread_id LEFT JOIN recipient ON recipient._id == thread." + d_thread_recipient_id + " LEFT JOIN groups ON groups.group_id == recipient.group_id WHERE HEX(" + d_mms_table + ".body) LIKE '%EFBFBC%'", &results); // if (results.rows() > 0) // { // std::cout << "WARNING" << " Mentions found! Probably a good idea to check these messages:" << std::endl; // for (unsigned int i = 0; i < results.rows(); ++i) // { // std::cout << " - Group: " << results.valueAsString(i, "title") << std::endl; // std::cout << " Date : " << results.valueAsString(i, "date_sent") << std::endl; // } // } // // and copy avatars and attachments. // for (auto &att : tf->d_attachments) // d_attachments.emplace(std::move(att)); // // update thread snippet and date and count // updateThreadsEntries(); // d_database.exec("VACUUM"); // d_database.freeMemory(); // return true; // } // /* // switch sender and recipient in single one-to-one conversation? // */ bool SignalBackup::hiperfall(uint64_t t_id, std::string const &selfid) { SqliteDB::QueryResults results; // get the recipient id's for the participants in this conversation long long int self_id = -1; if (selfid.empty()) { self_id = scanSelf(); if (self_id == -1) { Logger::error("Failed to determine recipient, please add option to specify backup owners own phone number, for example: `--setselfid \"+31612345678\"'"); return false; } } else { if (!d_database.exec("SELECT _id FROM recipient WHERE phone = ?", selfid, &results)) return false; if (results.rows() != 1) { Logger::error("Unexpected query results (1)"); return false; } self_id = results.getValueAs(0, "_id"); } long long int partner_id = -1; if (!d_database.exec("SELECT " + d_thread_recipient_id + " FROM thread WHERE _id = ?", t_id, &results)) return false; if (results.rows() != 1) { Logger::error("Unexpected query results (2)"); return false; } partner_id = results.getValueAs(0, d_thread_recipient_id); if (partner_id == self_id) { Logger::error("Got same recipients for sender and receiver: ", self_id, " & ", partner_id); return false; } Logger::message("Got recipients: ", self_id, " & ", partner_id); // now switch them if (!d_database.exec("UPDATE recipient SET _id = ? WHERE _id = ?", {-partner_id, self_id}) || !d_database.exec("UPDATE recipient SET _id = ? WHERE _id = ?", {self_id, partner_id}) || !d_database.exec("UPDATE recipient SET _id = ? WHERE _id = ?", {partner_id, -partner_id})) { Logger::error("Failed to switch recipient id's"); return false; } // since msl_ tables (messagesendlog) deal with sent messages, we should clear them // (they are received messages now). d_database.exec("DELETE FROM msl_payload"); d_database.exec("DELETE FROM msl_recipient"); d_database.exec("DELETE FROM msl_message"); // makes no sense to keep this, user should just make new profiles if (d_database.containsTable("notification_profile")) { d_database.exec("DELETE FROM notification_profile"); d_database.exec("DELETE FROM notification_profile_schedule"); d_database.exec("DELETE FROM notification_profile_allowed_members"); } // delete other threads if (d_database.containsTable("sms")) d_database.exec("DELETE FROM sms WHERE thread_id IS NOT ?", t_id); d_database.exec("DELETE FROM " + d_mms_table + " WHERE thread_id IS NOT ?", t_id); d_database.exec("DELETE FROM thread WHERE _id IS NOT ?", t_id); cleanDatabaseByMessages(); auto setType = [](uint64_t oldtype, uint64_t newtype) { return (oldtype & ~(static_cast (0x1f))) + newtype; }; if (d_database.containsTable("sms")) { // get min and max id from sms d_database.exec("SELECT MIN(_id),MAX(_id) FROM sms", &results); if (results.rows() != 1 || !results.valueHasType(0, 0) || !results.getValueAs(0, 1)) { Logger::error("Unexpected query results (3)"); return false; } uint64_t minsmsid = results.getValueAs(0, 0); uint64_t maxsmsid = results.getValueAs(0, 1); Logger::message("min/max: ", minsmsid, " ", maxsmsid); Logger::message("Switching sms entries..."); for (unsigned int i = minsmsid; i <= maxsmsid ; ++i) { if (!d_database.exec("SELECT * FROM sms WHERE _id = ?", i, &results)) return false; if (results.rows() == 0) continue; if (results.rows() > 1) { Logger::error("Unexpected query results (4)"); return false; } uint64_t type = results.getValueAs(0, "type"); using namespace std::string_literals; switch (type & 0x1F) { /* For incoming and outgoing calls, mostly only the type changes, but (at least on new entries) an incoming call that was not successful (not picked up), the notified_timestamp and reactions_last_seen are filled in. For incoming calls that do connect, these are empty (as with outgoing). When switching, I leave them unchanged (-1 and 0) as older entries, before these fields existed have this anyway, so the app should be able to deal with them. SELECT * from sms WHERE type BETWEEN 1 AND 3 ORDER BY + d_sms_date_received + ASC; _id|thread_id|address|address_device_id|person|date|date_sent|date_server|protocol|read|status|type|reply_path_present|delivery_receipt_count|subject|body|mismatched_identities|service_center|subscription_id|expires_in|expire_started|notified|read_receipt_count|unidentified|reactions|reactions_unread|reactions_last_seen|remote_deleted|notified_timestamp|server_guid|receipt_timestamp (outgoing, unsuccessful) 30|1|2|1||1633872620815|1633872620813|-1||1|-1|2||0|||||-1|0|0|0|0|0||0| -1|0| 0||-1 (outgoing, successful) 31|1|2|1||1633872637225|1633872637221|-1||1|-1|2||0|||||-1|0|0|0|0|0||0| -1|0| 0||-1 (incoming, missed) 32|1|2|1||1633872653837|1633872649947|-1||1|-1|3||0|||||-1|0|0|0|0|0||0|1633872655142|0|1633872654248||-1 (incoming, accepted) 33|1|2|1||1633872661166|1633872661164|-1||1|-1|1||0|||||-1|0|0|0|0|0||0| -1|0| 0||-1 SIMILAR GOES FOR VIDEO CALLS (outgoing, unsuccessful) 35|1|2|1||1633873910158|1633873910157|-1||1|-1|11||0|||||-1|0|0|0|0|0||0| -1|0| 0||-1 (outgoing, successful) 36|1|2|1||1633873922647|1633873922644|-1||1|-1|11||0|||||-1|0|0|0|0|0||0| -1|0| 0||-1 (incoming, missed) 37|1|2|1||1633873937640|1633873934877|-1||1|-1| 8||0|||||-1|0|0|0|0|0||0|1633873939058|0|1633873937835||-1 (incoming, rejected) 38|1|2|1||1633873946618|1633873946615|-1||1|-1| 8||0|||||-1|0|0|0|0|0||0|1633873947990|0| 0||-1 (incoming, accepted) 39|1|2|1||1633873954061|1633873954058|-1||1|-1|10||0|||||-1|0|0|0|0|0||0| -1|0| 0||-1 */ case Types::INCOMING_CALL_TYPE: { uint64_t newtype = setType(type, Types::OUTGOING_CALL_TYPE); d_database.exec("UPDATE sms SET type = ? WHERE _id IS ?", {newtype, i}); break; } case Types::OUTGOING_CALL_TYPE: { uint64_t newtype = setType(type, Types::INCOMING_CALL_TYPE); d_database.exec("UPDATE sms SET type = ? WHERE _id IS ?", {newtype, i}); break; } case Types::MISSED_CALL_TYPE: { uint64_t newtype = setType(type, Types::OUTGOING_CALL_TYPE); if (!d_database.exec("UPDATE sms SET" " type = ?," " reactions_last_seen = ?," " notified_timestamp = ?" " WHERE _id = ?", {newtype, -1, 0, i})) return false; break; } case Types::JOINED_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'JOINED_TYPE'"); break; } case Types::UNSUPPORTED_MESSAGE_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'UNSUPPORTED_MESSAGE_TYPE'"); break; } case Types::INVALID_MESSAGE_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'INVALID_MESSAGE_TYPE'"); break; } case Types::PROFILE_CHANGE_TYPE: { // incoming profile change messages are not present for sender d_database.exec("DELETE FROM sms WHERE _id = ?", i); break; } case Types::MISSED_VIDEO_CALL_TYPE: { uint64_t newtype = setType(type, Types::OUTGOING_VIDEO_CALL_TYPE); if (!d_database.exec("UPDATE sms SET" " type = ?," " reactions_last_seen = ?," " notified_timestamp = ?" " WHERE _id = ?", {newtype, -1, 0, i})) return false; break; } case Types::GV1_MIGRATION_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'GV1_MIGRATION_TYPE'"); // should not be present in 1-on-1 threads break; } case Types::INCOMING_VIDEO_CALL_TYPE: { uint64_t newtype = setType(type, Types::OUTGOING_VIDEO_CALL_TYPE); d_database.exec("UPDATE sms SET type = ? WHERE _id IS ?", {newtype, i}); break; } case Types::OUTGOING_VIDEO_CALL_TYPE: { uint64_t newtype = setType(type, Types::INCOMING_VIDEO_CALL_TYPE); d_database.exec("UPDATE sms SET type = ? WHERE _id IS ?", {newtype, i}); break; } case Types::GROUP_CALL_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'GROUP_CALL_TYPE'"); break; } case Types::BASE_INBOX_TYPE: { /* incoming to outgoing message changes: date_server -> -1 protocol -> NULL type -> |0x1f -> 23 reply_path_present -> NULL delivery_receipt_count -> 1 service_center -> NULL reactions_last_seen -> -1 notified_timestamp -> -1 server_guid -> NULL receipt_timestamp -> -1(old) ~(date+300)(new) NOTE, I use the old value for receipt_timestamp of -1. All messages that were in the db before this field existed are -1 so the app should be able to deal with this properly */ uint64_t newtype = setType(type, Types::BASE_SENT_TYPE); if (d_database.tableContainsColumn("sms", "protocol") && d_database.tableContainsColumn("sms", "service_center") && d_database.tableContainsColumn("sms", "reply_path_present")) // REMOVED IN DBV166 { if (!d_database.exec("UPDATE sms SET" " date_server = ?," " protocol = ?," " type = ?," " reply_path_present = ?," " delivery_receipt_count = ?," " service_center = ?," " reactions_last_seen = ?," " notified_timestamp = ?," " server_guid = ?" //" receipt_timestamp = ?" " WHERE _id = ?", {-1, nullptr, newtype, nullptr, 1, nullptr, -1, -1, nullptr, i})) return false; } else { if (!d_database.exec("UPDATE sms SET" " date_server = ?," " type = ?," " delivery_receipt_count = ?," " reactions_last_seen = ?," " notified_timestamp = ?," " server_guid = ?" //" receipt_timestamp = ?" " WHERE _id = ?", {-1, newtype, 1, -1, -1, nullptr, i})) return false; } break; } case Types::BASE_OUTBOX_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'BASE_OUTBOX_TYPE'"); break; } case Types::BASE_SENDING_TYPE: { /* Not sure what to do with this. Lets say it was eventually successfully sent sometime after this backup was made... not sure about all the fields, I don't have this type in my db's */ uint64_t newtype = setType(type, Types::BASE_INBOX_TYPE); if (d_database.tableContainsColumn("sms", "protocol") && d_database.tableContainsColumn("sms", "service_center") && d_database.tableContainsColumn("sms", "reply_path_present")) // REMOVED IN DBV166 { if (!d_database.exec("UPDATE sms SET" //" date_server = ?," " protocol = ?," " type = ?," " reply_path_present = ?," " delivery_receipt_count = ?," //" reactions_last_seen = ?," //" notified_timestamp = ?," //" server_guid = ?," //" receipt_timestamp = ?," " service_center = ?" " WHERE _id = ?", {31337, newtype, 1, 0, "GCM"s, i})) return false; } else { if (!d_database.exec("UPDATE sms SET" //" date_server = ?," " type = ?," " delivery_receipt_count = ?" //" reactions_last_seen = ?," //" notified_timestamp = ?," //" server_guid = ?," //" receipt_timestamp = ?," " WHERE _id = ?", {newtype, 0, i})) return false; } break; } case Types::BASE_SENT_TYPE: { /* outgoing to incoming message changes: date_server -> -1(old) ~(date-1000)(new) protocol -> 31337 type -> |0x1f -> 20 reply_path_present -> 1 delivery_receipt_count -> 0 service_center -> GCM reactions_last_seen -> -1(old) ~(date+40000)(new) notified_timestamp -> 0(old) ~(date+150)(new) server_guid -> NULL(old) Something(new) receipt_timestamp -> -1 NOTE, I use the old values where available. All messages that were in the db before these fields existed are -1 (or 0) so the app should be able to deal with this properly */ uint64_t newtype = setType(type, Types::BASE_INBOX_TYPE); if (d_database.tableContainsColumn("sms", "protocol") && d_database.tableContainsColumn("sms", "reply_path_present") && d_database.tableContainsColumn("sms", "service_center")) // removed in dbv 166 { if (!d_database.exec("UPDATE sms SET" //" date_server = ?," " protocol = ?," " type = ?," " reply_path_present = ?," " delivery_receipt_count = ?," //" reactions_last_seen = ?," //" notified_timestamp = ?," //" server_guid = ?," //" receipt_timestamp = ?," " service_center = ?" " WHERE _id = ?", {31337, newtype, 1, 0, "GCM"s, i})) return false; } else { if (!d_database.exec("UPDATE sms SET" " type = ?," " delivery_receipt_count = ?" " WHERE _id = ?", {newtype, 0, i})) return false; } break; } case Types::BASE_SENT_FAILED_TYPE: { // failed sent message is not present at receiver? d_database.exec("DELETE FROM sms WHERE _id = ?", i); break; } case Types::BASE_PENDING_SECURE_SMS_FALLBACK: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'BASE_PENDING_SECURE_SMS_FALLBACK'"); break; } case Types::BASE_PENDING_INSECURE_SMS_FALLBACK: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'BASE_PENDING_INSECURE_SMS_FALLBACK'"); break; } case Types::BASE_DRAFT_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'BASE_DRAFT_TYPE'"); break; } default: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f)); break; } } } } // get min and max id from mms d_database.exec("SELECT MIN(_id),MAX(_id) FROM " + d_mms_table, &results); if (results.rows() != 1 || !results.valueHasType(0, 0) || !results.getValueAs(0, 1)) { Logger::error("Unexpected query results (5)"); return false; } uint64_t minmmsid = results.getValueAs(0, 0); uint64_t maxmmsid = results.getValueAs(0, 1); Logger::message("min/max: ", minmmsid, " ", maxmmsid); Logger::message("Switching mms entries..."); for (unsigned int i = minmmsid; i <= maxmmsid ; ++i) { if (!d_database.exec("SELECT * FROM " + d_mms_table + " WHERE _id = ?", i, &results)) return false; if (results.rows() == 0) continue; if (results.rows() > 1) { Logger::error("Unexpected query results (6)"); return false; } uint64_t type = results.getValueAs(0, d_mms_type); using namespace std::string_literals; switch (type & 0x1F) { case Types::BASE_INBOX_TYPE: { uint64_t newtype = setType(type, Types::BASE_SENT_TYPE); if (!d_database.exec("UPDATE " + d_mms_table + " SET" " date_server = ?," " " + d_mms_type + " = ?," " m_type = ?," " st = ?," " " + d_mms_read_receipts + " = ?," " " + d_mms_delivery_receipts + " = ?," " reactions_last_seen = ?," " notified_timestamp = ?," " server_guid = ?" //" receipt_timestamp = ?" " WHERE _id = ?", {-1, newtype, 128, nullptr, 1, 1, -1, 0, nullptr, i})) return false; break; } case Types::BASE_SENT_TYPE: { /* */ uint64_t newtype = setType(type, Types::BASE_INBOX_TYPE); if (!d_database.exec("UPDATE " + d_mms_table + " SET" " " + d_mms_type + " = ?," " m_type = ?," " st = ?," " " + d_mms_read_receipts + " = ?," " " + d_mms_delivery_receipts + " = ?," " receipt_timestamp = ?" " WHERE _id = ?", {newtype, 132, 1, 1, 0, -1, i})) return false; break; } case Types::BASE_SENDING_TYPE: { /* Not sure what to do with this. Lets say it was eventually successfully sent sometime after this backup was made... */ uint64_t newtype = setType(type, Types::BASE_INBOX_TYPE); if (!d_database.exec("UPDATE " + d_mms_table + " SET" " " + d_mms_type + " = ?," " m_type = ?," " st = ?," " " + d_mms_read_receipts + " = ?," " " + d_mms_delivery_receipts + " = ?," " receipt_timestamp = ?" " WHERE _id = ?", {newtype, 132, 1, 1, 0, -1, i})) return false; break; } case Types::BASE_SENT_FAILED_TYPE: { // failed sent message is not present at receiver? d_database.exec("DELETE FROM " + d_mms_table + " WHERE _id = ?", i); break; } case Types::INCOMING_CALL_TYPE: { uint64_t newtype = setType(type, Types::OUTGOING_CALL_TYPE); d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_type + " = ? WHERE _id IS ?", {newtype, i}); break; } case Types::OUTGOING_CALL_TYPE: { uint64_t newtype = setType(type, Types::INCOMING_CALL_TYPE); d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_type + " = ? WHERE _id IS ?", {newtype, i}); break; } case Types::MISSED_CALL_TYPE: { uint64_t newtype = setType(type, Types::OUTGOING_CALL_TYPE); if (!d_database.exec("UPDATE " + d_mms_table + " SET" " " + d_mms_type + " = ?," " reactions_last_seen = ?," " notified_timestamp = ?" " WHERE _id = ?", {newtype, -1, 0, i})) return false; break; } case Types::INCOMING_VIDEO_CALL_TYPE: { uint64_t newtype = setType(type, Types::OUTGOING_VIDEO_CALL_TYPE); d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_type + " = ? WHERE _id IS ?", {newtype, i}); break; } case Types::OUTGOING_VIDEO_CALL_TYPE: { uint64_t newtype = setType(type, Types::INCOMING_VIDEO_CALL_TYPE); d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_type + " = ? WHERE _id IS ?", {newtype, i}); break; } case Types::MISSED_VIDEO_CALL_TYPE: { uint64_t newtype = setType(type, Types::OUTGOING_VIDEO_CALL_TYPE); if (!d_database.exec("UPDATE " + d_mms_table + " SET" " " + d_mms_type + " = ?," " reactions_last_seen = ?," " notified_timestamp = ?" " WHERE _id = ?", {newtype, -1, 0, i})) return false; break; } case Types::PROFILE_CHANGE_TYPE: { // incoming profile change messages are not present for sender d_database.exec("DELETE FROM " + d_mms_table + " WHERE _id = ?", i); break; } case Types::GV1_MIGRATION_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'GV1_MIGRATION_TYPE'"); // should not be present in 1-on-1 threads break; } case Types::GROUP_CALL_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'GROUP_CALL_TYPE'"); break; } case Types::JOINED_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'JOINED_TYPE'"); break; } case Types::UNSUPPORTED_MESSAGE_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'UNSUPPORTED_MESSAGE_TYPE'"); break; } case Types::INVALID_MESSAGE_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'INVALID_MESSAGE_TYPE'"); break; } case Types::BASE_OUTBOX_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'BASE_OUTBOX_TYPE'"); break; } case Types::BASE_PENDING_SECURE_SMS_FALLBACK: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'BASE_PENDING_SECURE_SMS_FALLBACK'"); break; } case Types::BASE_PENDING_INSECURE_SMS_FALLBACK: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'BASE_PENDING_INSECURE_SMS_FALLBACK'"); break; } case Types::BASE_DRAFT_TYPE: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f), " : 'BASE_DRAFT_TYPE'"); break; } default: { Logger::message("Unhandled type: ", i, " ", (type & 0x1f)); break; } } } return true; /* Mapping types: *20 (incoming) -> 23 (sent) *23 (sent) -> 20 (incoming) 22 (sending) -> ??? *1 (incoming audio call) -> 2 (outgoing audio call) *2 (outgoind audio call) -> 1 (incoming audio call) *3 (missed call) -> 2 (outgoing audio call)? *7 (profile change) -> REMOVE? *8 (missed video call) -> 11 (outgoing video call)? *10 (incoming video call) -> 11 (outgoing video call) *11 (outgoing video call) -> 10 (incoming video call)? *24 (sent_failed) -> ??? */ } // void SignalBackup::devCustom() const // { // SqliteDB::QueryResults res; // // d_database.exec("SELECT body FROM sms where _id = 120", &res); // // DecryptedGroupV2Context sts(res.valueAsString(0, "body")); // // sts.print(); // std::set uuids; // using namespace std::string_literals; // for (auto const &q : {d_database.containsTable("sms") ? "SELECT body FROM sms WHERE (type & ?) != 0 AND (type & ?) != 0"s : ""s, // "SELECT body FROM " + d_mms_table + " WHERE (" + d_mms_type + " & ?) != 0 AND (" + d_mms_type + " & ?) != 0"s}) // { // if (q.empty()) // continue; // //d_database.exec("SELECT body FROM sms WHERE (type & ?) != 0 AND (type & ?) != 0", // d_database.exec(q, {Types::GROUP_UPDATE_BIT, Types::GROUP_V2_BIT}, &res); // for (unsigned int i = 0; i < res.rows(); ++i) // { // DecryptedGroupV2Context sts2(res.valueAsString(i, "body")); // //std::cout << "STATUS MSG " << i << std::endl; // // NEW DATA // auto field3 = sts2.getField<3>(); // if (field3.has_value()) // { // auto field3_7 = field3->getField<7>(); // for (unsigned int j = 0; j < field3_7.size(); ++j) // { // auto field3_7_1 = field3_7[j].getField<1>(); // if (field3_7_1.has_value()) // uuids.insert(bepaald::bytesToHexString(*field3_7_1, true)); // // else // // { // // std::cout << "No members found in field 3" << std::endl; // // sts2.print(); // // } // } // // if (field3_7.size() == 0) // // { // // std::cout << "No members found in field 3" << std::endl; // // sts2.print(); // // } // } // // else // // { // // std::cout << "No members found in field 3" << std::endl; // // sts2.print(); // // } // // OLD DATA? // auto field4 = sts2.getField<4>(); // if (field4.has_value()) // { // auto field4_7 = field4->getField<7>(); // for (unsigned int j = 0; j < field4_7.size(); ++j) // { // auto field4_7_1 = field4_7[j].getField<1>(); // if (field4_7_1.has_value()) // uuids.insert(bepaald::bytesToHexString(*field4_7_1, true)); // // else // // { // // std::cout << "No members found in field 4" << std::endl; // // sts2.print(); // // } // } // // if (field4_7.size() == 0) // // { // // std::cout << "No members found in field 4" << std::endl; // // sts2.print(); // // } // } // // else // // { // // std::cout << "No members found in field 4" << std::endl; // // sts2.print(); // // } // } // } // std::cout << "LIST OF FOUND UUIDS:" << std::endl; // for (auto &uuid : uuids) // std::cout << uuid << std::endl; // if (uuids.size()) // { // std::string q = "SELECT DISTINCT _id FROM recipient WHERE LOWER(uuid) IN ("; // #if __cplusplus > 201703L // for (int pos = 0; std::string uuid : uuids) // #else // int pos = 0; // for (std::string uuid : uuids) // #endif // { // if (pos > 0) // q += ", "; // uuid.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); // //std::transform(uuid.begin(), uuid.end(), uuid.begin(), [](unsigned char c){ return std::tolower(c); }); // q += "LOWER('" + uuid + "')"; // //std::transform(uuid.begin(), uuid.end(), uuid.begin(), [](unsigned char c){ return std::toupper(c); }); // //q+= "'" + uuid + "'"; // ++pos; // } // q += ")"; // std::cout << "'" << q << "'" << std::endl; // d_database.exec(q, &res); // res.prettyPrint(); // } // } signalbackup-tools-20250313-1/signalbackup/datetomsecssinceepoch.cc000066400000000000000000000027511476450434500252610ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" long long int SignalBackup::dateToMSecsSinceEpoch(std::string const &date, bool *fromdatestring) const { long long int ret = -1; // check std::regex datestring("[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}"); if (std::regex_match(date, datestring)) { std::tm t = {}; // sets all to 0: NO daylight savings... t.tm_isdst = -1; // set daylight savings time to unknown (handle automatically) std::istringstream ss(date); if (ss >> std::get_time(&t, "%Y-%m-%d %H:%M:%S")) ret = std::mktime(&t) * 1000; if (fromdatestring) *fromdatestring = true; } else { ret = bepaald::toNumber(date, -1); if (fromdatestring) *fromdatestring = false; } return ret; } signalbackup-tools-20250313-1/signalbackup/decodeprofilechangemessage.cc000066400000000000000000000035601476450434500262230ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" std::string SignalBackup::decodeProfileChangeMessage(std::string const &body, std::string const &name) const { /* // from app/src/main/proto/Database.proto message ProfileChangeDetails { message StringChange { string previous = 1; string new = 2; } StringChange profileNameChange = 1; } */ ProfileChangeDetails profchangefull(body); //std::cout << body << std::endl; //profchangefull.print(); if (!profchangefull.getField<1>().has_value()) return name + " has changed their profile name."; StringChange profilenamechange = profchangefull.getField<1>().value(); if ((!profilenamechange.getField<1>().has_value() || profilenamechange.getField<1>().value() == "") || (!profilenamechange.getField<2>().has_value() || profilenamechange.getField<2>().value() == "")) return name + " has changed their profile name."; std::string oldname = profilenamechange.getField<1>().value(); std::string newname = profilenamechange.getField<2>().value(); return oldname + " changed their profile name to " + newname + "."; //decodeProfileChange(body); } signalbackup-tools-20250313-1/signalbackup/decodestatusmessage.cc000066400000000000000000001174401476450434500247430ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "htmlicontypes.h" std::string SignalBackup::decodeStatusMessage(std::string const &body, long long int expiration, long long int type, std::string const &contactname, IconType *icon) const { // std::cout << "DECODING: " << std::endl << " " << body << std::endl << " " << expiration << std::endl // << " " << type << std::endl << " " << contactname << std::endl; // std::cout << Types::isGroupUpdate(type) << std::endl; // std::cout << Types::isGroupV2(type) << std::endl; // old style group updates (v1) if (Types::isGroupUpdate(type) && !Types::isGroupV2(type)) { if (Types::isOutgoing(type)) return "You updated the group."; std::string result = contactname + " updated the group."; GroupContext statusmsg(body); std::string members; auto field4 = statusmsg.getField<4>(); if (field4.size()) { for (unsigned int k = 0; k < field4.size(); ++k) { // get name from members string SqliteDB::QueryResults res; if (d_database.containsTable("recipient")) [[likely]] // dbv >= 24 d_database.exec("SELECT COALESCE(" + (d_database.tableContainsColumn("recipient", "nickname_joined_name") ? "NULLIF(recipient.nickname_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_system_joined_name + ", ''), " + (d_database.tableContainsColumn("recipient", "profile_joined_name") ? "NULLIF(recipient.profile_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_profile_given_name + ", ''), " + "NULLIF(recipient." + d_recipient_e164 + ", ''), NULLIF(recipient." + d_recipient_aci + ", ''), recipient._id) AS 'name'" " FROM recipient WHERE " + d_recipient_e164 + " = ?", field4[k], &res); else d_database.exec("SELECT COALESCE(recipient_preferences.system_display_name, recipient_preferences.signal_profile_name) AS 'name' FROM recipient_preferences WHERE recipient_preferences.recipient_ids = ?", field4[k], &res); std::string name = field4[k]; if (res.rows() == 1 && res.columns() == 1 && res.valueHasType(0, "name")) name = res.getValueAs(0, "name"); members += name; if (k < field4.size() - 1) members += ", "; } } if (!members.empty()) result += "\n" + members + " joined the group."; std::string title = statusmsg.getField<3>().value_or(std::string()); if (!title.empty()) { result += (!members.empty() ? ' ' : '\n'); result += "Group name is now '" + title + "'."; } return result; } // GROUPUPDATE && !GROUP_V2 // SOME NON-GROUP TYPES (_can_ still occur in group) if (Types::isGroupQuit(type)) { if (Types::isOutgoing(type)) return "You left the group."; return contactname + " left the group."; } if (Types::isIncomingVideoCall(type)) return "Incoming video call"; if (Types::isOutgoingVideoCall(type)) return "Outgoing video call"; if (Types::isMissedVideoCall(type)) return "Missed video call"; if (Types::isIncomingCall(type)) return "Incoming voice call"; if (Types::isOutgoingCall(type)) return "Outgoing voice call"; if (Types::isMissedCall(type)) return "Missed voice call"; if (Types::isGroupCall(type)) return "Group call"; if (Types::isJoined(type)) return contactname + " is on Signal!"; if (Types::isExpirationTimerUpdate(type)) { expiration /= 1000; // from milli to seconds (the group update expiration timer is in seconds) if (expiration <= 0) { if (Types::isOutgoing(type)) return "You disabled disappearing messages."; return contactname + " disabled disappearing messages."; } std::string time; if (expiration < 60) // less than full minute time = bepaald::toString(expiration) + " second" + (expiration > 1 ? "s" : ""); else if (expiration < 60 * 60) // less than full hour time = bepaald::toString(expiration / 60) + " minute" + ((expiration / 60) > 1 ? "s" : ""); else if (expiration < 24 * 60 * 60) // less than full day time = bepaald::toString(expiration / (60 * 60)) + " hour" + ((expiration / (60 * 60)) > 1 ? "s" : ""); else if (expiration < 7 * 24 * 60 * 60) // less than full week time = bepaald::toString(expiration / (24 * 60 * 60)) + " day" + ((expiration / (24 * 60 * 60)) > 1 ? "s" : ""); else // show expiration in number of weeks time = bepaald::toString(expiration / (7 * 24 * 60 * 60)) + " week" + ((expiration / (7 * 24 * 60 * 60)) > 1 ? "s" : ""); if (Types::isOutgoing(type)) return "You set the disappearing message timer to " + time; return contactname + " set the disappearing message timer to " + time; } if (Types::isIdentityUpdate(type)) return "Your safety number with " + contactname + " has changed."; if (Types::isIdentityVerified(type)) { if (Types::isOutgoing(type)) return "You marked your safety number with " + contactname + " verified"; return "You marked your safety number with " + contactname + " verified from another device"; } if (Types::isIdentityDefault(type)) { if (Types::isOutgoing(type)) return "You marked your safety number with " + contactname + " unverified"; return "You marked your safety number with " + contactname + " unverified from another device"; } if (Types::isNumberChange(type)) { if (Types::isOutgoing(type)) return "You changed your phone number."; // doesnt exist return contactname + " changed their phone number."; } if (Types::isDonationRequest(type)) { return "Like this new feature? Help support Signal with a one-time donation."; } if (Types::isEndSession(type)) { if (Types::isOutgoing(type)) return "You reset the secure session."; return contactname + " reset the secure session."; } if (Types::isProfileChange(type)) { return decodeProfileChangeMessage(body, contactname); } if (Types::isMessageRequestAccepted(type)) { return "You accepted the message request"; } /* GROUP_V2 UPDATES */ if (Types::isGroupUpdate(type) && Types::isGroupV2(type)) // see app/src/test/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducerTest.java { //std::cout << body << std::endl; DecryptedGroupV2Context groupv2ctx(body); //groupv2ctx.print(); std::string statusmsg; if (groupv2ctx.getField<2>().has_value()) { DecryptedGroupChange groupchange = groupv2ctx.getField<2>().value(); //std::cout << bepaald::bytesToHexString(groupchange.data(), groupchange.size()) << std::endl; //groupchange.print(); // invite link changed: if (groupchange.getField<15>().has_value()) { if (icon && *icon == IconType::NONE) *icon = IconType::MEGAPHONE; // new value: 0 unknown, 1 any, 2 member, 3 admin, 4 unsatisfiable int accesscontrol = groupchange.getField<15>().value(); // get editor std::string editoruuid; if (groupchange.getField<1>().has_value()) { auto [uuid, uuid_size] = groupchange.getField<1>().value(); editoruuid = bepaald::toLower(bepaald::bytesToHexString(uuid, uuid_size, true)); editoruuid.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); } // previous accesscontrol: 0 unknown, 1 any, 2 member, 3 admin, 4 unsatisfiable int old_accesscontrol = 0; if (groupv2ctx.getField<4>().has_value() && groupv2ctx.getField<4>().value().getField<5>().has_value() && groupv2ctx.getField<4>().value().getField<5>().value().getField<3>().has_value()) old_accesscontrol = groupv2ctx.getField<4>().value().getField<5>().value().getField<3>().value(); if (editoruuid == d_selfuuid) { if (accesscontrol == 4) return "You turned off the group link."; if (accesscontrol == 3) { if (old_accesscontrol == 1) return "You turned on admin approval for the group link."; else return "You turned on the group link with admin approval on."; } if (accesscontrol == 1) { if (old_accesscontrol == 3) return "You turned off admin approval for the group link."; else return "You turned on the group link with admin approval off."; } } std::string editorname = editoruuid.empty() ? editoruuid : getNameFromUuid(editoruuid); if (editorname.empty()) { if (accesscontrol == 4) return "The group link has been turned off."; if (accesscontrol == 3) { if (old_accesscontrol == 1) return "The admin approval for the group link has been turned on."; else return "The group link has been turned on with admin approval on."; } if (accesscontrol == 1) { if (old_accesscontrol == 3) return "The admin approval for the group link has been turned off."; else return "The group link has been turned on with admin approval off."; } } else { if (accesscontrol == 4) return editorname + " turned off the group link."; if (accesscontrol == 3) { if (old_accesscontrol == 1) return editorname + " turned on admin approval for the group link."; else return editorname + " turned on the group link with admin approval on."; } if (accesscontrol == 1) { if (old_accesscontrol == 3) return editorname + " turned off admin approval for the group link."; else return editorname + " turned on the group link with admin approval off."; } // if (accesscontrol... } } // for accepting invites: check DecryptedGroupChange<1>: (editor) // and DecryptedGroupChange<9>[]:DecryptedMember<1>: uuid (promotePendingMembers) // // if editor == self // if (promotependingmembers contains self) // "You accepted the invitation to the group." // else // "You added invited member Bob." // else // if (promotependingmembers contains self) // "Bob added you to the group." (or if editor is unknown: "You joined the group."); // else if (promotependingmembers contains editor) // "Bob accepted an invitation to the group." // else // "Bob added invited member Alice." (or if editor is unknown: "Alice joined the group."); if (groupchange.getField<1>().has_value() && groupchange.getField<9>().size()) { // editor auto [uuid, uuid_size] = groupchange.getField<1>().value(); std::string uuidstr = bepaald::toLower(bepaald::bytesToHexString(uuid, uuid_size, true)); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); std::vector promotedmemberuuids; for (unsigned int i = 0; i < groupchange.getField<9>().size(); ++i) { DecryptedMember dm = groupchange.getField<9>()[i]; if (dm.getField<1>().has_value()) { auto [tmpuuid, tmpuuid_size] = dm.getField<1>().value(); std::string pmus = bepaald::toLower(bepaald::bytesToHexString(tmpuuid, tmpuuid_size, true)); pmus.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); promotedmemberuuids.emplace_back(pmus); } } if (bepaald::contains(promotedmemberuuids, uuidstr)) // the editor is promoted -> someone accepted an invite { if (icon && *icon == IconType::NONE) *icon = IconType::MEMBER_APPROVED; if (uuidstr == d_selfuuid) return "You accepted the invitation to the group."; else { std::string promotedmember = getNameFromUuid(uuidstr); if (!promotedmember.empty()) return promotedmember + " accepted an invitation to the group."; else return "A new member accepted an invitation to the group."; } } else // the editor is not one of the promoted members { if (icon && *icon == IconType::NONE) *icon = IconType::MEMBER_ADD; if (uuidstr == d_selfuuid) { if (promotedmemberuuids.size() == 1) { std::string promotedmember = getNameFromUuid(promotedmemberuuids[0]); if (!promotedmember.empty()) return "You added invited member " + promotedmember; else return "You added an invited member"; } else return "You added " + bepaald::toString(promotedmemberuuids.size()) + "invited members"; } else if (bepaald::contains(promotedmemberuuids, d_selfuuid)) // you were not editor, but were promoted { std::string editorname = getNameFromUuid(uuidstr); if (!editorname.empty()) return editorname + "added you to the group."; else return "You joined the group."; } else // you were not editor AND not the promoted member { std::string editorname = getNameFromUuid(uuidstr); if (!editorname.empty()) { if (promotedmemberuuids.size() == 1) { std::string promotedmember = getNameFromUuid(promotedmemberuuids[0]); if (!promotedmember.empty()) return editorname + " added invited member " + promotedmember + "."; else return editorname + " added an invited member."; } else return editorname + " added " + bepaald::toString(promotedmemberuuids.size()) + " invited members."; } else { if (promotedmemberuuids.size() == 1) { std::string promotedmember = getNameFromUuid(promotedmemberuuids[0]); if (!promotedmember.empty()) return promotedmember + " joined the group."; else return "An invited member joined the group."; } else return bepaald::toString(promotedmemberuuids.size()) + " members joined the group."; } } } } // if this is revision 0, and no previous state is given, and new title is -> creataed new group if (!groupv2ctx.getField<4>().has_value() && // no previous state (groupv2ctx.getField<1>().has_value() && groupv2ctx.getField<1>().value().getField<2>().has_value() && groupv2ctx.getField<1>().value().getField<2>().value() == 0)) { auto [uuid, uuid_size] = groupchange.getField<1>().value(); std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); if (Types::isOutgoing(type)) { if (icon && *icon == IconType::NONE) *icon = IconType::MEMBERS; return "You created the group."; } //else if (icon && *icon == IconType::NONE) *icon = IconType::MEMBER_ADD; return contactname + " added you to the group."; } // check group title changed if (groupchange.getField<10>().has_value() && groupchange.getField<10>().value().getField<1>().has_value()) { statusmsg += (Types::isOutgoing(type) ? "You" : contactname) + " changed the group name to \"" + groupchange.getField<10>().value().getField<1>().value() + "\"."; if (icon) *icon = IconType::PENCIL; } // check group description changed if (groupchange.getField<20>().has_value()/* && groupchange.getField<20>().value().getField<1>().has_value()*/) { statusmsg += (!statusmsg.empty() ? "\n" : "") + (Types::isOutgoing(type) ? "You" : contactname) + " changed the group description."; if (icon) *icon = IconType::PENCIL; } // check group avatar changed if (groupchange.getField<11>().has_value()/* && groupchange.getField<11>().value().getField<1>().has_value()*/) { statusmsg += (!statusmsg.empty() ? "\n" : "") + (Types::isOutgoing(type) ? "You" : contactname) + " changed the group avatar."; if (icon && *icon == IconType::NONE) *icon = IconType::AVATAR_UPDATE; } // check group timer changed : THIS TIMER IS IN SECONDS (message.expires_in, for non-group messages is in milliseconds) if (groupchange.getField<12>().has_value()/* && groupchange.getField<12>().value().getField<1>().has_value()*/) { uint32_t newexp = groupchange.getField<12>().value().getField<1>().value_or(0); std::string time; if (newexp == 0) time = "Off"; else if (newexp < 60) // less than full minute time = bepaald::toString(newexp) + " second" + (newexp > 1 ? "s" : ""); else if (newexp < 60 * 60) // less than full hour time = bepaald::toString(newexp / 60) + " minute" + ((newexp / 60) > 1 ? "s" : ""); else if (newexp < 24 * 60 * 60) // less than full day time = bepaald::toString(newexp / (60 * 60)) + " hour" + ((newexp / (60 * 60)) > 1 ? "s" : ""); else if (newexp < 7 * 24 * 60 * 60) // less than full week time = bepaald::toString(newexp / (24 * 60 * 60)) + " day" + ((newexp / (24 * 60 * 60)) > 1 ? "s" : ""); else // show newexp in number of weeks time = bepaald::toString(newexp / (7 * 24 * 60 * 60)) + " week" + ((newexp / (7 * 24 * 60 * 60)) > 1 ? "s" : ""); statusmsg += (!statusmsg.empty() ? "\n" : "") + (Types::isOutgoing(type) ? "You" : contactname) + " set the disappearing message timer to " + time + "."; if (icon && *icon == IconType::NONE) *icon = (newexp == 0) ? IconType::TIMER_DISABLE : IconType::TIMER_UPDATE; } // check new member: if (groupchange.getField<3>().size()) { // get editor of this group change std::string editoruuid; if (groupchange.getField<1>().has_value()) { auto [uuid, uuid_size] = groupchange.getField<1>().value(); editoruuid = bepaald::bytesToHexString(uuid, uuid_size, true); editoruuid.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); } auto newmembers = groupchange.getField<3>(); for (unsigned int i = 0; i < newmembers.size(); ++i) { auto [uuid, uuid_size] = newmembers[i].getField<1>().value_or(std::make_pair(nullptr, 0)); // bytes std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); if (uuidstr == editoruuid) // you can't add yourself to the group, if this happens { // you joined via invite link (without approval) statusmsg += (Types::isOutgoing(type) ? "You" : contactname) + " joined the group via the group link"; if (icon && *icon == IconType::NONE) *icon = IconType::MEMBER_APPROVED; } else { statusmsg += (Types::isOutgoing(type) ? "You" : contactname) + " added " + getNameFromUuid(uuidstr) + "."; if (icon && *icon == IconType::NONE) *icon = IconType::MEMBER_ADD; } } } // check members left: auto deletedmembers = groupchange.getField<4>(); for (unsigned int i = 0; i < deletedmembers.size(); ++i) // I dont know how this can be more than size() == 1 { auto [uuid, uuid_size] = deletedmembers[i]; // bytes std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); statusmsg += (!statusmsg.empty() ? "\n" : "") + (Types::isOutgoing(type) ? "You" : contactname) + " removed " + getNameFromUuid(uuidstr) + "."; if (icon && *icon == IconType::NONE) *icon = IconType::MEMBER_REMOVE; } // check memberrole change auto memberrolechanges = groupchange.getField<5>(); for (unsigned int i = 0; i < memberrolechanges.size(); ++i) // I dont know how this can be more than size() == 1 { DecryptedModifyMemberRole mr = memberrolechanges[i]; std::string uuidstr; if (mr.getField<1>().has_value()) { auto [uuid, uuid_size] = mr.getField<1>().value(); uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); } /* message Member { enum Role { UNKNOWN = 0; DEFAULT = 1; ADMINISTRATOR = 2; }*/ int newrole = mr.getField<2>().value_or(0); if (newrole == 2) statusmsg += (!statusmsg.empty() ? "\n" : "") + (Types::isOutgoing(type) ? "You" : contactname) + " made " + getNameFromUuid(uuidstr) + " an admin."; else statusmsg += (!statusmsg.empty() ? "\n" : "") + (Types::isOutgoing(type) ? "You" : contactname) + " revoked admin privileges from " + getNameFromUuid(uuidstr) + "."; if (icon && *icon == IconType::NONE) *icon = IconType::MEGAPHONE; } // // check members left: // auto deletedmembers = groupchange.getField<4>(); // for (unsigned int i = 0; i < deletedmembers.size(); ++i) // I dont know how this can be more than size() == 1 // { // auto [uuid, uuid_size] = deletedmembers[i]; // bytes // std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); // uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); // statusmsg += (!statusmsg.empty() ? "\n" : "") + (Types::isOutgoing(type) ? "You" : contactname) + " removed " + getNameFromUuid(uuidstr) + "."; // } // field 19 == newInviteLinkPassword if (groupchange.getField<19>().has_value() && groupchange.getField<1>().has_value()) { auto [uuid, uuid_size] = groupchange.getField<1>().value(); std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); if (icon && *icon == IconType::NONE) *icon = IconType::MEGAPHONE; // field 15 == newInviteLinkAccess (== always 1 (== admin approval off) on creation?) if (!groupchange.getField<15>().has_value()) statusmsg += (Types::isOutgoing(type) ? "You" : getNameFromUuid(uuidstr)) + " reset the group link."; else { int accesscontrol = groupchange.getField<15>().value(); statusmsg += (Types::isOutgoing(type) ? "You" : getNameFromUuid(uuidstr)) + " turned on the group link with admin approval " + (accesscontrol == 3 ? "on." : "off."); // never 3/on at creation } } // Field 21 'newIsAnnouncementGroup' if (groupchange.getField<21>().has_value()) { std::string uuidstr; if (groupchange.getField<1>().has_value()) { auto [uuid, uuid_size] = groupchange.getField<1>().value(); uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); } if (icon && *icon == IconType::NONE) *icon = IconType::MEGAPHONE; /* enum EnabledState { UNKNOWN = 0; ENABLED = 1; DISABLED = 2; } */ int enabledstate = groupchange.getField<21>().value(); if (enabledstate == 2) { if (uuidstr.empty()) statusmsg += "The group settings were changed to allow all members to send messages."; else statusmsg += (Types::isOutgoing(type) ? "You" : getNameFromUuid(uuidstr)) + " changed the group settings to allow all members to send messages."; } else { if (uuidstr.empty()) statusmsg += "The group settings were changed to only allow all admins to send messages."; else statusmsg += (Types::isOutgoing(type) ? "You" : getNameFromUuid(uuidstr)) + " changed the group settings to only allow admins to send messages."; } } // Field 13 'newAttributeAccess' : who can edit group info if (groupchange.getField<13>().has_value() && groupchange.getField<1>().has_value()) { auto [uuid, uuid_size] = groupchange.getField<1>().value(); std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); if (icon && *icon == IconType::NONE) *icon = IconType::MEGAPHONE; /* UNKNOWN = 0; ANY = 1; MEMBER = 2; ADMINISTRATOR = 3; UNSATISFIABLE = 4; */ int accesscontrol = groupchange.getField<13>().value(); statusmsg += (Types::isOutgoing(type) ? "You" : getNameFromUuid(uuidstr)) + " changed who can edit group info to \"" + (accesscontrol == 3 ? "Only admins" : "All members") + "\"."; } // Field 14 'newmemberaccess' : who can edit group membership if (groupchange.getField<14>().has_value() && groupchange.getField<1>().has_value()) { auto [uuid, uuid_size] = groupchange.getField<1>().value(); std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); if (icon && *icon == IconType::NONE) *icon = IconType::MEGAPHONE; /* UNKNOWN = 0; ANY = 1; MEMBER = 2; ADMINISTRATOR = 3; UNSATISFIABLE = 4; */ int accesscontrol = groupchange.getField<14>().value(); statusmsg += (Types::isOutgoing(type) ? "You" : getNameFromUuid(uuidstr)) + " changed who can edit group membership to \"" + (accesscontrol == 3 ? "Only admins" : "All members") + "\"."; } // Field 16 'requesting member' : want to join if (groupchange.getField<16>().size() && groupchange.getField<1>().has_value()) { auto [uuid, uuid_size] = groupchange.getField<1>().value(); std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); if (icon && *icon == IconType::NONE) *icon = IconType::MEMBERS; // if field 17 'deletereqestingmembers' is also present (and same uuid as reqesting member?) // the memebrs has cancelled their request. std::string cancelleduuidstr; if (groupchange.getField<17>().size()) { auto [cancelleduuid, cancelleduuid_size] = groupchange.getField<17>()[0]; cancelleduuidstr = bepaald::bytesToHexString(cancelleduuid, cancelleduuid_size, true); cancelleduuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); } if (cancelleduuidstr == uuidstr) { statusmsg += (statusmsg.empty() ? "" : " ") + getNameFromUuid(uuidstr) + " requested and cancelled their request to join via the group link."; } else { // field 16 is (vector of) requesting members (= [uuid, profilekey, timestamp]). // But all that's needed is the uuid and we have that from <1> already statusmsg += (statusmsg.empty() ? "" : " ") + getNameFromUuid(uuidstr) + " requested to join via the group link."; } } // Field 18 'approved member' : added after request if (groupchange.getField<18>().size() && groupchange.getField<1>().has_value()) { auto [uuid, uuid_size] = groupchange.getField<1>().value(); std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); if (icon && *icon == IconType::NONE) *icon = IconType::MEMBER_APPROVED; auto const &approved_members = groupchange.getField<18>(); for (unsigned int i = 0; i < approved_members.size(); ++i) { if (!approved_members[i].getField<1>().has_value()) continue; auto [uuid2, uuid_size2] = approved_members[i].getField<1>().value(); std::string uuidstr2 = bepaald::bytesToHexString(uuid2, uuid_size2, true); uuidstr2.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); std::string newmem = getNameFromUuid(uuidstr2); if (!newmem.empty()) statusmsg += (statusmsg.empty() ? "" : " ") + (Types::isOutgoing(type) ? "You" : getNameFromUuid(uuidstr)) + " approved a request to join the group from " + newmem + "."; } } // field 22 'new banned member' , when combined with 17 ('delete reqeusting members) : request denied if (groupchange.getField<22>().size() && groupchange.getField<1>().has_value()) { auto [uuid, uuid_size] = groupchange.getField<1>().value(); std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); if (icon && *icon == IconType::NONE) *icon = IconType::MEMBER_REJECTED; DecryptedBannedMember decryptedbannedmember = groupchange.getField<22>()[0]; std::string banneduuidstr; if (decryptedbannedmember.getField<1>().has_value()) { auto [banneduuid, banneduuid_size] = decryptedbannedmember.getField<1>().value(); banneduuidstr = bepaald::bytesToHexString(banneduuid, banneduuid_size, true); banneduuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); } // if field 17 'deletereqestingmembers' is also present (and same uuid as reqesting member?) // the members request was denied std::string cancelleduuidstr; if (groupchange.getField<17>().size()) { auto [cancelleduuid, cancelleduuid_size] = groupchange.getField<17>()[0]; cancelleduuidstr = bepaald::bytesToHexString(cancelleduuid, cancelleduuid_size, true); cancelleduuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); } if (cancelleduuidstr == banneduuidstr) { statusmsg += (statusmsg.empty() ? "" : " ") + getNameFromUuid(uuidstr) + " denied a request to join the group from " + getNameFromUuid(banneduuidstr); } else // non-requesting member was banned??? { } } // if groupchange has editor, but nothing else : editor added you to the group: if (groupchange.getField<1>().has_value() && !(groupchange.getField<2>().has_value() || groupchange.getField<3>().size() || groupchange.getField<4>().size() || groupchange.getField<5>().size() || groupchange.getField<6>().size() || groupchange.getField<7>().size() || groupchange.getField<8>().size() || groupchange.getField<9>().size() || groupchange.getField<10>().has_value() || groupchange.getField<11>().has_value() || groupchange.getField<12>().has_value() || groupchange.getField<13>().has_value() || groupchange.getField<14>().has_value() || groupchange.getField<15>().has_value() || groupchange.getField<16>().size() || groupchange.getField<17>().size() || groupchange.getField<18>().size() || groupchange.getField<19>().has_value() || groupchange.getField<20>().has_value() || groupchange.getField<21>().has_value() || groupchange.getField<22>().size() || groupchange.getField<23>().size() || groupchange.getField<24>().size())) { auto [uuid, uuid_size] = groupchange.getField<1>().value(); std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); statusmsg = getNameFromUuid(uuidstr) + " added you to the group."; if (icon && *icon == IconType::NONE) *icon = IconType::MEMBER_ADD; } } // maybe someone was invited, check for pending memebers... if (groupv2ctx.getField<3>().has_value() && groupv2ctx.getField<3>().value().getField<8>().size()) { std::vector> invitedmembers; // for (unsigned int i = 0; i < groupv2ctx.getField<3>().value().getField<8>().size(); ++i) { DecryptedPendingMember pm(groupv2ctx.getField<3>().value().getField<8>()[i]); //pm.print(); if (pm.getField<1>().has_value()) { auto [inv_uuid, inv_uuid_size] = pm.getField<1>().value(); std::string invited_uuidstr = bepaald::toLower(bepaald::bytesToHexString(inv_uuid, inv_uuid_size, true)); invited_uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); std::string invited_by_uuidstr; if (pm.getField<3>().has_value()) { auto [inv_by_uuid, inv_by_uuid_size] = pm.getField<3>().value(); invited_by_uuidstr = bepaald::toLower(bepaald::bytesToHexString(inv_by_uuid, inv_by_uuid_size, true)); invited_by_uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); } invitedmembers.emplace_back(std::pair{invited_uuidstr, invited_by_uuidstr}); } } if (icon && *icon == IconType::NONE) *icon = IconType::MEMBER_ADD; if (invitedmembers.size() > 1) // multiple members were invited { // not done yet } else // one member was invited { if (invitedmembers[0].second == d_selfuuid) // invited by you { std::string invitedname = getNameFromUuid(invitedmembers[0].first); if (!invitedname.empty()) return "You invited " + invitedname + " to the group."; else return "You invited 1 person to the group."; } else if (invitedmembers[0].first == d_selfuuid) // you were the one invited { std::string invitedbyname = getNameFromUuid(invitedmembers[0].second); if (!invitedbyname.empty()) return invitedbyname + " invited you to the group."; else return "You were invited to the group."; } else // someone was invited by someone (but neither were you) { std::string invitedbyname = getNameFromUuid(invitedmembers[0].second); if (!invitedbyname.empty()) return invitedbyname + " invited 1 person to the group."; else return "1 person was invited to the group."; } } } if (statusmsg.empty()) { // std::cout << "" << std::endl; // std::cout << " ********" << std::endl; // std::cout << body << std::endl; // //groupv2ctx.print(); // groupv2ctx.getField<2>().value().print(); // std::cout << " ********" << std::endl; // std::cout << "" << std::endl; return "(group V2 update)"; } return statusmsg; } if (type == Types::GV1_MIGRATION_TYPE) { if (body.empty()) { if (icon && *icon == IconType::NONE) *icon = IconType::MEGAPHONE; return "This group was updated to a New Group."; } std::string b; // parse body, get number of id's before '|': if one //b = "A member couldn't be added to the New Group and has been invited to join." // if N //b = "N members couldn't be added to the New Group and have been invited to join." // parse body, get number of id's after '|': if one //b = "A member couldn't be added to the New Group and has been removed."; // if N //b = "N members couldn't be added to the New Group and have been removed."; unsigned int middlepos = body.find('|'); int membersinvited = std::count(body.begin(), body.begin() + middlepos, ',') + (middlepos == 0 ? 0 : 1); int membersremoved = std::count(body.begin() + middlepos + 1, body.end(), ',') + (middlepos == body.size() - 1 ? 0 : 1); if (membersinvited == 1) b = "A member couldn't be added to the New Group and has been invited to join."; else if (membersinvited > 1) b = bepaald::toString(membersinvited) + " members couldn't be added to the New Group and have been invited to join."; if (membersremoved == 1) b = (b.empty() ? std::string() : "\n") + "A member couldn't be added to the New Group and has been removed."; else if (membersremoved > 1) b = (b.empty() ? "" : "\n") + bepaald::toString(membersremoved) + " members couldn't be added to the New Group and have been removed."; if (membersinvited >= 1 && membersremoved == 0) { if (icon && *icon == IconType::NONE) *icon = IconType::MEMBER_ADD; } else if (membersinvited == 0 && membersremoved >= 1) { if (icon && *icon == IconType::NONE) *icon = IconType::MEMBER_REMOVE; } else // membersinvited >= 1 && membersremoved >= 1, // never seen this... don't know... { if (icon && *icon == IconType::NONE) *icon = IconType::MEGAPHONE; //MEMBERS; } return b; } return body; } std::string SignalBackup::decodeStatusMessage(std::pair, size_t> const &body, long long int expiration, long long int type, std::string const &contactname, IconType *icon) const { // get GroupV2Context from MessageExtras, pass it as a base64string to decodestatusmessage MessageExtras me(body); auto field1 = me.getField<1>(); if (field1.has_value()) // GV2UpdateDescription { auto field1_1 = field1->getField<1>(); if (field1_1.has_value()) { return decodeStatusMessage(field1_1->getDataString(), expiration, type, contactname, icon); } } else { auto field3 = me.getField<3>(); // ProfileChangeDetails if (field3.has_value()) return decodeProfileChangeMessage(field3->getDataString(), contactname); } return std::string(); } /* type & 0x1f == 14 CHANGE_NUMBER_TYPE 689:app/src/main/res/values/strings.xml:1437: %1$s changed their phone number. 701:app/src/main/res/values/strings.xml:3982: Your phone number has been changed to %1$s */ signalbackup-tools-20250313-1/signalbackup/deleteattachments.cc000066400000000000000000000301471476450434500244030ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../attachmentmetadata/attachmentmetadata.h" bool SignalBackup::deleteAttachments(std::vector const &threadids, std::string const &before, std::string const &after, long long int filesize, std::vector const &mimetypes, std::string const &append, std::string const &prepend, std::vector> replace) { std::string query_delete("DELETE FROM " + d_part_table); std::string query_list("SELECT _id," + d_part_mid + "," + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id" : "-1 AS unique_id") + "," + d_part_ct + ",quote FROM " + d_part_table); std::string specification; if (!threadids.empty() && threadids[0] != -1) // -1 indicates 'ALL', easier not to specify { if (specification.empty()) specification += " WHERE " + d_part_mid + " IN (SELECT _id FROM " + d_mms_table + " WHERE "; else specification += " AND "; for (unsigned int i = 0; i < threadids.size(); ++i) specification += ((i == 0) ? "("s : ""s) + "thread_id IS " + bepaald::toString(threadids[i]) + ((i == threadids.size() - 1) ? ")" : " OR "); } if (!before.empty()) { long long int date = dateToMSecsSinceEpoch(before); if (date == -1) Logger::warning("Ignoring before-date: '", before, "'. Failed to parse or invalid."); else { if (specification.empty()) specification += " WHERE " + d_part_mid + " IN (SELECT _id FROM " + d_mms_table + " WHERE "; else specification += " AND "; specification += "date_received < " + bepaald::toString(date); } } if (!after.empty()) { long long int date = dateToMSecsSinceEpoch(after); if (date == -1) Logger::warning("Ignoring after-date: '", after, "'. Failed to parse or invalid."); else { if (specification.empty()) specification += " WHERE " + d_part_mid + " IN (SELECT _id FROM " + d_mms_table + " WHERE "; else specification += " AND "; specification += "date_received > " + bepaald::toString(date); } } if (!specification.empty()) specification += ")"; if (filesize >= 0) { if (specification.empty()) specification += " WHERE "; else specification += " AND "; specification += "data_size > " + bepaald::toString(filesize); } if (!mimetypes.empty()) { if (specification.empty()) specification += " WHERE "; else specification += " AND "; for (unsigned int i = 0; i < mimetypes.size(); ++i) specification += ((i == 0) ? "("s : ""s) + d_part_ct + " LIKE \"" + mimetypes[i] + "%\"" + ((i == mimetypes.size() - 1) ? ")" : " OR "); } query_delete += specification; query_list += specification; //std::cout << query_list << std::endl; SqliteDB::QueryResults res; if (!d_database.exec(query_list, &res)) { Logger::error("Failed to execute query."); return false; } //res.prettyPrint(); // just delete the attachments if (replace.empty()) { if (!d_database.exec(query_delete)) { Logger::error("Failed to execute query."); return false; } Logger::message("Deleted: ", d_database.changed(), " 'part'-entries."); // find all mms entries to which the deleted attachments belonged // if no attachments remain and body is empty -> delete mms -> cleanDatabaseByMessages() SqliteDB::QueryResults res2; long long int count = 0; for (unsigned int i = 0; i < res.rows(); ++i) { if (!append.empty() || !prepend.empty()) { // update existing message bodies if (!append.empty()) { if (!d_database.exec("UPDATE " + d_mms_table + " SET body = body || ? WHERE _id = ? AND (body IS NOT NULL AND body != '')", {"\n\n" + append, res.getValueAs(i, d_part_mid)})) return false; if (!d_database.exec("UPDATE " + d_mms_table + " SET body = ? WHERE _id = ? AND (body IS NULL OR body == '')", {append, res.getValueAs(i, d_part_mid)})) return false; } if (!prepend.empty()) { // update message ranges if present: std::pair, size_t> brdata = d_database.getSingleResultAs, size_t>>("SELECT " + d_mms_ranges + " FROM " + d_mms_table + " WHERE LENGTH(" + d_mms_ranges + ") != 0 AND _id = ?", res.getValueAs(i, d_part_mid), {nullptr, 0}); if (brdata.second) { BodyRanges brsproto(brdata); //brsproto.print(); BodyRanges new_bodyrange_vec; auto bodyrange_vec = brsproto.getField<1>(); for (auto const &bodyrange : bodyrange_vec) { BodyRange new_bodyrange = bodyrange; int start = new_bodyrange.getField<1>().value_or(-1); if (start != -1) [[likely]] { new_bodyrange.deleteFields(1); new_bodyrange.addField<1>(start + prepend.size() + 2); // plus two for the extra new lines //new_bodyrange.print(); } new_bodyrange_vec.addField<1>(new_bodyrange); } if (!d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_ranges + " = ? WHERE _id = ?", {std::make_pair(new_bodyrange_vec.data(), static_cast(new_bodyrange_vec.size())), res.getValueAs(i, d_part_mid)})) return false; } // update mentions if present d_database.exec("UPDATE mention SET range_start = range_start + ? WHERE message_id = ?", {prepend.size() + 2, res.getValueAs(i, d_part_mid)}); if (d_verbose) [[unlikely]] Logger::message("Updated ", d_database.changed(), " mention to adjust for prependbody"); if (!d_database.exec("UPDATE " + d_mms_table + " SET body = ? || body WHERE _id = ? AND (body IS NOT NULL AND body != '')", {prepend + "\n\n", res.getValueAs(i, d_part_mid)})) return false; if (!d_database.exec("UPDATE " + d_mms_table + " SET body = ? WHERE _id = ? AND (body IS NULL OR body == '')", {prepend, res.getValueAs(i, d_part_mid)})) return false; } } else // no append/prepend -> delete message if body empty { // check if message has other attachments if (!d_database.exec("SELECT _id FROM " + d_part_table + " WHERE " + d_part_mid + " = ?", res.getValueAs(i, d_part_mid), &res2)) return false; if (res2.empty()) // no other attachments for this message, we can delete if body is empty { //std::cout << "no other attachments (" << i << ")" << std::endl; // delete message if body empty if (!d_database.exec("DELETE FROM " + d_mms_table + " WHERE _id = ? AND (body IS NULL OR body == '')", res.getValueAs(i, d_part_mid))) return false; if (d_database.changed() == 1) { ++count; continue; } } } } if (count || (append.empty() && prepend.empty())) Logger::message("Deleted ", count, " empty messages"); // make sure the 'mms.previews' column does not reference non-existing attachments long long int maxlinkpreviews = d_database.getSingleResultAs("SELECT MAX(json_array_length(" + d_mms_previews + ")) FROM " + d_mms_table, 0); for (unsigned int lp = 0; lp < maxlinkpreviews; ++lp) { //d_database.print("SELECT " + d_mms_previews + " FROM " + d_mms_table + " WHERE " + d_mms_previews + " IS NOT NULL"); if (d_database.tableContainsColumn(d_part_table, "unique_id")) d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_previews + " = json_set(" + d_mms_previews + ", '$[" + bepaald::toString(lp) + "].attachmentId', json(null)) WHERE " "json_extract(" + d_mms_previews + ", '$[" + bepaald::toString(lp) + "].attachmentId.rowId') NOT IN (SELECT _id FROM " + d_part_table + ") AND " "json_extract(" + d_mms_previews + ", '$[" + bepaald::toString(lp) + "].attachmentId.uniqueId') NOT IN (SELECT unique_id FROM " + d_part_table +")"); else d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_previews + " = json_set(" + d_mms_previews + ", '$[" + bepaald::toString(lp) + "].attachmentId', json(null)) WHERE " "json_extract(" + d_mms_previews + ", '$[" + bepaald::toString(lp) + "].attachmentId.rowId') NOT IN (SELECT _id FROM " + d_part_table + ")"); //d_database.print("SELECT " + d_mms_previews + " FROM " + d_mms_table + " WHERE " + d_mms_previews + " IS NOT NULL"); } cleanAttachments(); cleanDatabaseByMessages(); return true; } // else replace attachments std::sort(replace.begin(), replace.end(), [](std::pair const &lhs, std::pair const &rhs) { return (lhs.first == "default" ? false : (rhs.first == "default" ? true : lhs.first.length() > rhs.first.length())); }); for (unsigned int i = 0; i < res.rows(); ++i) { // get ct (mimetype), if it matches replace[i].first -> replace with replace[i].second std::string mimetype = res.valueAsString(i, d_part_ct); Logger::message("Checking to replace attachment: ", mimetype); for (unsigned int j = 0; j < replace.size(); ++j) { if (res.valueAsString(i, d_part_ct).find(replace[j].first) == 0 || replace[j].first == "default") { // replace with replace[j].second auto attachment = d_attachments.find({res.getValueAs(i, "_id"), res.getValueAs(i, "unique_id")}); if (attachment == d_attachments.end()) { Logger::warning("Failed to find attachment with this part entry"); continue; } AttachmentMetadata amd = AttachmentMetadata::getAttachmentMetaData(replace[j].second); if (!amd) { Logger::message("Failed to get metadata on new attachment: \"", replace[j].second, "\", skipping..."); continue; } if (!updatePartTableForReplace(amd, res.getValueAs(i, "_id"))) //if (!d_database.exec("UPDATE part SET ct = ?, data_size = ?, width = ?, height = ?, data_hash = ? WHERE _id = ?", // {newmimetype, newdatasize, newwidth, newheight, newhash, res.getValueAs(i, "_id")}) || // d_database.changed() != 1) return false; attachment->second->setLength(amd.filesize); //attachment->second->setLazyDataRAW(amd.filesize, replace[j].second); attachment->second->setReader(new RawFileAttachmentReader(replace[j].second)); Logger::message("Replaced attachment at ", i + 1, "/", res.rows(), " with file \"", replace[j].second, "\""); break; } } // replacing -> delete where _id = res[i][_id] // insert into (_id, mid, unique_id, ct, data_size, quote, ...) values(..., ... ,) } return true; } signalbackup-tools-20250313-1/signalbackup/dropbadframes.cc000066400000000000000000000037761476450434500235260ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::dropBadFrames() { if (d_badattachments.empty()) return true; Logger::message("Removing ", d_badattachments.size(), " bad frames from database..."); for (auto it = d_badattachments.begin(); it != d_badattachments.end(); ) { uint32_t rowid = it->first; SqliteDB::QueryResults results; std::string query = "SELECT " + d_part_mid + " FROM " + d_part_table + " WHERE _id = " + bepaald::toString(rowid); if (d_database.tableContainsColumn(d_part_table, "unique_id")) query += " AND unique_id = " + bepaald::toString(it->second); long long int mid = -1; d_database.exec(query, &results); for (unsigned int i = 0; i < results.rows(); ++i) for (unsigned int j = 0; j < results.columns(); ++j) if (results.valueHasType(i, j)) if (results.header(j) == d_part_mid) { mid = results.getValueAs(i, j); break; } if (mid == -1) { Logger::error("Failed to remove frame :( Could not find matching 'part' entry"); return false; } d_database.exec("DELETE FROM " + d_part_table + " WHERE " + d_part_mid + " = " + bepaald::toString(mid)); d_badattachments.erase(it); } return true; } signalbackup-tools-20250313-1/signalbackup/dtcreaterecipient.cc000066400000000000000000000603321476450434500244020ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" long long int SignalBackup::dtCreateRecipient(SqliteDB const &ddb, std::string const &id, std::string const &phone, std::string const &groupidb64, std::string const &databasedir, std::map *recipient_info, bool create_valid_contacts, bool *was_warned) { std::string printable_uuid(makePrintable(id)); Logger::message("Creating new recipient for id: ", printable_uuid); SqliteDB::QueryResults res; if (!ddb.exec("SELECT " "type, TRIM(name) AS name, profileName, profileFamilyName, " "profileFullName, e164, " + d_dt_c_uuid + " AS uuid, json_extract(conversations.json,'$.color') AS color, " "COALESCE(json_extract(conversations.json, '$.profileAvatar.path'), json_extract(conversations.json, '$.avatar.path')) AS avatar, " // 'profileAvatar' for persons, 'avatar' for groups "IFNULL(COALESCE(json_extract(conversations.json, '$.profileAvatar.localKey'), json_extract(conversations.json, '$.avatar.localKey')), '') AS localKey, " "IFNULL(COALESCE(json_extract(conversations.json, '$.profileAvatar.size'), json_extract(conversations.json, '$.avatar.size')), 0) AS size, " "IFNULL(COALESCE(json_extract(conversations.json, '$.profileAvatar.version'), json_extract(conversations.json, '$.avatar.version')), 0) AS version, " "groupId, IFNULL(json_extract(conversations.json,'$.groupId'),'') AS 'json_groupId', " "IFNULL(json_extract(conversations.json, '$.expireTimer'), 0) AS 'expireTimer', " "IFNULL(json_extract(conversations.json, '$.expireTimerVersion'), 1) AS 'expireTimerVersion', " "json_extract(conversations.json, '$.storageID') AS 'storageId', " "json_extract(conversations.json, '$.pni') AS 'pni', " "IFNULL(json_extract(conversations.json, '$.profileSharing'), '0') AS 'profileSharing', " "json_extract(conversations.json, '$.firstUnregisteredAt') AS 'firstUnregisteredAt', " "IFNULL(json_extract(conversations.json, '$.sealedSender'), 0) AS 'sealedSender', " "json_extract(identityKeys.json, '$.publicKey') AS 'publicKey', " "IFNULL(json_extract(identityKeys.json, '$.verified'), 0) AS 'verified', " "IFNULL(json_extract(identityKeys.json, '$.firstUse'), 0) AS 'firstUse', " "IFNULL(json_extract(identityKeys.json, '$.timestamp'), 0) AS 'timestamp', " "IFNULL(json_extract(identityKeys.json, '$.nonblockingApproval'), 0) AS 'nonblockingApproval', " "IFNULL(json_extract(conversations.json,'$.groupVersion'), 1) AS groupVersion, " "NULLIF(json_extract(conversations.json,'$.nicknameGivenName'), '') AS nick_first, " "NULLIF(json_extract(conversations.json,'$.nicknameFamilyName'), '') AS nick_last, " "TOKENCOUNT(members) AS nummembers, json_extract(conversations.json, '$.masterKey') AS masterKey " "FROM conversations " "LEFT JOIN identityKeys ON conversations." + d_dt_c_uuid + " = identityKeys.id " "WHERE " + d_dt_c_uuid + " = ? OR e164 = ? OR groupId = ? OR (SUBSTR(?, 1, 3) == 'pni' AND pni = ?)", {id, phone, groupidb64, id, id}, &res)) { // std::cout << bepaald::bold_on << "Error" << bepaald::bold_off << ": ." << std::endl; return -1; } //res.prettyPrint(d_truncate); if (res.rows() != 1) { if (res.rows() > 1) Logger::error("Unexpected number of results getting new recipient data."); else // = 0 Logger::error("No results trying to get new recipient data."); return -1; } if (*was_warned == false) { Logger::warning("Chat partner was not found in recipient-table. Attempting to create."); Logger::warning_indent(Logger::Control::BOLD, "NOTE THE RESULTING BACKUP CAN MOST LIKELY NOT BE RESTORED"); Logger::warning_indent("ON SIGNAL ANDROID. IT IS ONLY MEANT TO EXPORT TO HTML.", Logger::Control::NORMAL); *was_warned = true; } if (res("type") == "group") { if (res.getValueAs(0, "groupVersion") < 2) { // group v1 not yet.... Logger::error("Old style groups (v1) not supported for creation..."); return -1; } std::pair groupid_data = Base64::base64StringToBytes(res("json_groupId")); if (!groupid_data.first || groupid_data.second == 0) // json data was not valid base64 string, lets try the other one groupid_data = Base64::base64StringToBytes(res("groupId")); if (!groupid_data.first || groupid_data.second == 0) { // maybe, just create out own id here? we don't care Logger::error("Failed to get group_id"); return -1; } std::string group_id = "__signal_group__v2__!" + bepaald::bytesToHexString(groupid_data, true); bepaald::destroyPtr(&groupid_data.first, &groupid_data.second); if (res("name").empty()) { Logger::warning("Group name of new recipient is empty. Here is the data from the Desktop db:"); ddb.printLineMode("SELECT * FROM conversations WHERE " + d_dt_c_uuid + " = ? OR e164 = ? OR groupId = ?", {id, phone, groupidb64}); } d_database.exec("BEGIN TRANSACTION"); // things could still go bad... std::any new_rid; if (!insertRow("recipient", {{"group_id", group_id}, {d_recipient_type, 3}, // group type {"storage_service_id", res.value(0, "storageId")}, {"message_expiration_time_version", res.value(0, "expireTimerVersion")}, {"message_expiration_time", res.value(0, "expireTimer")}, {d_recipient_avatar_color, res.value(0, "color")}}, "_id", &new_rid)) { Logger::error("Failed to insert new (group) recipient into database."); return -1; } if (new_rid.type() != typeid(long long int)) { Logger::error("New (group) recipient _id has unexpected type."); return -1; } long long int new_rec_id = std::any_cast(new_rid); std::pair masterkey = Base64::base64StringToBytes(res("masterKey")); if (!insertRow("groups", {{"title", res.value(0, "name")}, {"group_id", group_id}, {"recipient_id", new_rec_id}, {"avatar_id", 0}, {"master_key", masterkey}, // {"decrypted_group",}, // {"distribution_id",}, {"revision", 0}})) { Logger::error("Failed to insert new group into database."); d_database.exec("ROLLBACK TRANSACTION"); bepaald::destroyPtr(&masterkey.first, &masterkey.second); return -1; } bepaald::destroyPtr(&masterkey.first, &masterkey.second); // get group members: std::string oldstyle_members; // I suspect members can occur double in the list? or maybe my tokenizer is no good?, this is just to check std::set members_processed; // the actual UUID of the member returned by getRecipientIdFromUuidMapped may differ from the input id, since it may // exist in the Android database under a different uuid. Normally the database only referes to recipient._id so it's // no problem, but member roles use the uuid, so we need the correct one... std::map member_uuids; // [ ddb_uuid -> android_uuid ] for (unsigned int i = 0; i < res.getValueAs(0, "nummembers"); ++i) { SqliteDB::QueryResults mem; if (!ddb.exec("SELECT members,TOKEN(members, ?) AS member FROM conversations WHERE groupId = ?", {i, groupidb64}, &mem)) continue; //std::cout << "Got members: " << mem("member") << std::endl; if (bepaald::contains(members_processed, mem("member"))) { Logger::warning("Asked to process same member again. Skipping."); Logger::warning_indent("Here is is raw members-list:"); Logger::warning_indent("'", mem("members"), "'"); continue; } members_processed.insert(mem("member")); long long int member_rid = getRecipientIdFromUuidMapped(mem("member"), recipient_info, was_warned); if (member_rid == -1) { if (d_verbose) [[unlikely]] Logger::message("Creating group member..."); member_rid = dtCreateRecipient(ddb, mem("member"), std::string(), std::string(), databasedir, recipient_info, create_valid_contacts, was_warned); if (member_rid == -1) { Logger::error("Failed to get new groups members uuid."); d_database.exec("ROLLBACK TRANSACTION"); return -1; } } // save the actual uuid of this recipient in map std::string android_uuid = d_database.getSingleResultAs("SELECT " + d_recipient_aci + " FROM recipient WHERE _id = ?", member_rid, std::string()); if (!android_uuid.empty()) member_uuids[mem("member")] = android_uuid; if (d_database.containsTable("group_membership")) { if (!insertRow("group_membership", {{"group_id", group_id}, {"recipient_id", member_rid}})) { Logger::error("Failed to set new groups membership."); d_database.exec("ROLLBACK TRANSACTION"); return -1; } } else oldstyle_members += ((oldstyle_members.empty() ? "" : ",") + bepaald::toString(member_rid)); } // set old-style members if (d_database.tableContainsColumn("groups", "members")) { if (!d_database.exec("UPDATE groups SET members = ? WHERE _id = ?", {oldstyle_members, new_rec_id})) { Logger::error("Failed to set new groups membership (old style)."); d_database.exec("ROLLBACK TRANSACTION"); return -1; } } // set member roles: if (d_database.containsTable("group_membership") && d_database.tableContainsColumn("groups", "decrypted_group")) { std::map memberroles; // [ uuid -> Role ] for (unsigned int i = 0; i < res.getValueAs(0, "nummembers"); ++i) { // get member role (0 = unknown, 1 = normal, 2 = admin) SqliteDB::QueryResults memberrole_results; if (ddb.exec("SELECT json_extract(json, '$.membersV2[" + bepaald::toString(i) + "].role') AS role, " "COALESCE(json_extract(json, '$.membersV2[" + bepaald::toString(i) + "].aci'), json_extract(json, '$.membersV2[" + bepaald::toString(i) + "].uuid')) AS uuid " "FROM conversations WHERE groupId = ?", groupidb64, &memberrole_results)) { //memberrole_results.prettyPrint(); for (unsigned int mr = 0; mr < memberrole_results.rows(); ++mr) { if (!memberrole_results.valueHasType(mr, "role") || !memberrole_results.valueHasType(mr, "uuid")) continue; std::string uuid = bepaald::contains(member_uuids, memberrole_results(mr, "uuid")) ? member_uuids[memberrole_results(mr, "uuid")] : memberrole_results(mr, "uuid"); // check if uuid is an actual member: long long int uuidpresent = d_database.getSingleResultAs("SELECT COUNT(*) FROM group_membership WHERE " "recipient_id IS (SELECT _id FROM recipient WHERE " + d_recipient_aci + " = ?) AND " "group_id = ?", {uuid, group_id}, 0); if (uuidpresent) memberroles[uuid] = memberrole_results.getValueAs(mr, "role"); } } } // now create a GroupV2Context and add it /* message DecryptedGroup { string title = 2; string avatar = 3; DecryptedTimer disappearingMessagesTimer = 4; AccessControl accessControl = 5; uint32 revision = 6; repeated DecryptedMember members = 7; repeated DecryptedPendingMember pendingMembers = 8; repeated DecryptedRequestingMember requestingMembers = 9; bytes inviteLinkPassword = 10; string description = 11; EnabledState isAnnouncementGroup = 12; repeated DecryptedBannedMember bannedMembers = 13; } */ std::string title = res("name"); // if available // std::string description = res("description"); DecryptedGroup group_info; group_info.addField<2>(title); //group_info.print(); for (auto const &m : memberroles) { /* message DecryptedMember { bytes uuid = 1; Member.Role role = 2; bytes profileKey = 3; uint32 joinedAtRevision = 5; bytes pni = 6; } enum Role { UNKNOWN = 0; DEFAULT = 1; ADMINISTRATOR = 2; } */ DecryptedMember mem; unsigned char rawuuid[16]; uint64_t rawuuid_size = 16; if (bepaald::hexStringToBytes(m.first, rawuuid, rawuuid_size)) { mem.addField<1>({rawuuid, rawuuid_size}); mem.addField<2>(m.second); group_info.addField<7>(mem); //group_info.print(); } } // add it std::pair groupdetails = {group_info.data(), group_info.size()}; d_database.exec("UPDATE groups SET decrypted_group = ? WHERE recipient_id = ?", {groupdetails, new_rid}); } d_database.exec("COMMIT TRANSACTION"); (*recipient_info)[groupidb64] = new_rec_id; // set avatar if (!dtSetAvatar(res("avatar"), res("localKey"), res.valueAsInt(0, "size"), res.valueAsInt(0, "version"), new_rec_id, databasedir)) Logger::warning("Failed to set avatar for new recipient."); Logger::message("Successfully created new recipient for group (id: ", new_rec_id, ")."); return new_rec_id; //-1; } // type != group : long long int new_rec_id = -1; if (res("profileName").empty() && res("profileFamilyName").empty() && res("profileFullName").empty() && res("e164").empty() && res("uuid").empty()) { Logger::warning("All relevant info on new recipient is empty. Here is the data from the Desktop db:"); ddb.printLineMode("SELECT * FROM conversations WHERE " + d_dt_c_uuid + " = ? OR e164 = ? OR groupId = ?", {id, phone, groupidb64}); } // it is possible the contacts exists already, but not as a valid Signal contact (with uuid and keys) // (maybe should check for username and email as well, both are also unique in the recipient table) SqliteDB::QueryResults existing_recipient; if (!d_database.exec("SELECT _id, " + d_recipient_aci + " FROM recipient WHERE pni = ? COLLATE NOCASE OR " + d_recipient_e164 + " = ?", {res.value(0, "pni"), res.value(0, "e164")}, &existing_recipient)) return -1; if (existing_recipient.rows() > 1) { Logger::error("Unexpected number of results for query (existing_recipient"); return -1; } if (existing_recipient.rows() == 1) // update existing recipient { long long int existing_recipient_id = existing_recipient.getValueAs(0, "_id"); std::string existing_recipient_uuid = existing_recipient(d_recipient_aci); // if the existing recipient already has a uuid and a indentity key, just use it??? if (!existing_recipient_uuid.empty()) { if (d_database.getSingleResultAs("SELECT _id FROM identities WHERE address = ? AND identity_key IS NOT NULL", existing_recipient_uuid, -1) != -1) { Logger::message("Found existing valid contact under different uuid [", printable_uuid, " -> ", makePrintable(existing_recipient_uuid), "] " "(id: ", existing_recipient_id, ")."); (*recipient_info)[id.empty() ? phone : id] = existing_recipient_id; return existing_recipient_id; } else // the existing contact does not have a valid identity_key { if (!create_valid_contacts) // ...but we don't care { (*recipient_info)[id.empty() ? phone : id] = existing_recipient_id; return existing_recipient_id; } else { Logger::error("Contact already exists with a different uuid, but no valid identity key. not sure what to do here yet..."); return -1; } } } else // contact uuid == NULL in Android db, lets update it with Desktop data { if (!d_database.exec("UPDATE recipient SET " + d_recipient_aci + " = ?, " + d_recipient_e164 + " = COALESCE(" + d_recipient_e164 + ", ?), " + "pni = COALESCE(pni, ?), " "storage_service_id = COALESCE(storage_service_id, ?), " "registered = ? " "WHERE _id = ?", {res.value(0, "uuid"), res.value(0, "e164"), res.value(0, "pni"), res.value(0, "storageId"), res.isNull(0, "firstUnregisteredAt") ? 1 : 0, existing_recipient_id})) return -1; Logger::message("Found existing contact without uuid, Updating... (id: ", existing_recipient_id, ")."); new_rec_id = existing_recipient_id; } } else // insert new recipient { std::any new_rid; if (!insertRow("recipient", {{d_recipient_profile_given_name, res.value(0, "profileName")}, {"profile_family_name", res.value(0, "profileFamilyName")}, {"profile_joined_name", res.value(0, "profileFullName")}, {"nickname_given_name", res.value(0, "nick_first")}, {"nickname_family_name", res.value(0, "nick_last")}, {(!res.isNull(0, "nick_first") || !res.isNull(0, "nick_last")) ? "nickname_joined_name" : "", (res(0, "nick_first").empty() ? res(0, "nick_last") : (res(0, "nick_last").empty() ? res(0, "nick_first") : res(0, "nick_first") + " " + res(0, "nick_last")))}, {d_recipient_e164, res.value(0, "e164")}, {d_recipient_aci, res.value(0, "uuid")}, {"pni", res.value(0, "pni")}, {"message_expiration_time_version", res.value(0, "expireTimerVersion")}, {"message_expiration_time", res.value(0, "expireTimer")}, {"storage_service_id", res.value(0, "storageId")}, {"profile_sharing", res.value(0, "profileSharing")}, {"registered", res.isNull(0, "firstUnregisteredAt") ? 1 : 0}, // registered if no Unregister-timestamp is found, unknown otherwise {d_recipient_sealed_sender, res.value(0, "sealedSender")}, // {d_database.tableContainsColumn("recipient", "blocked") ? // blocked recipients do not exist in Desktop? // "blocked" : "", res.value(0, "blocked")}, {d_recipient_avatar_color, res.value(0, "color")}}, "_id", &new_rid)) { Logger::error("Failed to insert new recipient into database."); return -1; } if (new_rid.type() != typeid(long long int)) { Logger::error("New recipient _id has unexpected type."); d_database.exec("DELETE FROM recipient WHERE _id = ?", new_rid); return -1; } new_rec_id = std::any_cast(new_rid); // set avatar dtSetAvatar(res("avatar"), res("localKey"), res.valueAsInt(0, "size"), res.valueAsInt(0, "version"), new_rec_id, databasedir); } std::string identity_key = res(0, "publicKey"); if (identity_key.empty() && create_valid_contacts) { Logger::warning("No publicKey found for new recipient, inserting fake key..."); identity_key = "BUZBS0VLRVkgRkFLRUtFWSBGQUtFS0VZIEZBS0VLRVkh"; /// keys always start with 0x05 for some reason... // $ echo "BUZBS0VLRVkgRkFLRUtFWSBGQUtFS0VZIEZBS0VLRVkh" | base64 -d | xxd -g 1 -c 33 // 00000000: 05 46 41 4b 45 4b 45 59 20 46 41 4b 45 4b 45 59 20 46 41 4b 45 4b 45 59 20 46 41 4b 45 4b 45 59 21 .FAKEKEY FAKEKEY FAKEKEY FAKEKEY! } // set identity info if (!res.isNull(0, "uuid")) { if (!insertRow("identities", {{"address", res.value(0, "uuid")}, {"identity_key", identity_key}, {"first_use", res("firstUse")}, {"timestamp", res.value(0, "timestamp")}, {"verified", res.value(0, "verified")}, {"nonblocking_approval", res("nonblockingApproval")}})) { if (create_valid_contacts) { Logger::error("Failed to insert identity key for newly created recipient entry."); d_database.exec("DELETE FROM recipient WHERE _id = ?", new_rec_id); return -1; } else Logger::warning("Failed to insert identity key for newly created recipient entry."); } else Logger::message("Successfully updated identity-key for contact (", new_rec_id, ", ", makePrintable(res("uuid")), ")"); } else { if (create_valid_contacts) { Logger::error("Newly created contact has no UUID"); d_database.exec("DELETE FROM recipient WHERE _id = ?", new_rec_id); return -1; } else Logger::warning("Newly created contact has no UUID"); } Logger::message("Successfully created new recipient (id: ", new_rec_id, ")."); //d_database.printLineMode("SELECT * FROM recipient WHERE _id = ?", new_rec_id); (*recipient_info)[id.empty() ? phone : id] = new_rec_id; return new_rec_id; } /* ANDROID system_display_name = signal_profile_name = John profile_family_name = Doe profile_joined_name = John Doe title = phone = +316xxxxxxxx uuid = 9372xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx username = color = A110 DESKTOP type = private profileName = John profileFamilyName = Doe profileFullName = John Doe e164 = +316xxxxxxxx uuid = 9372xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx json'$.color' = A130 ANDROID system_display_name = profile_joined_name = signal_profile_name = title = Test group phone = uuid = username = color = A120 group_id = __signal_group__v2__!e57ccxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx DESKTOP type = group name = Test group profileName = profileFamilyName = profileFullName = e164 = uuid = json'$.color' = A130 groupId = 5XzCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx members = 0d70xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 9372xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 09efxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ??? (note: $ echo 5XzCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | base64 -d | xxd -plain -c 0 e57cxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) */ signalbackup-tools-20250313-1/signalbackup/dtimportlongtext.cc000066400000000000000000000100511476450434500243240ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include void SignalBackup::dtImportLongText(std::string const &msgbody_full, long long int new_mms_id, long long int uniqueid) { if (d_verbose) [[unlikely]] Logger::message_start("Insert LONG_TEXT attachment..."); // gethash std::string hash; unsigned char rawhash[SHA256_DIGEST_LENGTH]; bool fail = true; std::unique_ptr sha256(EVP_MD_CTX_new(), &::EVP_MD_CTX_free); if (sha256.get() && EVP_DigestInit_ex(sha256.get(), EVP_sha256(), nullptr) == 1) { fail = false; if (EVP_DigestUpdate(sha256.get(), msgbody_full.c_str(), msgbody_full.length()) != 1) fail = true; fail |= (EVP_DigestFinal_ex(sha256.get(), rawhash, nullptr) != 1); } hash = fail ? std::string() : Base64::bytesToBase64String(rawhash, SHA256_DIGEST_LENGTH); std::string filename = bepaald::toDateString(uniqueid / 1000, "signal-%Y-%m-%d-%H%M%S.txt"); //std::cout << "SIZE: " << msgbody_full.length() << std::endl; //std::cout << "HASH: " << hash << std::endl; //std::cout << "FILENAME: " << filename << std::endl; std::any new_part_id_ret; if (!insertRow(d_part_table, {{d_part_mid, new_mms_id}, {d_part_ct, "text/x-signal-plain"}, {d_part_pending, 0}, {"data_size", msgbody_full.length()}, {(d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id" : ""), uniqueid}, {(d_database.tableContainsColumn(d_part_table, "data_hash") ? "data_hash" : ""), hash}, {(d_database.tableContainsColumn(d_part_table, "data_hash_start") ? "data_hash_start" : ""), hash}, {(d_database.tableContainsColumn(d_part_table, "data_hash_end") ? "data_hash_end" : ""), hash}, {"upload_timestamp", uniqueid}, //{"cdn_number", results_attachment_data.value(0, "cdn_number")}, // will be 0 on sticker, usually 0 or 2, but I dont know what it means {"file_name", filename}}, "_id", &new_part_id_ret)) Logger::error("Failed to set LONG_TEXT attachment"); else // set actual attachment data { long long int new_part_id = std::any_cast(new_part_id_ret); DeepCopyingUniquePtr new_attachment_frame; if (setFrameFromStrings(&new_attachment_frame, std::vector{"ROWID:uint64:" + bepaald::toString(new_part_id), (d_database.tableContainsColumn(d_part_table, "unique_id") ? "ATTACHMENTID:uint64:" + bepaald::toString(uniqueid) : ""), "LENGTH:uint32:" + bepaald::toString(msgbody_full.length())})) { new_attachment_frame->setAttachmentDataUnbacked(reinterpret_cast(msgbody_full.c_str()), msgbody_full.length()); d_attachments.emplace(std::make_pair(new_part_id, d_database.tableContainsColumn(d_part_table, "unique_id") ? uniqueid : -1), new_attachment_frame.release()); } else { Logger::error("Failed to create AttachmentFrame for data"); // try to remove the inserted part entry: d_database.exec("DELETE FROM " + d_part_table + " WHERE _id = ?", new_part_id); } } if (d_verbose) [[unlikely]] Logger::message_end("done"); } signalbackup-tools-20250313-1/signalbackup/dtimportstickerpacks.cc000066400000000000000000000243421476450434500251560ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::dtImportStickerPacks(SqliteDB const &ddb, std::string const &databasedir) { if (!d_database.containsTable("sticker") || !ddb.containsTable("sticker_packs") || !ddb.containsTable("stickers")) { Logger::error("Database does not contain expected sticker tables"); return false; } // get all stickerpacks SqliteDB::QueryResults dtstickerpacks; ddb.exec("SELECT id, key, author, coverStickerId, title, status FROM sticker_packs", &dtstickerpacks); for (unsigned int i = 0; i < dtstickerpacks.rows(); ++i) { // check if this pack is already installed in the backup... std::string dtpackid = dtstickerpacks(i, "id"); if (d_database.getSingleResultAs("SELECT COUNT(*) FROM sticker WHERE installed = 1 AND pack_id IS ?", dtpackid, -1) > 0) { if (d_verbose) [[unlikely]] Logger::message("Skipping stickerpack '", dtpackid, "': Already installed"); continue; } // if it is not installed (but 'known'), check if it is known to Android (and skip if so) long long int dt_installed = dtstickerpacks(i, "status") == "installed"; if (dt_installed == 0 && d_database.getSingleResultAs("SELECT COUNT(*) FROM sticker WHERE pack_id IS ?", dtpackid, -1) > 0) { if (d_verbose) [[unlikely]] Logger::message("Skipping stickerpack '", dtpackid, "': Already installed"); continue; } // the pack may not be installed, but may be known, in which case, the cover already exists... // when this is the case, the cover can be skipped on import (it's already there) and the 'installed' // status must be updated instead. long long int known_backup_id = d_database.getSingleResultAs("SELECT _id FROM sticker WHERE installed = 0 AND pack_id IS ?", dtpackid, -1); std::string dtkey = dtstickerpacks(i, "key"); std::pair dtkey_data = Base64::base64StringToBytes(dtkey); dtkey = bepaald::bytesToHexString(dtkey_data, true); bepaald::destroyPtr(&dtkey_data.first, &dtkey_data.second); std::string dtauthor = dtstickerpacks(i, "author"); std::string dttitle = dtstickerpacks(i, "title"); long long int dtcoversticker = dtstickerpacks.valueAsInt(i, "coverStickerId"); SqliteDB::QueryResults dtstickers; std::string stickerquery = "SELECT id, emoji, isCoverOnly, path"; if (ddb.tableContainsColumn("stickers", "version", "localKey", "size")) [[likely]] stickerquery += ", version, localKey, size"; else stickerquery += ", 0 AS version, '' AS localKey, 0 AS size"; stickerquery += " FROM stickers WHERE packId = ?"; ddb.exec(stickerquery + (dt_installed == 0 ? " AND id = " + bepaald::toString(dtcoversticker) : "") , dtpackid, &dtstickers); if (dtstickers.rows() == 0) continue; if (d_verbose) [[unlikely]] Logger::message("Importing ", dtstickers.rows(), " stickers from stickerpack ", dtpackid, " (key: ", dtkey, ")"); for (unsigned int j = 0; j < dtstickers.rows(); ++j) { long long int dtcoveronly = dtstickers.valueAsInt(j, "isCoverOnly"); long long int dtstickerid = dtstickers.valueAsInt(j, "id"); std::string dtemoji = dtstickers(j, "emoji"); uint64_t filelength = dtstickers.valueAsInt(j, "size"); long long int version = dtstickers.valueAsInt(j, "version"); std::string localkey = dtstickers(j, "localKey"); std::string fullpath(databasedir + "/stickers.noindex/" + dtstickers(j, "path")); // get filelength if not in database if (filelength <= 0) //{ //std::ifstream dtstickerfile(fullpath, std::ios_base::binary | std::ios_base::in); //if (!dtstickerfile.is_open()) //{ // Logger::error("Error opening Desktop sticker at path '", fullpath, "'. Skipping..."); // continue; //} //dtstickerfile.seekg(0, std::ios_base::end); //filelength = dtstickerfile.tellg(); filelength = bepaald::fileSize(fullpath); //} if (filelength == 0 || filelength == static_cast(-1)) [[unlikely]] { Logger::error("Failed to get Sticker filesize. Skipping..."); return false; } // install the sticker (if not already present) if (dtstickerid != dtcoversticker || // if this is not the cover, known_backup_id == -1) // or its the cover of an unknown pack { // -> add it! std::any retval; if (!insertRow("sticker", {{"pack_id", dtpackid}, {"pack_key", dtkey}, {"pack_title", dttitle}, {"pack_author", dtauthor}, {"sticker_id", dtstickerid}, {"cover", dtstickerid == dtcoversticker ? 1 : 0}, {"emoji", dtstickerid == dtcoversticker ? "" : dtemoji}, {"installed", dt_installed}, {"file_path", "[has_non_null_constraint_but_is_recreated_on_backup_restore]"}, {"file_length", filelength}}, "_id", &retval)) { Logger::error("Error inserting sticker"); return false; } long long int new_sticker_id = std::any_cast(retval); // add file to d_stickers... DeepCopyingUniquePtr new_sticker_frame; if (setFrameFromStrings(&new_sticker_frame, std::vector{"ROWID:uint64:" + bepaald::toString(new_sticker_id), "LENGTH:uint32:" + bepaald::toString(filelength)})) { //new_sticker_frame->setLazyDataRAW(filelength, databasedir + "/stickers.noindex/" + dtpath); //new_sticker_frame->setReader(new RawFileAttachmentReader(databasedir + "/stickers.noindex/" + dtpath)); new_sticker_frame->setReader(new DesktopAttachmentReader(version, fullpath, localkey, filelength)); d_stickers.emplace(new_sticker_id, new_sticker_frame.release()); } else { Logger::error("Failed to add new sticker to backup"); d_database.exec("DELETE FROM sticker WHERE _id = ?", new_sticker_id); continue; } } if (dtstickerid == dtcoversticker && // if this is the cover sticker (this condition is only here to do this only once) known_backup_id != -1) // and the pack is already known, just not _installed_ d_database.exec("UPDATE sticker SET installed = 1, pack_key = ? WHERE _id = ?", {dtkey, known_backup_id}); if (dtstickerid == dtcoversticker && // this was the cover sticker dtcoveronly == 0 && // but also a valid sticker itself dt_installed == 1) // we're fully installing, not just making known { std::any retval; if (!insertRow("sticker", {{"pack_id", dtpackid}, {"pack_key", dtkey}, {"pack_title", dttitle}, {"pack_author", dtauthor}, {"sticker_id", dtstickerid}, {"cover", 0}, {"emoji", dtemoji}, {"installed", 1}, {"file_path", "[has_non_null_constraint_but_is_recreated_on_backup_restore]"}, {"file_length", filelength}}, "_id", &retval)) { Logger::error("Error inserting sticker"); return false; } long long int new_sticker_id = std::any_cast(retval); DeepCopyingUniquePtr new_sticker_frame2; if (setFrameFromStrings(&new_sticker_frame2, std::vector{"ROWID:uint64:" + bepaald::toString(new_sticker_id), "LENGTH:uint32:" + bepaald::toString(filelength)})) { //new_sticker_frame2->setLazyDataRAW(filelength, databasedir + "/stickers.noindex/" + dtpath); //new_sticker_frame2->setReader(new RawFileAttachmentReader(databasedir + "/stickers.noindex/" + dtpath)); new_sticker_frame2->setReader(new DesktopAttachmentReader(version, fullpath, localkey, filelength)); d_stickers.emplace(new_sticker_id, new_sticker_frame2.release()); } else { Logger::error("Failed to add new sticker to backup"); d_database.exec("DELETE FROM sticker WHERE _id = ?", new_sticker_id); continue; } } } } return true; } /* id = 9acc9e8aba563d26a4994e69263e3b25 key = Wm3/OUjCjvubeq+T7MN1xp/DFueAd+0mhnoU0QoPahI= author = Agnes Lee coverStickerId = 24 createdAt = 1668618591135 downloadAttempts = 1 installedAt = 1668688271632 lastUsed = status = installed stickerCount = 24 title = Bandit the Cat attemptedStatus = downloaded position = 0 storageID = 7ApKBVHNF+ZaKsOIUYOvjw== storageVersion = 170 storageUnknownFields = storageNeedsSync = 0 id = 24 packId = 9acc9e8aba563d26a4994e69263e3b25 emoji = height = 512 isCoverOnly = 1 lastUsed = path = 03/0399b0c0b87f750ddd65c58617b1d18c3951f894c688154f965a76194f79e74b width = 512 */ signalbackup-tools-20250313-1/signalbackup/dtinsertattachments.cc000066400000000000000000000605231476450434500247760ustar00rootroot00000000000000/* Copyright (C) 2022-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../attachmentmetadata/attachmentmetadata.h" bool SignalBackup::dtInsertAttachments(long long int mms_id, long long int unique_id, int numattachments, long long int haspreview, long long int rowid, SqliteDB const &ddb, std::string const &where, std::string const &databasedir, bool isquote, bool issticker, bool targetisdummy) { bool quoted_linkpreview = false; bool quoted_sticker = false; if (numattachments == -1 && isquote) // quote attachments, number not known yet { SqliteDB::QueryResults res; if (!ddb.exec("SELECT IFNULL(json_array_length(json, '$.attachments'), 0) AS numattachments FROM messages " + where, &res) || (res.rows() != 1 && res.columns() != 1)) { Logger::warning("Failed to get number of attachments in quoted message. Skipping"); return false; } numattachments = res.getValueAs(0, "numattachments"); if (numattachments == 0) { if (ddb.exec("SELECT json_extract(json, '$.preview[0].image.path') IS NOT NULL AS quoteispreview FROM messages " + where, &res) && res.rows() == 1 && res.getValueAs(0, "quoteispreview") != 0) { quoted_linkpreview = true; numattachments = 1; //std::cout << "GOT QUOTED LINK PREVIEW" << std::endl; } } if (numattachments == 0) { if (ddb.exec("SELECT json_extract(json, '$.sticker.data.path') IS NOT NULL AS quoteissticker FROM messages " + where, &res) && res.rows() == 1 && res.getValueAs(0, "quoteissticker") != 0) { quoted_sticker = true; numattachments = 1; } } } if (numattachments == 0) { if (haspreview > 0) numattachments = 1; else if (issticker) numattachments = 1; } //std::cout << "rowid: " << rowid << std::endl; //if (numattachments) // std::cout << " " << numattachments << " attachments" << (isquote ? " (in quote)" : "") << std::endl; // for each attachment: SqliteDB::QueryResults results_attachment_data; for (int k = 0; k < numattachments; ++k) { //std::cout << " Attachment " << k + 1 << "/" << numattachments << ": " << std::flush; std::string jsonpath = "$.attachments[" + bepaald::toString(k) + "]"; SqliteDB::QueryResults linkpreview_results; if (haspreview) { jsonpath = "$.preview[0].image"; ddb.exec("SELECT " "json_extract(json, '$.preview[0].url') AS url," "json_extract(json, '$.preview[0].title') AS title," "json_extract(json, '$.preview[0].description') AS description," "json_extract(json, '$.preview[0].image') AS image" " FROM messages WHERE rowid = ?", rowid, &linkpreview_results); } if (issticker || quoted_sticker) jsonpath = "$.sticker.data"; if (quoted_linkpreview) jsonpath = "$.preview[0].image"; // get the attachment info (content-type, size, path, ...) if (!ddb.exec("SELECT " "json_extract(json, '" + jsonpath + ".path') AS path," "json_extract(json, '" + jsonpath + ".contentType') AS content_type," "json_extract(json, '" + jsonpath + ".size') AS size," //"json_extract(json, '" + jsonpath + ".cdnKey') AS cdn_key," "json_extract(json, '" + jsonpath + ".localKey') AS localKey," "IFNULL(json_extract(json, '" + jsonpath + ".version'), 1) AS version," "IFNULL(json_extract(json, '" + jsonpath + ".width'), 0) AS width," "IFNULL(json_extract(json, '" + jsonpath + ".height'), 0) AS height," // only in sticker "json_extract(json, '" + jsonpath + ".emoji') AS sticker_emoji," "json_extract(json, '" + jsonpath + ".packId') AS sticker_packid," "json_extract(json, '" + jsonpath + ".id') AS sticker_id," // not when sticker "json_extract(json, '" + jsonpath + ".fileName') AS file_name," "IFNULL(JSONLONG(json_extract(json, '" + jsonpath + ".uploadTimestamp')), 0) AS upload_timestamp," "IFNULL(json_extract(json, '" + jsonpath + ".flags'), 0) AS flags," // currently, the only flag implemented in Signal is: VOICE_NOTE = 1 "IFNULL(json_extract(json, '" + jsonpath + ".pending'), 0) AS pending," "IFNULL(json_extract(json, '" + jsonpath + ".cdnNumber'), 0) AS cdn_number" " FROM messages " + where, &results_attachment_data)) { Logger::error("Failed to get attachment data from desktop database"); continue; } //results_attachment_data.printLineMode(); // insert any attachments with missing data. (pending != 0) if (results_attachment_data.valueAsString(0, "path").empty()) { if (results_attachment_data.getValueAs(0, "pending") != 0) { if (!insertRow(d_part_table, {{d_part_mid, mms_id}, {d_part_ct, results_attachment_data.value(0, "content_type")}, {d_part_pending, 2}, {"data_size", results_attachment_data.value(0, "size")}, {"file_name", results_attachment_data.value(0, "file_name")}, {(d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id" : ""), unique_id}, {"voice_note", results_attachment_data.isNull(0, "flags") ? 0 : (results_attachment_data.valueAsInt(0, "flags", 0) == 1 ? 1 : 0)}, {"width", 0}, {"height", 0}, {"quote", isquote ? 1 : 0}, {"upload_timestamp", results_attachment_data.value(0, "upload_timestamp")}, {"cdn_number", results_attachment_data.value(0, "cdn_number")}}, "_id")) { Logger::error("Inserting part-data (pending)"); continue; } } else { //std::cout << bepaald::bold_on << "Warning" << bepaald::bold_off // << ": Attachment not found." << std::endl; } if (haspreview && linkpreview_results.rows()) { // this work, but just for consistency, I'd like to escape the string as Signal does for some reason //d_database.exec("UPDATE " + d_mms_table + " SET link_previews = json_array(json_object('url', ?, 'title', ?, 'description', ?, 'date', 0, 'attachmentId', NULL)) WHERE _id = ?", // {linkpreview_results.value(0, "url"), linkpreview_results.value(0, "title"), linkpreview_results.value(0, "description"), mms_id}); /* STRING_ESCAPE(): Quotation mark (") \" Reverse solidus (\) \| Solidus (/) \/ Backspace \b Form feed \f New line \n Carriage return \r Horizontal tab \t As far as I can tell/test, only '/' '\' and '"' are escaped NOTE backslash needs to be done first, or the backslashes inserted by other escapes are escaped... */ std::string url = linkpreview_results("url"); bepaald::replaceAll(&url, '\\', R"(\\)"); //bepaald::replaceAll(&url, "'", R"(\')"); // not done in db bepaald::replaceAll(&url, '/', R"(\/)"); bepaald::replaceAll(&url, '\"', R"(\")"); bepaald::replaceAll(&url, '\'', R"('')"); bepaald::replaceAll(&url, '\n', R"(\n)"); bepaald::replaceAll(&url, '\t', R"(\t)"); bepaald::replaceAll(&url, '\b', R"(\b)"); bepaald::replaceAll(&url, '\f', R"(\f)"); bepaald::replaceAll(&url, '\r', R"(\r)"); bepaald::replaceAll(&url, '\x0B', R"( )"); bepaald::replaceAll(&url, '\v', R"(\v)"); std::string title = linkpreview_results("title"); bepaald::replaceAll(&title, '\\', R"(\\)"); bepaald::replaceAll(&title, '/', R"(\/)"); bepaald::replaceAll(&title, '\"', R"(\")"); bepaald::replaceAll(&title, '\'', R"('')"); bepaald::replaceAll(&title, '\n', R"(\n)"); bepaald::replaceAll(&title, '\t', R"(\t)"); bepaald::replaceAll(&title, '\b', R"(\b)"); bepaald::replaceAll(&title, '\f', R"(\f)"); bepaald::replaceAll(&title, '\r', R"(\r)"); bepaald::replaceAll(&title, '\x0B', R"( )"); bepaald::replaceAll(&title, '\v', R"(\v)"); std::string description = linkpreview_results("description"); bepaald::replaceAll(&description, '\\', R"(\\)"); bepaald::replaceAll(&description, '/', R"(\/)"); bepaald::replaceAll(&description, '\"', R"(\")"); bepaald::replaceAll(&description, '\'', R"('')"); bepaald::replaceAll(&description, '\n', R"(\n)"); bepaald::replaceAll(&description, '\t', R"(\t)"); bepaald::replaceAll(&description, '\b', R"(\b)"); bepaald::replaceAll(&description, '\f', R"(\f)"); bepaald::replaceAll(&description, '\r', R"(\r)"); bepaald::replaceAll(&description, '\x0B', R"( )"); bepaald::replaceAll(&description, '\v', R"(\v)"); SqliteDB::QueryResults jsonstring; ddb.exec("SELECT json_array(json_object('url', json('\"" + url + "\"'), 'title', json('\"" + title + "\"'), 'description', json('\"" + description + "\"'), 'date', 0, 'attachmentId', NULL)) AS link_previews", &jsonstring); std::string linkpreview_as_string = jsonstring("link_previews"); bepaald::replaceAll(&linkpreview_as_string, '\'', R"('')"); d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_previews + " = '" + linkpreview_as_string + "' WHERE _id = ?", mms_id); //d_database.print("SELECT _id,link_previews FROM message WHERE _id = ?", mms_id); } // std::cout << "Here is the message full data:" << std::endl; // SqliteDB::QueryResults res; // ddb.exec("SELECT *,DATETIME(ROUND(IFNULL(received_at, 0) / 1000), 'unixepoch', 'localtime') AS HUMAN_READABLE_TIME FROM messages " + where, &res); // res.printLineMode(); // std::string convuuid = res.valueAsString(0, "conversationId"); // ddb.printLineMode("SELECT profileFullName FROM conversations where id = '" + convuuid + "'"); continue; } if (results_attachment_data.isNull(0, "path") || results_attachment_data.valueAsString(0, "path").empty()) { Logger::error("Attachment path not found."); //results_attachment_data.printLineMode(); continue; } int version = results_attachment_data.valueAsInt(0, "version", -1); std::string localkey(results_attachment_data(0, "localKey")); int64_t size = results_attachment_data.valueAsInt(0, "size", -1); std::string fullpath(databasedir + "/attachments.noindex/" + results_attachment_data.valueAsString(0, "path")); if (version >= 2 && (localkey.empty() || size == -1)) { Logger::error("Decryption info for attachment not valid. (version: ", version, ", key: ", localkey, ", size: ", size, ")"); //results_attachment_data.printLineMode(); continue; } long long int filesize = results_attachment_data.valueAsInt(0, "size", 0); std::string hash; if (!targetisdummy || filesize == 0) { // get attachment metadata AttachmentMetadata amd; if (version >= 2) [[likely]] { DesktopAttachmentReader dar(version, fullpath, localkey, size); #if __cpp_lib_out_ptr >= 202106L std::unique_ptr att_data; if (dar.getAttachmentData(std::out_ptr(att_data), d_verbose) != 0) #else unsigned char *att_data = nullptr; // !! NOTE RAW POINTER if (dar.getAttachmentData(&att_data, d_verbose) != 0) #endif { Logger::error("Failed to get attachment data"); continue; } #if __cpp_lib_out_ptr >= 202106L amd = AttachmentMetadata::getAttachmentMetaData(fullpath, att_data.get(), size); // get metadata from heap #else amd = AttachmentMetadata::getAttachmentMetaData(fullpath, att_data, size); // get metadata from heap if (att_data) delete[] att_data; #endif } else amd = AttachmentMetadata::getAttachmentMetaData(fullpath); // get from file if (amd.filename.empty() || (amd.filesize == 0 && results_attachment_data.valueAsInt(0, "size", 0) != 0)) { Logger::error("Trying to set attachment data. Skipping."); Logger::error_indent("Pending: ", results_attachment_data.valueAsInt(0, "pending")); //results_attachment_data.prettyPrint(); //std::cout << amd.filesize << std::endl; //std::cout << "Corresponding message:" << std::endl; //ddb.prettyPrint("SELECT DATETIME(ROUND(messages.sent_at/1000),'unixepoch','localtime'),messages.body,COALESCE(conversations.profileFullName,conversations.name) AS correspondent FROM messages LEFT JOIN conversations ON json_extract(messages.json, '$.conversationId') == conversations.id " + where); continue; } if (amd.filesize == 0) { Logger::warning("Skipping 0 byte attachment. Not supported in Signal Android."); continue; } filesize = amd.filesize; hash = amd.hash; } //insert into part std::any retval; if (!insertRow(d_part_table, {{d_part_mid, mms_id}, {d_part_ct, results_attachment_data.value(0, "content_type")}, {d_part_pending, 0}, {"data_size", filesize}, {(d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id" : ""), unique_id}, {"voice_note", results_attachment_data.isNull(0, "flags") ? 0 : (results_attachment_data.valueAsInt(0, "flags", 0) == 1 ? 1 : 0)}, {"width", results_attachment_data.value(0, "width")}, {"height", results_attachment_data.value(0, "height")}, {"quote", isquote ? 1 : 0}, {(d_database.tableContainsColumn(d_part_table, "data_hash") ? "data_hash" : ""), hash}, {(d_database.tableContainsColumn(d_part_table, "data_hash_start") ? "data_hash_start" : ""), hash}, {(d_database.tableContainsColumn(d_part_table, "data_hash_end") ? "data_hash_end" : ""), hash}, {"upload_timestamp", results_attachment_data.value(0, "upload_timestamp")}, // will be 0 on sticker {"cdn_number", results_attachment_data.value(0, "cdn_number")}, // will be 0 on sticker, usually 0 or 2, but I dont know what it means {"file_name", results_attachment_data.value(0, "file_name")}}, "_id", &retval)) { Logger::error("Inserting part-data"); continue; } long long int new_part_id = std::any_cast(retval); //std::cout << "Inserted part, new id: " << new_part_id << std::endl; if (issticker || quoted_sticker) { // get the data from $.sticker (instead of $.sticker.data) SqliteDB::QueryResults stickerdata; if (ddb.exec("SELECT " "json_extract(json, '$.sticker.packKey') AS 'packkey'," "json_extract(json, '$.sticker.packId') AS 'packid'," "json_extract(json, '$.sticker.stickerId') AS 'stickerid'," "IFNULL(json_extract(json, '$.sticker.emoji'), '') AS 'emoji' " "FROM messages " + where, &stickerdata) && stickerdata.rows() == 1) { // gather data std::string sticker_emoji = stickerdata("emoji"); std::string sticker_packid = stickerdata("packid"); long long int sticker_id = -1; if (stickerdata.valueHasType(0, "stickerid")) sticker_id = stickerdata.getValueAs(0, "stickerid"); std::string sticker_packkey = stickerdata("packkey"); if (!sticker_packkey.empty()) { auto [key, keysize] = Base64::base64StringToBytes(sticker_packkey); if (key && keysize) { sticker_packkey = bepaald::bytesToHexString(key, keysize, true); bepaald::destroyPtr(&key, &keysize); } } // check data, emoji can be empty if (sticker_packid.empty() || sticker_packkey.empty() || sticker_id == -1) stickerdata.printLineMode(); else { if (d_database.exec("UPDATE " + d_part_table + " SET " "sticker_pack_id = ?, " "sticker_pack_key = ?, " "sticker_id = ?" " WHERE _id = ?", {sticker_packid, sticker_packkey, sticker_id, new_part_id})) // set emoji if not empty if (!sticker_emoji.empty()) d_database.exec("UPDATE " + d_part_table + " SET sticker_emoji = ? WHERE _id = ?", {sticker_emoji, new_part_id}); } } } if (haspreview && linkpreview_results.rows()) { // this works, but I want to escape the string like Signal does //d_database.exec("UPDATE " + d_mms_table + " SET d_mms_previews = json_array(json_object('url', ?, 'title', ?, 'description', ?, 'date', 0, 'attachmentId', json_object('rowId', ?, 'uniqueId', ?, 'valid', true))) " //"WHERE _id = ?", {linkpreview_results.value(0, "url"), linkpreview_results.value(0, "title"), linkpreview_results.value(0, "description"), new_part_id, unique_id, mms_id}); std::string url = linkpreview_results("url"); bepaald::replaceAll(&url, '\\', R"(\\)"); bepaald::replaceAll(&url, '/', R"(\/)"); bepaald::replaceAll(&url, '\"', R"(\")"); bepaald::replaceAll(&url, '\'', R"('')"); bepaald::replaceAll(&url, '\n', R"(\n)"); bepaald::replaceAll(&url, '\t', R"(\t)"); bepaald::replaceAll(&url, '\b', R"(\b)"); bepaald::replaceAll(&url, '\f', R"(\f)"); bepaald::replaceAll(&url, '\r', R"(\r)"); bepaald::replaceAll(&url, '\x0B', R"( )"); bepaald::replaceAll(&url, '\v', R"(\v)"); std::string title = linkpreview_results("title"); bepaald::replaceAll(&title, '\\', R"(\\)"); bepaald::replaceAll(&title, '/', R"(\/)"); bepaald::replaceAll(&title, '\"', R"(\")"); bepaald::replaceAll(&title, '\'', R"('')"); bepaald::replaceAll(&title, '\n', R"(\n)"); bepaald::replaceAll(&title, '\t', R"(\t)"); bepaald::replaceAll(&title, '\b', R"(\b)"); bepaald::replaceAll(&title, '\f', R"(\f)"); bepaald::replaceAll(&title, '\r', R"(\r)"); bepaald::replaceAll(&title, '\x0B', R"( )"); bepaald::replaceAll(&title, '\v', R"(\v)"); std::string description = linkpreview_results("description"); bepaald::replaceAll(&description, '\\', R"(\\)"); bepaald::replaceAll(&description, '/', R"(\/)"); bepaald::replaceAll(&description, '\"', R"(\")"); bepaald::replaceAll(&description, '\'', R"('')"); bepaald::replaceAll(&description, '\n', R"(\n)"); bepaald::replaceAll(&description, '\t', R"(\t)"); bepaald::replaceAll(&description, '\b', R"(\b)"); bepaald::replaceAll(&description, '\f', R"(\f)"); bepaald::replaceAll(&description, '\r', R"(\r)"); bepaald::replaceAll(&description, '\x0B', R"( )"); bepaald::replaceAll(&description, '\v', R"(\v)"); SqliteDB::QueryResults jsonstring; ddb.exec("SELECT json_array(json_object(" "'url', json('\"" + url + "\"'), " "'title', json('\"" + title + "\"'), " "'description', json('\"" + description + "\"'), " "'date', 0, " "'attachmentId', json_object('rowId', ?, 'uniqueId', ?, 'valid', json('true')))) AS link_previews", {new_part_id, unique_id}, &jsonstring); std::string linkpreview_as_string = jsonstring("link_previews"); bepaald::replaceAll(&linkpreview_as_string, '\'', R"('')"); d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_previews + " = '" + linkpreview_as_string + "' WHERE _id = ?", mms_id); //d_database.print("SELECT _id,d_mms_previews FROM message WHERE _id = ?", mms_id); } /* // 1 link with preview image // DESKTOP // json_extract(json, '$.preview') = [{"url":"https://www.reddit.com/r/StableDiffusionInfo/comments/10h30h6/tutorial_on_installing_sd_to_run_locally_on/","title":"r/StableDiffusionInfo on Reddit","image":{"contentType":"image/jpeg","size":69266,"flags":0,"width":1200,"height":630,"blurHash":"LASX=n.:zATd_2rpkoy?:4K*BX+Z","uploadTimestamp":1674935886235,"cdnNumber":2,"cdnKey":"AnPqh3-Ujsx9ZGeW1_J1","path":"d6/d6e6cad87d22f14500024701a34aa2d76abfce1f5b2ce0e619c0c1dd6d235be1","thumbnail":{"path":"2d/2d3f2e7e9d782fd33f2b20e2a6ead088decacc5d549b1451d2d43dddae99db96","contentType":"image/png","width":150,"height":150}},"description":"Tutorial on installing SD to run locally on Windows?"}] // --> // ANDROID // link_previews = [{"url":"https:\/\/www.reddit.com\/r\/StableDiffusionInfo\/comments\/10h30h6\/tutorial_on_installing_sd_to_run_locally_on\/","title":"r\/StableDiffusionInfo on Reddit","description":"Tutorial on installing SD to run locally on Windows?","date":0,"attachmentId":{"rowId":28,"uniqueId":1675171736355,"valid":true}}] // // 1 link, no preview image: // DESKTOP // preview":[{"description":"Posted by u/calilaser - 65 votes and 7 comments","title":"r/esp32 on Reddit: 10 Steps To Building a Light Up IoT Button from Scratch","url":"https://www.reddit.com/r/esp32/comments/12b5258/10_steps_to_building_a_light_up_iot_button_from/","domain":"www.reddit.com","isStickerPack":false}] // --> ANDROID // link_previews = [{"url":"https:\/\/www.reddit.com\/r\/esp32\/comments\/12b5258\/10_steps_to_building_a_light_up_iot_button_from\/","title":"r\/esp32 on Reddit: 10 Steps To Building a Light Up IoT Button from Scratch","description":"Posted by u\/calilaser - 65 votes and 7 comments","date":0,"attachmentId":null}] */ DeepCopyingUniquePtr new_attachment_frame; if (setFrameFromStrings(&new_attachment_frame, std::vector{"ROWID:uint64:" + bepaald::toString(new_part_id), (d_database.tableContainsColumn(d_part_table, "unique_id") ? "ATTACHMENTID:uint64:" + bepaald::toString(unique_id) : ""), "LENGTH:uint32:" + bepaald::toString(filesize)})) { new_attachment_frame->setReader(new DesktopAttachmentReader(version, fullpath, localkey, size)); d_attachments.emplace(std::make_pair(new_part_id, d_database.tableContainsColumn(d_part_table, "unique_id") ? unique_id : -1), new_attachment_frame.release()); } else { Logger::error("Failed to create AttachmentFrame for data"); Logger::error_indent(" rowid : ", new_part_id); Logger::error_indent(" attachmentid: ", unique_id); Logger::error_indent(" length : ", filesize); Logger::error_indent(" path : ", databasedir, "/attachments.noindex/", results_attachment_data.valueAsString(0, "path")); // try to remove the inserted part entry: d_database.exec("DELETE FROM " + d_part_table + " WHERE _id = ?", new_part_id); continue; } //std::cout << "APPENDED ATTACHMENT FRAME[" << new_part_id << "," << unique_id << "]. FILE NAME: '" << d_attachments[{new_part_id, unique_id}]->filename() << "'" << std::endl; } return true; } signalbackup-tools-20250313-1/signalbackup/dtinsertreactions.cc000066400000000000000000000056231476450434500244520ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::dtInsertReactions(SqliteDB const &ddb, long long int message_id, std::vector> const &reactions, bool mms, std::map *savedmap, std::string const &databasedir, bool createcontacts, bool createcontacts_valid) { if (d_verbose && reactions.size()) [[unlikely]] Logger::message("Inserting ", reactions.size(), " message reactions."); // insert into reactions for (auto const &r : reactions) { // r[0] : emoji // r[1] : timestamp // r[2] : author uuid // r[3] : author phone long long int author = -1; if (!r[2].empty()) author = getRecipientIdFromUuidMapped(r[2], savedmap); if (author == -1) author = getRecipientIdFromPhoneMapped(r[3], savedmap); if (author == -1 && !r[2].empty() && (createcontacts || createcontacts_valid)) author = dtCreateRecipient(ddb, r[2], r[3], std::string(), databasedir, savedmap, createcontacts_valid, &createcontacts); if (author == -1) { Logger::warning("Reaction author not found. Skipping"); continue; } if (d_database.tableContainsColumn("reaction", "is_mms")) // not actually removed yet? just unused... { if (!insertRow("reaction", {{"message_id", message_id}, {"is_mms", mms ? 1 : 0}, {"author_id", author}, {"emoji", r[0]}, {"date_sent", bepaald::toNumber(r[1])}, {"date_received", bepaald::toNumber(r[1])}})) Logger::error("Failed to insert into reaction table"); } else { if (!insertRow("reaction", {{"message_id", message_id}, {"author_id", author}, {"emoji", r[0]}, {"date_sent", bepaald::toNumber(r[1])}, {"date_received", bepaald::toNumber(r[1])}})) Logger::error("Failed to insert into reaction table"); } } } signalbackup-tools-20250313-1/signalbackup/dtsetavatar.cc000066400000000000000000000055141476450434500232270ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../attachmentmetadata/attachmentmetadata.h" bool SignalBackup::dtSetAvatar(std::string const &avatarpath, std::string const &key, int64_t size, int version, long long int rid, std::string const &databasedir) { // set avatar //std::string avatarpath = res("avatar"); if (avatarpath.empty()) return true; if (version >= 2 && (key.empty() || size <= 0)) { Logger::error("Decryption info for avatar not valid. (version: ", version, ", key: ", key, ", size: ", size, ")"); return false; } // get attachment metadata !! NOTE RAW POINTER AttachmentMetadata amd; std::string fullpath(databasedir + "/attachments.noindex/" + avatarpath); if (version >= 2) { DesktopAttachmentReader dar(version, fullpath, key, size); #if __cpp_lib_out_ptr >= 202106L std::unique_ptr att_data; if (dar.getAttachmentData(std::out_ptr(att_data), d_verbose) != 0) #else unsigned char *att_data = nullptr; if (dar.getAttachmentData(&att_data, d_verbose) != 0) #endif { Logger::error("Failed to get avatar data"); return false; } #if __cpp_lib_out_ptr >= 202106L amd = AttachmentMetadata::getAttachmentMetaData(fullpath, att_data.get(), size); // get metadata from heap #else amd = AttachmentMetadata::getAttachmentMetaData(fullpath, att_data, size); // get metadata from heap if (att_data) delete[] att_data; #endif } else amd = AttachmentMetadata::getAttachmentMetaData(fullpath); // get from file if (!amd) return false; DeepCopyingUniquePtr new_avatar_frame; if (setFrameFromStrings(&new_avatar_frame, std::vector{"RECIPIENT:string:" + bepaald::toString(rid), "LENGTH:uint32:" + bepaald::toString(amd.filesize)})) { new_avatar_frame->setReader(new DesktopAttachmentReader(version, fullpath, key, size)); d_avatars.emplace_back(std::make_pair(bepaald::toString(rid), std::move(new_avatar_frame))); return true; } return false; } signalbackup-tools-20250313-1/signalbackup/dtsetcolumnnames.cc000066400000000000000000000030241476450434500242640ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::dtSetColumnNames(SqliteDB *ddb) { // conversations.uuid -> conversations.serviceid if (ddb->tableContainsColumn("conversations", "serviceId")) d_dt_c_uuid = "serviceId"; else if (ddb->tableContainsColumn("conversations", "uuid")) d_dt_c_uuid = "uuid"; // messages.sourceuuid -> messages.sourceserviceid if (ddb->tableContainsColumn("messages", "sourceServiceId")) d_dt_m_sourceuuid = "sourceServiceId"; else if (ddb->tableContainsColumn("messages", "sourceUuid")) d_dt_m_sourceuuid = "sourceUuid"; // sessions.ourUuid -> sessions.ourServiceId if (ddb->tableContainsColumn("sessions", "ourServiceId")) d_dt_s_uuid = "ourServiceId"; else if (ddb->tableContainsColumn("sessions", "ourUuid")) d_dt_s_uuid = "ourUuid"; } signalbackup-tools-20250313-1/signalbackup/dtsetmessagedeliveryreceipts.cc000066400000000000000000000164041476450434500267000ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::dtSetMessageDeliveryReceipts(SqliteDB const &ddb, long long int rowid, std::map *savedmap, std::string const &databasedir, bool createcontacts, long long int msg_id, bool is_mms, bool isgroup, bool create_valid_contacts, bool *warn) { // public static final int STATUS_UNKNOWN = -1; // public static final int STATUS_UNDELIVERED = 0; // public static final int STATUS_DELIVERED = 1; // public static final int STATUS_READ = 2; // public static final int STATUS_VIEWED = 3; // public static final int STATUS_SKIPPED = 4; long long int constexpr STATUS_DELIVERED = 1; long long int constexpr STATUS_READ = 2; SqliteDB::QueryResults status_results; if (!ddb.exec("SELECT " "delivery_details.key AS conv_id," "conversations." + d_dt_c_uuid + " AS uuid," "conversations.e164 AS e164," "json_extract(delivery_details.value, '$.status') AS status," "COALESCE(json_extract(delivery_details.value, '$.updatedAt'), delivery_details.sent_at) AS updated_timestamp" " FROM " "(SELECT sent_at,key,value FROM messages,json_each(messages.json, '$.sendStateByConversationId') WHERE rowid IS ?) AS delivery_details" " LEFT JOIN conversations ON conversations.id IS conv_id", rowid, &status_results)) { Logger::error("Getting message delivery status"); return; } // results: // key = 37fb0475-13e7-43e6-965a-4b11d9370488 // status = Sent //updated_timestamp = 1668710610793 // // key = d1c1693d-91e1-486f-8634-29c22afb881b // status = Delivered //updated_timestamp = 1668710612716 //status_results.prettyPrint(); long long int deliveryreceiptcount = 0; long long int readreceiptcount = 0; long long int updatedtimestamp = -1; for (unsigned int i = 0; i < status_results.rows(); ++i) { if (status_results.valueAsString(i, "status") == "Delivered") { ++deliveryreceiptcount; if (updatedtimestamp == -1) updatedtimestamp = status_results.valueAsInt(i, "updated_timestamp", -1); if (isgroup && !status_results.isNull(i, "updated_timestamp")) // add per-group-member details to cdelivery_receipts table { long long int member_id = getRecipientIdFromUuidMapped(status_results.valueAsString(i, "uuid"), savedmap, createcontacts); if (member_id == -1) // try phone member_id = getRecipientIdFromPhoneMapped(status_results.valueAsString(i, "e164"), savedmap, createcontacts); if (member_id == -1) { if (createcontacts) { if ((member_id = dtCreateRecipient(ddb, status_results.valueAsString(i, "uuid"), std::string(), std::string(), databasedir, savedmap, create_valid_contacts, warn)) == -1) { Logger::error("Failed to create delivery_receipt member. Skipping"); continue; } } else { Logger::error("Failed to get id of delivery_receipt member. Skipping"); continue; } } if (!insertRow("group_receipts", {{"mms_id", msg_id}, {"address", member_id}, {"status", STATUS_DELIVERED}, {"timestamp", status_results.getValueAs(i, "updated_timestamp")}})) Logger::error("Inserting group_receipt"); } } else if (status_results.valueAsString(i, "status") == "Read") { ++readreceiptcount; if (updatedtimestamp == -1) updatedtimestamp = status_results.valueAsInt(i, "updated_timestamp", -1); if (isgroup && !status_results.isNull(i, "updated_timestamp")) // add per-group-member details to cdelivery_receipts table { long long int member_id = getRecipientIdFromUuidMapped(status_results.valueAsString(i, "uuid"), savedmap, createcontacts); if (member_id == -1) // try phone member_id = getRecipientIdFromPhoneMapped(status_results.valueAsString(i, "e164"), savedmap, createcontacts); if (member_id == -1) { if (createcontacts) { if ((member_id = dtCreateRecipient(ddb, status_results.valueAsString(i, "uuid"), std::string(), std::string(), databasedir, savedmap, create_valid_contacts, warn)) == -1) { Logger::error("Failed to create delivery_receipt member. Skipping"); continue; } } else { Logger::error("Failed to get id of delivery_receipt member. Skipping"); continue; } } if (!insertRow("group_receipts", {{"mms_id", msg_id}, {"address", member_id}, {"status", STATUS_READ}, {"timestamp", status_results.getValueAs(i, "updated_timestamp")}})) Logger::error("Inserting group_receipt"); } } } // update the message in its table (mms/sms) if (deliveryreceiptcount < readreceiptcount) deliveryreceiptcount = readreceiptcount; // lets just say read messages are also delivered... if (deliveryreceiptcount) if (!d_database.exec("UPDATE " + (is_mms ? d_mms_table : "sms"s) + " SET " + d_mms_delivery_receipts + " = ? WHERE _id = ?", {deliveryreceiptcount, msg_id})) Logger::error("Updating ", (is_mms ? d_mms_table : "sms"), " ", d_mms_delivery_receipts, "."); if (readreceiptcount) if (!d_database.exec("UPDATE " + (is_mms ? d_mms_table : "sms"s) + " SET " + d_mms_read_receipts + " = ? WHERE _id = ?", {readreceiptcount, msg_id})) Logger::error("Updating ", (is_mms ? d_mms_table : "sms"), " ", d_mms_read_receipts, "."); // update receipt timestamp (if available) if (d_database.tableContainsColumn((is_mms ? d_mms_table : "sms"s), "receipt_timestamp")) if (!d_database.exec("UPDATE " + (is_mms ? d_mms_table : "sms"s) + " SET receipt_timestamp = ? WHERE _id = ?", {updatedtimestamp, msg_id})) Logger::error("Updating ", (is_mms ? d_mms_table : "sms"), " receipt_timestamp."); //insert into group_receipts // sqlite> SELECT * from group_receipts where _id = 8; // _id = 8 // mms_id = 27 // address = 4 // status = 1 // timestamp = 1669579600157 //unidentified = 1 } signalbackup-tools-20250313-1/signalbackup/dtsetsharedcontactsjsonstring.cc000066400000000000000000000272021476450434500270750ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "dtsharedcontactstruct.h" // On android message.shared_contacts is a json string looking like: /* [ { "name": { "displayName":"Selwin van Dijk", "givenName":"Selwin", "familyName":"van Dijk", "prefix":null, "suffix":null, "middleName":null, "empty":false }, "organization":null, <--------------------------------------------------- never seen this filled "phoneNumbers":[ { "number":"0201234567", "type":"HOME", "label":"home" }, { "number":"0612345678", "type":"MOBILE", "label":"cell" } ], "emails":[ { "email":"balabla@something.fake", "type":"WORK", "label":"work" }, { "email":"anotherfake@email.address", "type":"HOME", "label":"home" } ], "postalAddresses":[], <--------------------------------------------------- never seen this filled "avatar": { "attachmentId":null, / {"rowId":5318,"uniqueId":1660632296881,"valid":true} "isProfile":false, <--------------------------------------------------- never seen !false "profile":false <--------------------------------------------------- never seen !false } } ] */ /* In android: message.shared_contacts = [{"name":{"displayName":"Fake Jay Contact, Sr.","givenName":"Fake","familyName":"Contact","prefix":null,"suffix":"Sr","middleName":null,"empty":false},"organization":null,"phoneNumbers":[{"number":"0611112222","type":"MOBILE","label":"cell"},{"number":"0505411111","type":"HOME","label":"home"},{"number":"08000611","type":"WORK","label":"work"},{"number":"000000000","type":"CUSTOM","label":"customphonefield"}],"emails":[{"email":"home@home.hh","type":"HOME","label":"home"},{"email":"wor@work.ww","type":"WORK","label":"work"},{"email":"other@other.oo","type":"CUSTOM","label":null},{"email":"custom@custom.cc","type":"CUSTOM","label":"customfield"}],"postalAddresses":[],"avatar":{"attachmentId":null,"isProfile":false,"profile":false}}] On Desktop: json_extract(json, '$.contact') = [{"name":{"givenName":"Fake","familyName":"Contact","suffix":"Sr","displayName":"Fake Jay Contact, Sr."},"number":[{"value":"+31611112222","type":2,"label":"cell"},{"value":"+31505411111","type":1,"label":"home"},{"value":"+318000611","type":3,"label":"work"},{"value":"000000000","type":4,"label":"customphonefield"}],"email":[{"value":"home@home.hh","type":1,"label":"home"},{"value":"wor@work.ww","type":3,"label":"work"},{"value":"other@other.oo","type":4},{"value":"custom@custom.cc","type":4,"label":"customfield"}]}] */ std::string SignalBackup::dtSetSharedContactsJsonString(SqliteDB const &ddb, long long int rowid) const { //*** gather data from Desktop database ***// SqliteDB::QueryResults dtsc; ddb.exec("SELECT " "json_extract(json, '$.contact[0].name.givenName') AS givenName, " "json_extract(json, '$.contact[0].name.familyName') AS familyName, " "json_extract(json, '$.contact[0].name.suffix') AS suffix, " "json_extract(json, '$.contact[0].name.displayName') AS displayName, " "json_extract(json, '$.contact[0].name.prefix') AS prefix, " "json_extract(json, '$.contact[0].name.middleName') AS middleName, " "IFNULL(json_array_length(json, '$.contact[0].number'), 0) AS numphones, " "IFNULL(json_array_length(json, '$.contact[0].email'), 0) AS numemails " "FROM messages WHERE rowid = ?", rowid, &dtsc); //dtsc.prettyPrint(); //*** save data to structure ***// SharedContactData scd; if (!dtsc.isNull(0, "givenName")) scd.name.givenName = dtsc("givenName"); if (!dtsc.isNull(0, "familyName")) scd.name.familyName = dtsc("familyName"); if (!dtsc.isNull(0, "suffix")) scd.name.suffix = dtsc("suffix"); if (!dtsc.isNull(0, "prefix")) scd.name.prefix = dtsc("prefix"); if (!dtsc.isNull(0, "displayName")) scd.name.displayName = dtsc("displayName"); if (!dtsc.isNull(0, "middleName")) scd.name.middleName = dtsc("middleName"); scd.name.empty = (!scd.name.givenName.has_value() && !scd.name.familyName.has_value() && !scd.name.displayName.has_value() && !scd.name.middleName.has_value()); for (unsigned int i = 0; i < dtsc.valueAsInt(0, "numphones", 0); ++i) { SqliteDB::QueryResults dtsc_array; ddb.exec("SELECT " "json_extract(json, '$.contact[0].number[" + bepaald::toString(i) + "].value') AS number, " "json_extract(json, '$.contact[0].number[" + bepaald::toString(i) + "].type') AS type, " "json_extract(json, '$.contact[0].number[" + bepaald::toString(i) + "].label') AS label " "FROM messages WHERE rowid = ?", rowid, &dtsc_array); SharedContactDataPhone tmp; if (!dtsc_array.isNull(0, "number")) tmp.number = dtsc_array("number"); if (!dtsc_array.isNull(0, "label")) tmp.label = dtsc_array("label"); int type = dtsc_array.valueAsInt(0, "type", -1); switch (type) { case 1: tmp.type = "HOME"s; break; case 2: tmp.type = "MOBILE"s; break; case 3: tmp.type = "WORK"s; break; case 4: tmp.type = "CUSTOM"s; break; default: tmp.type = "CUSTOM"s; break; } scd.phoneNumbers.emplace_back(std::move(tmp)); } for (unsigned int i = 0; i < dtsc.valueAsInt(0, "numemails", 0); ++i) { SqliteDB::QueryResults dtsc_array; ddb.exec("SELECT " "json_extract(json, '$.contact[0].email[" + bepaald::toString(i) + "].value') AS email, " "json_extract(json, '$.contact[0].email[" + bepaald::toString(i) + "].type') AS type, " "json_extract(json, '$.contact[0].email[" + bepaald::toString(i) + "].label') AS label " "FROM messages WHERE rowid = ?", rowid, &dtsc_array); SharedContactDataEmail tmp; if (!dtsc_array.isNull(0, "email")) tmp.email = dtsc_array("email"); if (!dtsc_array.isNull(0, "label")) tmp.label = dtsc_array("label"); int type = dtsc_array.valueAsInt(0, "type", -1); switch (type) { case 1: tmp.type = "HOME"s; break; case 2: tmp.type = "MOBILE"s; // ???? break; case 3: tmp.type = "WORK"s; break; case 4: tmp.type = "CUSTOM"s; break; default: tmp.type = "CUSTOM"s; break; } scd.emails.emplace_back(std::move(tmp)); } //*** set data in new json string for insertion into android db ***// std::string jsonstring = ddb.getSingleResultAs("SELECT json_array(" "json_object('name', json_object('displayName', ?, " "'givenName', ?, " "'familyName', ?, " "'prefix', ?, " "'suffix', ?, " "'middleName', ?, " "'empty', "s + (scd.name.empty ? "json('true')" : "json('false')") + "), " "'organization', ?, " "'phoneNumbers', json_array(), " "'emails', json_array(), " "'postalAddresses', json_array(), " //"'avatar', json_object('attachmentId', json_object('rowId', '', 'uniqueId', '', 'valid', json(true)), " // not yet, this requires attachment stuff.. // plus never seen in Desktop db "'avatar', json_object('attachmentId', json('null'), " // so for now, leave empty "'isProfile', json('false'), " "'profile', json('false'))" "))", {scd.name.displayName, scd.name.givenName, scd.name.familyName, scd.name.prefix, scd.name.suffix, scd.name.middleName, nullptr // scd.organization }, std::string()); //std::cout << jsonstring << std::endl; if (jsonstring.empty()) // above failed, the rest will also fail... return jsonstring; // add phonenumbers for (unsigned int p = 0; p < scd.phoneNumbers.size(); ++p) jsonstring = ddb.getSingleResultAs("SELECT json_insert(?, '$[0].phoneNumbers[#]', json_object('number', ?, 'type', ?, 'label', ?))", {jsonstring, scd.phoneNumbers[p].number, scd.phoneNumbers[p].type, scd.phoneNumbers[p].label}, std::string()); if (jsonstring.empty()) // above failed, the rest will also fail... return jsonstring; // add emails for (unsigned int p = 0; p < scd.emails.size(); ++p) jsonstring = ddb.getSingleResultAs("SELECT json_insert(?, '$[0].emails[#]', json_object('email', ?, 'type', ?, 'label', ?))", {jsonstring, scd.emails[p].email, scd.emails[p].type, scd.emails[p].label}, std::string()); return jsonstring; } signalbackup-tools-20250313-1/signalbackup/dtsharedcontactstruct.h000066400000000000000000000037241476450434500251670ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef DTSHARED_CONTACTSTRUCT #define DTSHARED_CONTACTSTRUCT #include #include #include struct SharedContactDataName { std::any displayName = nullptr; std::any givenName = nullptr; std::any familyName = nullptr; std::any prefix = nullptr; std::any suffix = nullptr; std::any middleName = nullptr; bool empty; }; struct SharedContactDataPhone { std::any number = nullptr; std::any type = nullptr; std::any label = nullptr; }; struct SharedContactDataEmail { std::any email = nullptr; std::any type = nullptr; std::any label = nullptr; }; struct SharedContactDataOrganization // ??? { }; struct SharedContactDataPostalAddress // ??? { }; struct SharedContactDataAttachmentId { long long int rowId; long long int uniqueId; bool valid; }; struct SharedContactDataAvatar { std::optional attachmentId; bool isProfile; bool profile; }; struct SharedContactData { SharedContactDataName name; std::optional organization; std::vector phoneNumbers; std::vector emails; std::vector postalAddresses; SharedContactDataAvatar avatar; }; #endif signalbackup-tools-20250313-1/signalbackup/dtupdateprofile.cc000066400000000000000000000132571476450434500241030ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::dtUpdateProfile(SqliteDB const &ddb, std::string const &dtid, long long int aid, std::string const &databasedir) { if (d_verbose) [[unlikely]] Logger::message("Updating profile for id: ", dtid); SqliteDB::QueryResults res; if (!ddb.exec("SELECT type, name, profileName, IFNULL(profileFamilyName, '') AS profileFamilyName, profileFullName, " "IFNULL(json_extract(json,'$.groupVersion'), 1) AS groupVersion, " "COALESCE(json_extract(json, '$.profileAvatar.path'),json_extract(json, '$.avatar.path')) AS avatar, " // 'profileAvatar' for persons, 'avatar' for groups "IFNULL(COALESCE(json_extract(json, '$.profileAvatar.localKey'), json_extract(json, '$.avatar.localKey')), '') AS localKey, " "IFNULL(COALESCE(json_extract(json, '$.profileAvatar.size'), json_extract(json, '$.avatar.size')), 0) AS size, " "IFNULL(COALESCE(json_extract(json, '$.profileAvatar.version'), json_extract(json, '$.avatar.version')), 0) AS version " "FROM conversations WHERE " + d_dt_c_uuid + " = ?1 OR e164 = ?1 OR groupId = ?1", dtid, &res)) return false; // check if we have some data if (res.rows() != 1) { if (res.rows() > 1) Logger::error("Unexpected number of results getting recipient profile data."); else // = 0 Logger::error("No results trying to get recipient profile data."); return false; } // handle group if (res("type") == "group") { if (res.getValueAs(0, "groupVersion") < 2) { // group v1 not yet.... Logger::warning("Updating profile data for groupV1 not yet supported."); return false; } if (res.isNull(0, "name") || res("name").empty()) { Logger::warning("Profile data empty. Not updating group recipient."); return false; } // get actual group id std::pair groupid_data = Base64::base64StringToBytes(res("json_groupId")); if (!groupid_data.first || groupid_data.second == 0) // json data was not valid base64 string, lets try the other one groupid_data = Base64::base64StringToBytes(res("groupId")); if (!groupid_data.first || groupid_data.second == 0) { Logger::warning("Failed to deteremine group_id when trying to update profile."); return false; } std::string group_id = "__signal_group__v2__!" + bepaald::bytesToHexString(groupid_data, true); bepaald::destroyPtr(&groupid_data.first, &groupid_data.second); if (!d_database.exec("UPDATE groups SET title = ? WHERE group_id = ?", {res("name"), group_id})) return false; } else // handle NOT group { if ((res.isNull(0, "profileName") || res("profileName").empty()) && (res.isNull(0, "profileFamilyName") || res("profileFamilyName").empty()) && // not updating with empty info (res.isNull(0, "profileFullName") || res("profileFullName").empty())) { Logger::warning("Profile data empty. Not updating group recipient."); return false; } // if (d_verbose) [[unlikely]] // { // std::cout << "Updating profile:" << std::endl; // res.prettyPrint(); // } // update name info if (!d_database.exec("UPDATE recipient SET " + d_recipient_profile_given_name + " = ?, " "profile_family_name = ?, " "profile_joined_name = ? " "WHERE _id = ?", {res.value(0, "profileName"), res.value(0, "profileFamilyName"), res.value(0, "profileFullName"), aid})) return false; } // update avatar if (!res("avatar").empty()) { if (d_verbose) [[unlikely]] Logger::message_overwrite("Updating avatar..."); // find current auto pos = std::find_if(d_avatars.begin(), d_avatars.end(), [aid](auto const &p) { return p.first == bepaald::toString(aid); }); DeepCopyingUniquePtr backup; // save the current in case something goes wrong... if (pos != d_avatars.end()) { backup = std::move(pos->second); d_avatars.erase(pos); } if (!dtSetAvatar(res("avatar"), res("localKey"), res.valueAsInt(0, "size"), res.valueAsInt(0, "version"), aid, databasedir)) { if (d_verbose && !backup) [[unlikely]] Logger::message_overwrite("Updating avatar... Failed to set new avatar", Logger::Control::ENDOVERWRITE); if (backup) { Logger::message_overwrite("Updating avatar... Failed, restoring previous...", Logger::Control::ENDOVERWRITE); d_avatars.emplace_back(std::make_pair(bepaald::toString(aid), std::move(backup))); } } else { if (d_verbose) [[unlikely]] { Logger::message("Set new avatar. Info:"); for (auto const &a : d_avatars) if (a.first == bepaald::toString(aid)) a.second->printInfo(); } } } return true; } signalbackup-tools-20250313-1/signalbackup/dumpavatars.cc000066400000000000000000000110361476450434500232300ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../attachmentmetadata/attachmentmetadata.h" bool SignalBackup::dumpAvatars(std::string const &dir, std::vector const &contacts, bool overwrite) const { Logger::message_overwrite("Dumping avatars to dir '", dir, "'..."); if (!d_database.containsTable("recipient")) { Logger::error("Database too old, dumping avatars is not (yet) supported, consider a full decrypt by just passing a directory as output"); return false; } if (!prepareOutputDirectory(dir, overwrite)) return false; #if __cplusplus > 201703L for (int count = 0; auto const &avframe : d_avatars) #else int count = 0; for (auto const &avframe : d_avatars) #endif { ++count; Logger::message_overwrite("Dumping avatars to dir '", dir, "'... ", count, "/", d_avatars.size()); AvatarFrame *af = avframe.second.get(); SqliteDB::QueryResults results; std::string query = "SELECT COALESCE(" + (d_database.tableContainsColumn("recipient", "nickname_joined_name") ? "NULLIF(recipient.nickname_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_system_joined_name + ", ''), " + (d_database.tableContainsColumn("recipient", "profile_joined_name") ? "NULLIF(recipient.profile_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_profile_given_name + ", ''), NULLIF(groups.title, ''), " + (d_database.containsTable("distribution_list") ? "NULLIF(distribution_list.name, ''), " : "") + "NULLIF(recipient." + d_recipient_e164 + ", ''), NULLIF(recipient." + d_recipient_aci + ", ''), " " recipient._id) AS 'chatpartner' " "FROM recipient " "LEFT JOIN groups ON recipient.group_id = groups.group_id " + (d_database.containsTable("distribution_list") ? "LEFT JOIN distribution_list ON recipient._id = distribution_list.recipient_id " : "") + "WHERE recipient._id = ?"; // if ! limit.empty() // query += " AND _id == something, or chatpartner == '' if (!d_database.exec(query, af->recipient(), &results)) return false; if (results.rows() != 1) { Logger::error("Unexpected number of results: ", results.rows(), " (recipient: ", af->recipient(), ")"); continue; } std::string name = results.valueAsString(0, "chatpartner"); if (!contacts.empty() && std::find(contacts.begin(), contacts.end(), name) == contacts.end()) continue; // get avatar data, to get extension std::string extension; unsigned char *avatardata = af->attachmentData(); uint64_t avatarsize = af->attachmentSize(); AttachmentMetadata amd = AttachmentMetadata::getAttachmentMetaData(std::string(), avatardata, avatarsize, true/*skiphash*/); extension = "." + std::string(MimeTypes::getExtension(amd.filetype, "jpg")); std::string filename = sanitizeFilename(name + extension); if (filename.empty() || filename == extension) // filename was not set in database or was not impossible // to sanitize (eg reserved name in windows 'COM1') filename = af->recipient() + extension; // make filename unique while (bepaald::fileOrDirExists(dir + "/" + filename)) filename += "(2)"; std::ofstream attachmentstream(dir + "/" + filename, std::ios_base::binary); if (!attachmentstream.is_open()) { Logger::error("Failed to open file for writing: ", dir, "/", filename); af->clearData(); continue; } if (!attachmentstream.write(reinterpret_cast(af->attachmentData()), af->attachmentSize())) { Logger::error("Failed to write data to file: ", dir, "/", filename); af->clearData(); continue; } attachmentstream.close(); // need to close, or the auto-close will change files mtime again. af->clearData(); } Logger::message("done."); return true; } signalbackup-tools-20250313-1/signalbackup/dumpinfoonbadframe.cc000066400000000000000000000240161476450434500245430ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::dumpInfoOnBadFrame(std::unique_ptr *frame) { Logger::warning("Bad MAC in frame, trying to print frame info:"); (*frame)->printInfo(); if ((*frame)->frameType() == BackupFrame::FRAMETYPE::ATTACHMENT) { std::unique_ptr a = std::make_unique(*reinterpret_cast(frame->get())); uint32_t rowid = a->rowId(); int64_t uniqueid = a->attachmentId(); d_badattachments.emplace_back(std::make_pair(rowid, uniqueid)); Logger::message("Frame is attachment, it belongs to entry in the 'part' table of the database:"); //std::vector>> results; SqliteDB::QueryResults results; std::string query = "SELECT * FROM " + d_part_table + " WHERE _id = " + bepaald::toString(rowid); if (d_database.tableContainsColumn(d_part_table, "unique_id")) query += " AND unique_id = " + bepaald::toString(uniqueid); long long int mid = -1; d_database.exec(query, &results); for (unsigned int i = 0; i < results.rows(); ++i) { for (unsigned int j = 0; j < results.columns(); ++j) { Logger::message_start(" - ", results.header(j), " : "); if (results.isNull(i, j)) Logger::message("(NULL)"); else if (results.valueHasType(i, j)) Logger::message(results.getValueAs(i, j)); else if (results.valueHasType(i, j)) Logger::message(results.getValueAs(i, j)); else if (results.valueHasType(i, j)) { if (results.header(j) == d_part_mid) mid = results.getValueAs(i, j); Logger::message(results.getValueAs(i, j)); } else if (results.valueHasType, size_t>>(i, j)) Logger::message(bepaald::bytesToHexString(results.getValueAs, size_t>>(i, j))); else Logger::message("(unhandled result type)"); } } Logger::message("Which belongs to entry in '" + d_mms_table + "' table:"); query = "SELECT * FROM " + d_mms_table + " WHERE _id = " + bepaald::toString(mid); d_database.exec(query, &results); for (unsigned int i = 0; i < results.rows(); ++i) for (unsigned int j = 0; j < results.columns(); ++j) { Logger::message_start(" - ", results.header(j), " : "); if (results.isNull(i, j)) Logger::message("(NULL)"); else if (results.valueHasType(i, j)) Logger::message(results.getValueAs(i, j)); else if (results.valueHasType(i, j)) Logger::message(results.getValueAs(i, j)); else if (results.valueHasType(i, j)) { if (results.header(j) == d_mms_date_sent || results.header(j) == "date_received") { long long int datum = results.getValueAs(i, j); std::time_t epoch = datum / 1000; //std::cout << std::put_time(std::localtime(&epoch), "%F %T %z") << " (" << results.getValueAs(i, j) << ")"); // %F and %T do not work with mingw Logger::message(std::put_time(std::localtime(&epoch), "%Y-%m-%d %H:%M:%S %z"), " (", results.getValueAs(i, j), ")"); } else Logger::message(results.getValueAs(i, j)); } else if (results.valueHasType, size_t>>(i, j)) Logger::message(bepaald::bytesToHexString(results.getValueAs, size_t>>(i, j))); else Logger::message("(unhandled result type)"); } std::string afilename = "attachment_" + bepaald::toString(mid) + ".bin"; Logger::message("Trying to dump decoded attachment to file '", afilename, "'"); std::ofstream bindump(afilename, std::ios_base::binary); bindump.write(reinterpret_cast(a->attachmentData()), a->attachmentSize()); } else { Logger::error("Bad MAC in frame other than AttachmentFrame. Just dropping the frame, this could cause more problems..."); (*frame)->printInfo(); } } void SignalBackup::dumpInfoOnBadFrames() const { for (unsigned int a = 0; a < d_badattachments.size(); ++a) { uint32_t rowid = d_badattachments[a].first; int64_t uniqueid = d_badattachments[a].second; Logger::message("Short info on message to which attachment with bad mac belongs (", a + 1, "/", d_badattachments.size(), "):"); SqliteDB::QueryResults results; std::string query = "SELECT " + d_part_mid + " FROM " + d_part_table + " WHERE _id = " + bepaald::toString(rowid); if (d_database.tableContainsColumn(d_part_table, "unique_id")) query += " AND unique_id = " + bepaald::toString(uniqueid); long long int mid = -1; d_database.exec(query, &results); if (results.header(0) == d_part_mid && results.valueHasType(0, 0)) mid = results.getValueAs(0, 0); else { Logger::error("Failed to get info 1"); return; } query = "SELECT * FROM " + d_mms_table + " WHERE _id = " + bepaald::toString(mid); d_database.exec(query, &results); long long int thread_id = -1; std::string body; std::string date; std::string date_received; long long int type = -1; for (unsigned int i = 0; i < results.rows(); ++i) for (unsigned int j = 0; j < results.columns(); ++j) { if (results.valueHasType(i, j)) { if (results.header(j) == "thread_id") thread_id = results.getValueAs(i, j); if (results.header(j) == d_mms_type) type = results.getValueAs(i, j); if (results.header(j) == d_mms_date_sent) { long long int datum = results.getValueAs(i, j); std::time_t epoch = datum / 1000; std::ostringstream tmp; //tmp << std::put_time(std::localtime(&epoch), "%F T %z") << " (" << results.getValueAs(i, j) << ")"; // %F and %T do not work on mingw tmp << std::put_time(std::localtime(&epoch), "%Y-%m-%d %H:%M:%S %z") << " (" << results.getValueAs(i, j) << ")"; date = tmp.str(); } if (results.header(j) == "date_received") { long long int datum = results.getValueAs(i, j); std::time_t epoch = datum / 1000; std::ostringstream tmp; //tmp << std::put_time(std::localtime(&epoch), "%F T %z") << " (" << results.getValueAs(i, j) << ")"; // %F and %T do not work on mingw tmp << std::put_time(std::localtime(&epoch), "%Y-%m-%d %H:%M:%S %z") << " (" << results.getValueAs(i, j) << ")"; date_received = tmp.str(); } } else if (results.header(j) == "body" && results.valueHasType(i, j)) body = results.getValueAs(i, j); } if (thread_id == -1 || date.empty() || date_received.empty() || type == -1) { Logger::error("Failed to get info 2"); return; } std::string partner; if (d_databaseversion < 24) // OLD VERSION query = "SELECT COALESCE(recipient_preferences.system_display_name, recipient_preferences.signal_profile_name, groups.title) AS 'convpartner' FROM thread LEFT JOIN recipient_preferences ON thread." + d_thread_recipient_id + " = recipient_preferences.recipient_ids LEFT JOIN groups ON thread." + d_thread_recipient_id + " = groups.group_id WHERE thread._id = " + bepaald::toString(thread_id); else //query = "SELECT COALESCE(recipient." + d_recipient_system_joined_name + ", recipient." + d_recipient_profile_given_name + ", groups.title) AS 'convpartner' FROM thread LEFT JOIN recipient ON thread." + d_thread_recipient_id + " = recipient._id LEFT JOIN groups ON recipient.group_id = groups.group_id WHERE thread._id = " + bepaald::toString(thread_id); query = "SELECT COALESCE(" + (d_database.tableContainsColumn("recipient", "nickname_joined_name") ? "NULLIF(recipient.nickname_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_system_joined_name + ", ''), " + (d_database.tableContainsColumn("recipient", "profile_joined_name") ? "NULLIF(recipient.profile_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_profile_given_name + ", ''), NULLIF(groups.title, ''), " + "NULLIF(recipient." + d_recipient_e164 + ", ''), NULLIF(recipient." + d_recipient_aci + ", ''), " " recipient._id) AS 'convpartner' FROM thread LEFT JOIN recipient ON thread." + d_thread_recipient_id + " = recipient._id LEFT JOIN groups ON recipient.group_id = groups.group_id WHERE thread._id = " + bepaald::toString(thread_id); d_database.exec(query, &results); if (results.header(0) == "convpartner" && results.valueHasType(0, 0)) partner = results.getValueAs(0, 0); Logger::message("Date sent : ", date); Logger::message("Date received : ", date_received); Logger::message("Sent ", (Types::isInboxType(type) ? "by : " : "to : "), partner); Logger::message("Message body : ", body); } } signalbackup-tools-20250313-1/signalbackup/dumpmedia.cc000066400000000000000000000326621476450434500226560ustar00rootroot00000000000000/* Copyright (C) 2021-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::dumpMedia(std::string const &dir, std::vector const &daterangelist, std::vector const &threads, bool excludestickers, bool overwrite) const { Logger::message("Dumping media to dir '", dir, "'"); if (!d_database.containsTable(d_part_table) || !d_database.tableContainsColumn(d_part_table, "display_order")) { Logger::error("Database too badly damaged or too old, dumping media is not (yet) supported, consider a full decrypt by just passing a directory as output"); return false; } // check if dir exists, create if not if (!prepareOutputDirectory(dir, overwrite)) return false; std::pair, std::vector> conversations; // links thread_id to thread title, if the // folder already exists, but from another _id, // it is a different thread with the same name // minimal query, for incomplete database bool fullbackup = false; std::string query = "SELECT " + d_part_table + "." + d_part_mid + ", " + d_part_table + "." + d_part_ct + ", " + d_part_table + ".file_name, " + d_part_table + ".display_order" " FROM " + d_part_table + " WHERE " + d_part_table + "._id == ?" + (d_database.tableContainsColumn(d_part_table, "unique_id") ? " AND unique_id == ?" : "") + ((excludestickers && d_database.tableContainsColumn(d_part_table, "sticker_id")) ? " AND sticker_id = -1" : ""); // if all tables for detailed info are present... if (d_database.containsTable(d_mms_table) && d_database.containsTable("thread") && d_database.containsTable("groups") && d_database.containsTable("recipient")) { fullbackup = true; query = "SELECT " + d_part_table + "." + d_part_mid + ", " + d_part_table + "." + d_part_ct + ", " + d_part_table + ".file_name, " + d_part_table + ".display_order, " + d_mms_table + ".date_received, " + d_mms_table + "." + d_mms_type + ", " + d_mms_table + ".thread_id, " "thread." + d_thread_recipient_id + ", " "COALESCE(NULLIF(groups.title, ''), " + (d_database.containsTable("distribution_list") ? "NULLIF(distribution_list.name, ''), " : "") + (d_database.tableContainsColumn("recipient", "nickname_joined_name") ? "NULLIF(recipient.nickname_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_system_joined_name + ", ''), " + (d_database.tableContainsColumn("recipient", "profile_joined_name") ? "NULLIF(recipient.profile_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_profile_given_name + ", '')) AS 'chatpartner'" " FROM " + d_part_table + " " "LEFT JOIN " + d_mms_table + " ON " + d_part_table + "." + d_part_mid + " == " + d_mms_table + "._id " "LEFT JOIN thread ON " + d_mms_table + ".thread_id == thread._id " "LEFT JOIN recipient ON thread." + d_thread_recipient_id + " == recipient._id " "LEFT JOIN groups ON recipient.group_id == groups.group_id " + (d_database.containsTable("distribution_list") ? "LEFT JOIN distribution_list ON recipient._id = distribution_list.recipient_id " : "") + "WHERE " + d_part_table + "._id == ?" + (d_database.tableContainsColumn(d_part_table, "unique_id") ? " AND unique_id == ?" : "") + ((excludestickers && d_database.tableContainsColumn(d_part_table, "sticker_id")) ? " AND sticker_id = -1" : ""); } if (!threads.empty()) { query += " AND thread._id IN ("; for (unsigned int i = 0; i < threads.size(); ++i) query += bepaald::toString(threads[i]) + ((i == threads.size() - 1) ? ")" : ","); } if (!daterangelist.empty()) { // create dateranges std::vector> dateranges; if (daterangelist.size() % 2 == 0) for (unsigned int i = 0; i < daterangelist.size(); i += 2) dateranges.push_back({daterangelist[i], daterangelist[i + 1]}); std::string datewhereclause; for (unsigned int i = 0; i < dateranges.size(); ++i) { bool needrounding = false; long long int startrange = dateToMSecsSinceEpoch(dateranges[i].first); long long int endrange = dateToMSecsSinceEpoch(dateranges[i].second, &needrounding); if (startrange == -1 || endrange == -1 || endrange < startrange) { Logger::error("Skipping range: '", dateranges[i].first, " - ", dateranges[i].second, "'. Failed to parse or invalid range."); Logger::error_indent(startrange, " ", endrange); continue; } if (d_verbose) [[unlikely]] Logger::message(" Using range: ", dateranges[i].first, " - ", dateranges[i].second, " (", startrange, " - ", endrange, ")"); if (needrounding)// if called with "YYYY-MM-DD HH:MM:SS" endrange += 999; // to get everything in the second specified... dateranges[i].first = bepaald::toString(startrange); dateranges[i].second = bepaald::toString(endrange); datewhereclause += (datewhereclause.empty() ? " AND (" : " OR ") + "date_received BETWEEN "s + dateranges[i].first + " AND " + dateranges[i].second; if (i == dateranges.size() - 1) datewhereclause += ')'; } query += datewhereclause; } if (d_verbose) [[unlikely]] Logger::message("Dump media query: ", query); #if __cplusplus > 201703L for (int count = 1; auto const &aframe : d_attachments) #else int count = 1; for (auto const &aframe : d_attachments) #endif { Logger::message_overwrite("Saving attachments... ", count, "/", d_attachments.size()); AttachmentFrame *a = aframe.second.get(); //std::cout << "Looking for attachment: " << std::endl; //std::cout << "rid: " << a->rowId() << std::endl; //std::cout << "uid: " << a->attachmentId() << std::endl; SqliteDB::QueryResults results; uint64_t rowid = a->rowId(); int64_t uniqueid = a->attachmentId(); if (uniqueid == 0) uniqueid = -1; if (d_database.tableContainsColumn(d_part_table, "unique_id")) { if (!d_database.exec(query, {rowid, uniqueid}, &results)) return false; } else // no "unique_id" in part table if (!d_database.exec(query, rowid, &results)) return false; //results.prettyPrint(); if (results.rows() == 0 && (!threads.empty() || !daterangelist.empty())) // probably an attachment for a de-selected thread continue; if (results.rows() != 1) { Logger::error("Unexpected number of results: ", results.rows(), " (rowid: ", a->rowId(), ", uniqueid: ", a->attachmentId(), ")"); continue; } std::string filename; long long int datum = a->attachmentId(); // only works on older dbs... if (fullbackup && !results.isNull(0, "date_received")) datum = results.getValueAs(0, "date_received"); long long int order = results.getValueAs(0, "display_order"); if (!results.isNull(0, "file_name")) // file name IS SET in database filename = sanitizeFilename(results.valueAsString(0, "file_name")); if (filename.empty()) // filename was not set in database or was not impossible { // to sanitize (eg reserved name in windows 'COM1') std::ostringstream tmp; if (datum != -1) [[likely]] { // get datestring std::time_t epoch = datum / 1000; tmp << std::put_time(std::localtime(&epoch), "signal-%Y-%m-%d-%H%M%S"); //tmp << "." << datum % 1000; } else tmp << "signal"; // get file ext std::string mime = results.valueAsString(0, d_part_ct); std::string ext = std::string(MimeTypes::getExtension(mime)); if (ext.empty()) { ext = "attach"; Logger::warning("mimetype not found in database (", mime, ") -> saving as '", tmp.str(), ".", ext, "'"); } //build filename filename = tmp.str() + ((order) ? ("_" + bepaald::toString(order)) : "") + "." + ext; } // std::cout << "FILENAME: " << filename << std::endl; std::string targetdir = dir; if (fullbackup && !results.isNull(0, "thread_id") && !results.isNull(0, "chatpartner") && !results.isNull(0, d_mms_type)) { long long int tid = results.getValueAs(0, "thread_id"); std::string chatpartner = sanitizeFilename(results.valueAsString(0, "chatpartner")); if (chatpartner.empty()) chatpartner = "Contact " + bepaald::toString(tid); int idx_of_thread = -1; if ((idx_of_thread = bepaald::findIdxOf(conversations.first, tid)) == -1) // idx not found { if (std::find(conversations.second.begin(), conversations.second.end(), chatpartner) == conversations.second.end()) // chatpartner not used yet { // add it conversations.first.push_back(tid); conversations.second.push_back(chatpartner); idx_of_thread = conversations.second.size() - 1; } else // new conversation, but another conversation with same name already exists! { // get unique conversation name chatpartner += "(2)"; while (std::find(conversations.second.begin(), conversations.second.end(), chatpartner) != conversations.second.end()) chatpartner += "(2)"; conversations.first.push_back(tid); conversations.second.push_back(chatpartner); idx_of_thread = conversations.second.size() - 1; } } // else, thread was found, use the name that was used before // create dir if not exists if (!bepaald::isDir(dir + "/" + conversations.second[idx_of_thread])) { // std::cout << " Creating subdirectory '" << conversations.second[idx_of_thread] << "' for conversation..." << std::endl; if (!bepaald::createDir(dir + "/" + conversations.second[idx_of_thread])) { //std::cout << " ERROR creating directory '" << dir << "/" << conversations.second[idx_of_thread] << "'" << std::endl; Logger::error("Failed to create directory '", dir, "/", conversations.second[idx_of_thread], "'"); continue; } } long long int msg_box = results.getValueAs(0, d_mms_type); targetdir = dir + "/" + conversations.second[idx_of_thread] + "/" + (Types::isOutgoing(msg_box) ? "sent" : "received"); // create dir if not exists if (!bepaald::isDir(targetdir)) { //std::cout << " Creating subdirectory '" << targetdir << "' for conversation..." << std::endl; if (!bepaald::createDir(targetdir)) { Logger::error("Failed to create directory '", targetdir, "'"); continue; } } } // make filename unique if (!makeFilenameUnique(targetdir, &filename)) { Logger::error("getting unique filename for '", targetdir, "/", filename, "'"); continue; } /* while (bepaald::fileOrDirExists(targetdir + "/" + filename)) { //std::cout << std::endl << "File exists: " << targetdir << "/" << filename << " -> "; std::filesystem::path p(filename); std::regex numberedfile(".*( \\(([0-9]*)\\))$"); std::smatch sm; std::string filestem(p.stem().string()); std::string ext(p.extension().string()); int counter = 2; if (regex_match(filestem, sm, numberedfile) && sm.size() >= 3 && sm[2].matched) { // increase the counter counter = bepaald::toNumber(sm[2]) + 1; // remove " (xx)" part from stem filestem.erase(sm[1].first, sm[1].second); } filename = filestem + " (" + bepaald::toString(counter) + ")" + p.extension().string(); //std::cout << filename << std::endl; } */ std::ofstream attachmentstream(targetdir + "/" + filename, std::ios_base::binary); if (!attachmentstream.is_open()) { Logger::error("Failed to open file for writing: '", targetdir, "/", filename, "'"); continue; } ++count; if (!attachmentstream.write(reinterpret_cast(a->attachmentData()), a->attachmentSize())) { Logger::error("Failed to write data to file: '", targetdir, "/", filename, "'"); a->clearData(); continue; } attachmentstream.close(); // need to close, or the auto-close will change files mtime again. a->clearData(); if (!setFileTimeStamp(targetdir + "/" + filename, datum)) [[unlikely]] Logger::warning("Failed to set timestamp for attachment '", targetdir, "/", filename, "'"); // !! ifdef c++20 //std::error_code ec; //std::filesystem::last_write_time(dir + "/" + chatpartner + "/" + filename, std::chrono::clock_cast(datum / 1000), ec); } Logger::message_overwrite("Saving attachments... done.", Logger::Control::ENDOVERWRITE); return true; } signalbackup-tools-20250313-1/signalbackup/escapexmlstring.cc000066400000000000000000000102521476450434500241100ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::escapeXmlString(std::string *str) const { size_t pos = 0; while (pos != str->size()) { if ((*str)[pos] == '&') { str->replace(pos, 1, "&"); pos += STRLEN("&"); continue; } if ((*str)[pos] == '<') { str->replace(pos, 1, "<"); pos += STRLEN("<"); continue; } if ((*str)[pos] == '>') { str->replace(pos, 1, ">"); pos += STRLEN(">"); continue; } if ((*str)[pos] == '"') { str->replace(pos, 1, """); pos += STRLEN("""); continue; } if ((*str)[pos] == '\'') { str->replace(pos, 1, "'"); pos += STRLEN("'"); continue; } // [^\u0020-\uD7FF] <-- range that's escaped (note the ^) /* under \u0020 = control chars (escape, linebreak, etc...) */ if ((static_cast((*str)[pos]) & 0xFF) < 0x20) { std::string rep = "&#" + bepaald::toString(static_cast((*str)[pos]) & 0xFF) + ";"; str->replace(pos, 1, rep); pos += rep.length(); continue; } /* If you know that the data is UTF-8, then you just have to check the high bit: 0xxxxxxx = single-byte ASCII character 1xxxxxxx = part of multi-byte character Or, if you need to distinguish lead/trail bytes: 10xxxxxx = 2nd, 3rd, or 4th byte of multi-byte character 110xxxxx = 1st byte of 2-byte character 1110xxxx = 1st byte of 3-byte character 11110xxx = 1st byte of 4-byte character */ /* Over 0xd7ff. */ //0x800 - 0xffff is only 3 byte utf chars, and are represented by utf16 points directly? if (((*str)[pos] & 0b11110000) == 0b11100000) { if (pos + 2 >= str->size()) { ++pos; continue; } uint32_t unicode = 0; unicode += (static_cast((*str)[pos] & 0b0001111) << 12); unicode += (static_cast((*str)[pos + 1] & 0b0111111) << 6); unicode += (static_cast((*str)[pos + 2] & 0b0111111)); std::string rep = "&#" + bepaald::toString(unicode) + ";"; str->replace(pos, 3, rep); pos += rep.length(); continue; } // Beyond 0xffff is only 4 byte utf chars if (((*str)[pos] & 0b11111000) == 0b11110000) // or 0b11110000 { if (pos + 3 >= str->size()) { ++pos; continue; } /* bytes of unicode char: UTF8: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX UNICODE: ........ ...XXX_XX XXXX_XXXX XX_XXXXXX {21} U' (UNICODE - 0x10000): yyyyyyyyyzzzzzzzzzz UTF16 LOW: 110110yyyyyyyyyy // DONT REMEMBER WHY IM DOING THIS, UTF16 HI : 110111zzzzzzzzzz // THE ORIGINAL PROBABLY OUTPUT UTF16? BUT I DONT NEED TO */ uint32_t unicode = 0; unicode += (static_cast((*str)[pos] & 0b0000111) << 18); unicode += (static_cast((*str)[pos + 1] & 0b0111111) << 12); unicode += (static_cast((*str)[pos + 2] & 0b0111111) << 6); unicode += (static_cast((*str)[pos + 3] & 0b0111111)); //unicode -= 0x10000; //std::string rep = "&#" + bepaald::toString(0xd800 + (unicode >> 10)) + ";&#" + bepaald::toString(0xdc00 + (unicode & 0x3FF)) + ";"; std::string rep = "&#" + bepaald::toString(unicode) + ";"; str->replace(pos, 4, rep); pos += rep.length(); continue; } ++pos; } } signalbackup-tools-20250313-1/signalbackup/exportbackup.cc000066400000000000000000000027471476450434500234210ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../common_filesystem.h" bool SignalBackup::exportBackup(std::string const &filename, std::string const &passphrase, bool overwrite, bool keepattachmentdatainmemory, bool onlydb) { // if output is existing directory, or doesn't exist but ends in directory delim. -> output to dir if ((bepaald::fileOrDirExists(filename) && bepaald::isDir(filename)) || (!bepaald::fileOrDirExists(filename) && (filename.back() == '/' || filename.back() == std::filesystem::path::preferred_separator))) return exportBackupToDir(filename, overwrite, keepattachmentdatainmemory, onlydb); // export to file return exportBackupToFile(filename, passphrase, overwrite, keepattachmentdatainmemory); } signalbackup-tools-20250313-1/signalbackup/exportcsv.cc000066400000000000000000000046101476450434500227360ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::duplicateQuotes(std::string *s) const { size_t pos = 0; while ((pos = s->find('"', pos)) != std::string::npos) s->insert((++pos)++, 1, '"'); // this is beautiful ;P } bool SignalBackup::exportCsv(std::string const &filename, std::string const &table, bool overwrite) const { if (!overwrite && (bepaald::fileOrDirExists(filename) && !bepaald::isDir(filename))) { Logger::message("File ", filename, " exists, use --overwrite to overwrite"); return false; } // output header std::ofstream outputfile(filename, std::ios_base::binary); if (!outputfile.is_open()) { Logger::error("Failed to open output file '", filename, "' for writing"); return false; } SqliteDB::QueryResults results; if (!d_database.exec("SELECT * FROM " + table, &results)) { Logger::error("Gathering data from database"); return false; } // output header for (unsigned int i = 0; i < results.columns(); ++i) outputfile << results.header(i) << ((i == results.columns() - 1) ? '\n' : ','); // output data for (unsigned int j = 0; j < results.rows(); ++j) for (unsigned int i = 0; i < results.columns(); ++i) { std::string vas = results.valueAsString(j, i); duplicateQuotes(&vas); bool escape = (vas.find_first_of(",\"\n") != std::string::npos) || // contains newline, quote or comma (!vas.empty() && (std::find_if(vas.begin(), vas.end(), [](char c){ return !std::isspace(c); }) == vas.end())); // is all whitespace (and non empty) outputfile << (escape ? "\"" : "") << vas << (escape ? "\"" : "") << ((i == results.columns() - 1) ? '\n' : ','); } return true; } signalbackup-tools-20250313-1/signalbackup/exporthtml.cc000066400000000000000000001540371476450434500231200ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ /* Many thanks to Gertjan van den Burg (https://github.com/GjjvdBurg) for his original project (used with permission) without which this function would not have come together so quickly (if at all). */ #include "signalbackup.ih" #include "../scopeguard/scopeguard.h" #include bool SignalBackup::exportHtml(std::string const &directory, std::vector const &limittothreads, std::vector const &daterangelist, std::string const &splitby, long long int split, std::string const &selfphone, bool calllog, bool searchpage, bool stickerpacks, bool migrate, bool overwrite, bool append, bool lighttheme, bool themeswitching, bool addexportdetails, bool blocked, bool fullcontacts, bool settings, bool receipts, bool originalfilenames, bool linkify, bool chatfolders, bool compact, bool pagemenu, std::vector const &ignoremediatypes) { Logger::message("Starting HTML export to '", directory, "'"); // v170 and above should work. Anything below will first migrate (I believe anything down to ~23 should more or less work) bool databasemigrated = false; MemSqliteDB backup_database; if (d_databaseversion < 170 || migrate) { SqliteDB::copyDb(d_database, backup_database); if (!migrateDatabase(d_databaseversion, 170)) // migrate == TRUE, but migration fails { Logger::error("Failed to migrate currently unsupported database version (", d_databaseversion, ")." " Please upgrade your database"); SqliteDB::copyDb(backup_database, d_database); return false; } databasemigrated = true; } ScopeGuard restore_migrated_database([&]() { if (databasemigrated) SqliteDB::copyDb(backup_database, d_database); }); if (originalfilenames && append) [[unlikely]] Logger::warning("Options 'originalfilenames' and 'append' are incompatible"); // // check if dir exists, create if not if (!prepareOutputDirectory(directory, overwrite, !originalfilenames /*allowappend only allowed when not using original filenames*/, append)) return false; // check and warn about selfid & note-to-self thread long long int note_to_self_thread_id = -1; d_selfid = selfphone.empty() ? scanSelf() : d_database.getSingleResultAs("SELECT _id FROM recipient WHERE " + d_recipient_e164 + " = ?", selfphone, -1); if (d_selfid == -1) { if (!selfphone.empty()) Logger::warning("Failed to determine id of 'self'."); else // if (selfphone.empty()) Logger::warning("Failed to determine Note-to-self thread. Consider passing `--setselfid \"[phone]\"' to set it manually"); } else { note_to_self_thread_id = d_database.getSingleResultAs("SELECT _id FROM thread WHERE " + d_thread_recipient_id + " = ?", d_selfid, -1); d_selfuuid = bepaald::toLower(d_database.getSingleResultAs("SELECT " + d_recipient_aci + " FROM recipient WHERE _id = ?", d_selfid, std::string())); } std::vector threads = ((limittothreads.empty() || (limittothreads.size() == 1 && limittothreads[0] == -1)) ? threadIds() : limittothreads); std::vector excludethreads; // threads excluded by limittodates... std::map recipient_info; // set where-clause for date requested std::vector> dateranges; if (daterangelist.size() % 2 == 0) for (unsigned int i = 0; i < daterangelist.size(); i += 2) dateranges.push_back({daterangelist[i], daterangelist[i + 1]}); std::string datewhereclause; std::string datewhereclausecalllog; long long int maxdate = -1; for (unsigned int i = 0; i < dateranges.size(); ++i) { bool needrounding = false; long long int startrange = dateToMSecsSinceEpoch(dateranges[i].first); long long int endrange = dateToMSecsSinceEpoch(dateranges[i].second, &needrounding); if (startrange == -1 || endrange == -1 || endrange < startrange) { Logger::error("Skipping range: '", dateranges[i].first, " - ", dateranges[i].second, "'. Failed to parse or invalid range."); Logger::error_indent(startrange, " ", endrange); continue; } Logger::message(" Using range: ", dateranges[i].first, " - ", dateranges[i].second, " (", startrange, " - ", endrange, ")"); if (endrange > maxdate) maxdate = endrange; if (needrounding)// if called with "YYYY-MM-DD HH:MM:SS" endrange += 999; // to get everything in the second specified... dateranges[i].first = bepaald::toString(startrange); dateranges[i].second = bepaald::toString(endrange); datewhereclause += (datewhereclause.empty() ? " AND (" : " OR ") + "date_received BETWEEN "s + dateranges[i].first + " AND " + dateranges[i].second; datewhereclausecalllog += (datewhereclausecalllog.empty() ? " AND (" : " OR ") + "timestamp BETWEEN "s + dateranges[i].first + " AND " + dateranges[i].second; if (i == dateranges.size() - 1) { datewhereclause += ')'; datewhereclausecalllog += ')'; } } std::sort(dateranges.begin(), dateranges.end()); // // get releasechannel thread, to skip // int releasechannel = -1; // for (auto const &skv : d_keyvalueframes) // if (skv->key() == "releasechannel.recipient_id") // releasechannel = bepaald::toNumber(skv->value()); SqliteDB::QueryResults search_idx_results; std::ofstream searchidx; bool searchidx_write_started = false; long long int searchidx_page_idx = 0; std::map searchidx_page_idx_map; // start search index page if (searchpage) { searchidx.open(WIN_LONGPATH(directory + "/" + "searchidx.js"), std::ios_base::binary); if (!searchidx.is_open()) { Logger::error("Failed to open 'searchidx.js' for writing"); return false; } searchidx << "message_idx = [" << std::endl; } std::string exportdetails_html; if (addexportdetails) { std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); std::string filename = d_filename; HTMLescapeString(&filename); std::string options = "--exporthtml " + directory; HTMLescapeString(&options); if (!limittothreads.empty()) { options += "
--limittothreads "; for (unsigned int i = 0; i < limittothreads.size(); ++i) options += bepaald::toString(limittothreads[i]) + (i < limittothreads.size() - 1 ? "," : ""); } if (!daterangelist.empty()) { options += "
--limittodates "; for (unsigned int i = 0; i < daterangelist.size(); i += 2) options += daterangelist[i] + "–" + daterangelist[i + 1] + (i < daterangelist.size() - 1 ? "," : ""); } if (split > -1) options += "
--split " + bepaald::toString(split); if (!splitby.empty()) options += "
--split-by " + splitby; if (receipts) options += "
--includereceipts"; if (calllog) options += "
--includecalllog"; if (blocked) options += "
--includeblockedlist"; if (settings) options += "
--includesettings"; if (addexportdetails) options += "
--addexportdetails"; if (searchpage) options += "
--searchpage"; if (stickerpacks) options += "
--stickerpacks"; if (overwrite) options += "
--overwrite"; if (append) options += "
--append"; if (d_verbose) options += "
--verbose"; if (lighttheme) options += "
--light"; if (themeswitching) options += "
--themeswitching"; if (originalfilenames) options += "
--originalfilenames"; if (linkify) options += "
--linkify"; if (chatfolders) options += "
--chatfolders"; SqliteDB::QueryResults res; d_database.exec("SELECT MIN(" + d_mms_table + ".date_received) AS 'mindate', MAX(" + d_mms_table + ".date_received) AS 'maxdate' FROM " + d_mms_table, &res); std::string date_range = bepaald::toDateString(res.valueAsInt(0, "mindate") / 1000, "%Y-%m-%d %H:%M:%S") + "–" + bepaald::toDateString(res.valueAsInt(0, "maxdate") / 1000, "%Y-%m-%d %H:%M:%S"); exportdetails_html = "
\n" "
Export details
\n" "
Exported by signalbackup-tools version:
"s + VERSIONDATE + "
\n" "
Exported date:
" + bepaald::toDateString(now, "%Y-%m-%d %H:%M:%S") + "
\n" "
Export options:
" + options + "
\n" "
Backup file:
" + filename + "
\n" + (d_fd ? ("
Backup file size:
" + bepaald::toString(d_fd->total()) + " bytes
\n") : "") + "
Backup range:
" + date_range + "
\n" "
Backup file version:
" + bepaald::toString(d_backupfileversion) + "
\n" "
Database version:
" + bepaald::toString(d_databaseversion) + "
\n" "
\n"; } std::string periodsplitformat; std::string readablesplitformat; if (!splitby.empty()) { auto icasecompare = [](std::string const &a, std::string const &b) { return std::equal(a.begin(), a.end(), b.begin(), b.end(), [](char ca, char cb) { return std::tolower(ca) == std::tolower(cb); }); }; if (icasecompare(splitby, "year")) { periodsplitformat = "%Y"; readablesplitformat = "%Y"; } else if (icasecompare(splitby, "month")) { periodsplitformat = "%Y%m"; readablesplitformat = "%b, %Y"; } else if (icasecompare(splitby, "week")) { periodsplitformat = "%Y%W"; readablesplitformat = "week %W, %Y"; } else if (icasecompare(splitby, "day")) { periodsplitformat = "%Y%j"; readablesplitformat = "%b %d, %Y"; } else Logger::warning("Ignoring invalid 'split-by'-value ('", splitby, "')"); } for (unsigned int t_idx = 0; t_idx < threads.size(); ++t_idx) { int t = threads[t_idx]; // if (t == releasechannel) // { // std::cout << "INFO: Skipping releasechannel thread..." << std::endl; // continue; // } Logger::message("Dealing with thread ", t); bool is_note_to_self = (t == note_to_self_thread_id); // check if this is releasechannel: bool is_releasechannel = false; for (auto const &skv : d_keyvalueframes) if (skv->key() == "releasechannel.recipient_id") is_releasechannel = (t == d_database.getSingleResultAs("SELECT _id FROM thread WHERE " + d_thread_recipient_id + " = ?", bepaald::toNumber(skv->value()), -1)); // get recipient_id for thread; SqliteDB::QueryResults recid; long long int thread_recipient_id = -1; if (!d_database.exec("SELECT _id," + d_thread_recipient_id + " FROM thread WHERE _id = ?", t, &recid) || recid.rows() != 1 || (thread_recipient_id = recid.valueAsInt(0, d_thread_recipient_id)) == -1) { Logger::error("Failed to find recipient_id for thread (", t, ")... skipping"); continue; } long long int thread_id = recid.getValueAs(0, "_id"); bool isgroup = false; SqliteDB::QueryResults groupcheck; d_database.exec("SELECT group_id FROM recipient WHERE _id = ? AND group_id IS NOT NULL", thread_recipient_id, &groupcheck); if (groupcheck.rows()) isgroup = true; // now get all messages SqliteDB::QueryResults messages; d_database.exec("SELECT "s "_id, " + d_mms_recipient_id + ", " + (d_database.tableContainsColumn(d_mms_table, "to_recipient_id") ? "to_recipient_id" : "-1") + " AS to_recipient_id, body, " "MIN(date_received, " + d_mms_date_sent + ") AS bubble_date, " "date_received, " + d_mms_date_sent + ", " + d_mms_type + ", " + (!periodsplitformat.empty() ? "strftime('" + periodsplitformat + "', IFNULL(date_received, 0) / 1000, 'unixepoch', 'localtime')" : "''") + " AS periodsplit, " "quote_id, quote_author, quote_body, quote_mentions, quote_missing, " "attcount, reactioncount, mentioncount, " + d_mms_delivery_receipts + ", " + d_mms_read_receipts + ", IFNULL(remote_deleted, 0) AS remote_deleted, " "IFNULL(view_once, 0) AS view_once, expires_in, " + d_mms_ranges + ", shared_contacts, " + (d_database.tableContainsColumn(d_mms_table, "original_message_id") ? "original_message_id, " : "") + + (d_database.tableContainsColumn(d_mms_table, "revision_number") ? "revision_number, " : "") + + (d_database.tableContainsColumn(d_mms_table, "parent_story_id") ? "parent_story_id, " : "") + + (d_database.tableContainsColumn(d_mms_table, "message_extras") ? "message_extras, " : "") + + (d_database.tableContainsColumn(d_mms_table, "receipt_timestamp") ? "receipt_timestamp, " : "-1 AS receipt_timestamp, ") + // introduced in 117 "json_extract(link_previews, '$[0].url') AS link_preview_url, " "json_extract(link_previews, '$[0].title') AS link_preview_title, " "json_extract(link_previews, '$[0].description') AS link_preview_description " "FROM " + d_mms_table + " " // get attachment count for message: "LEFT JOIN (SELECT " + d_part_mid + " AS message_id, COUNT(*) AS attcount FROM " + d_part_table + " GROUP BY message_id) AS attmnts ON " + d_mms_table + "._id = attmnts.message_id " // get reaction count for message: "LEFT JOIN (SELECT message_id, COUNT(*) AS reactioncount FROM reaction GROUP BY message_id) AS rctns ON " + d_mms_table + "._id = rctns.message_id " // get mention count for message: "LEFT JOIN (SELECT message_id, COUNT(*) AS mentioncount FROM mention GROUP BY message_id) AS mntns ON " + d_mms_table + "._id = mntns.message_id " "WHERE thread_id = ?" + datewhereclause + + (d_database.tableContainsColumn(d_mms_table, "latest_revision_id") ? " AND latest_revision_id IS NULL " : " ") + + (d_database.tableContainsColumn(d_mms_table, "story_type") ? " AND story_type = 0 OR story_type IS NULL " : "") + // storytype NONE(0), STORY_WITH(OUT)_REPLIES(1/2), TEXT_...(3/4) " ORDER BY date_received ASC", t, &messages); if (messages.rows() == 0) { if (d_verbose) [[unlikely]] Logger::message("Thread appears empty. Skipping..."); excludethreads.push_back(t); continue; } // get all recipients in thread (group member (past and present), quote/reaction authors, mentions) std::set all_recipients_ids = getAllThreadRecipients(t); //try to set any missing info on recipients setRecipientInfo(all_recipients_ids, &recipient_info); //for (auto const &ri : recipient_info) // std::cout << ri.first << ": " << ri.second.display_name << std::endl; // get conversation name, sanitize it and create dir if (recipient_info.find(thread_recipient_id) == recipient_info.end()) { Logger::error("Failed set recipient info for thread (", t, ")... skipping"); continue; } std::string basethreaddir(is_note_to_self ? "Note to Self" : recipient_info[thread_recipient_id].display_name); WIN_LIMIT_FILENAME_LENGTH(basethreaddir); std::string threaddir(sanitizeFilename(basethreaddir) + " (_id"s + bepaald::toString(thread_id) + ")"); if (compact) [[unlikely]] threaddir = "id" + bepaald::toString(thread_id); if (bepaald::fileOrDirExists(directory + "/" + threaddir)) { if (!bepaald::isDir(directory + "/" + threaddir)) { Logger::error("dir is regular file"); return false; } if (!append && !overwrite) // should be impossible at this point.... { Logger::error("Refusing to overwrite existing directory"); return false; } } else if (!bepaald::createDir(directory + "/" + threaddir)) // try to create it { Logger::error("Failed to create directory `", directory, "/", threaddir, "'", " (errno: ", std::strerror(errno), ")"); // note: errno is not required to be set by std // temporary !! { std::error_code ec; std::filesystem::space_info const si = std::filesystem::space(directory, ec); if (!ec) { Logger::message("Available: ", static_cast(si.available)); Logger::message(" Filesize: ", d_fd->total()); } } return false; } // now append messages to html std::map written_avatars; // maps recipient_ids to the path of a written avatar file. unsigned int messagecount = 0; // current message unsigned int max_msg_per_page = messages.rows(); int pagenumber = 0; // current page int totalpages = 1; if (split > 0) { totalpages = (messages.rows() / split) + (messages.rows() % split > 0 ? 1 : 0); max_msg_per_page = messages.rows() / totalpages + (messages.rows() % totalpages ? 1 : 0); } std::vector split_page_names; if (!periodsplitformat.empty()) { SqliteDB::QueryResults pagenames; if (d_database.exec("SELECT " "strftime('" + periodsplitformat + "', IFNULL(date_received, 0) / 1000, 'unixepoch', 'localtime') AS splitdate, date_received / 1000 AS date_secs " "FROM " + d_mms_table + " WHERE thread_id = ? " + datewhereclause + " GROUP BY splitdate", t, &pagenames)) { totalpages = pagenames.rows(); for (unsigned int p = 0; p < pagenames.rows(); ++p) split_page_names.emplace_back(bepaald::toDateString(pagenames.valueAsInt(p, "date_secs", 0), readablesplitformat)); } } else if (totalpages > 1) for (int p = 0; p < totalpages; ++p) split_page_names.emplace_back("Page " + bepaald::toString(p + 1)); // std::cout << "Split: " << split << std::endl; // std::cout << "N MSG: " << messages.rows() << std::endl; // std::cout << "MAX PER PAGE: " << max_msg_per_page << std::endl; // std::cout << "N PAGES: " << totalpages << std::endl; unsigned int daterangeidx = 0; while (true) { std::string previous_period_split_string(messages(messagecount, "periodsplit")); std::string previous_day_change; // create output-file std::string raw_base_filename = (is_note_to_self ? "Note to Self" : recipient_info[thread_recipient_id].display_name); WIN_LIMIT_FILENAME_LENGTH(raw_base_filename); std::string sanitized_base_filename(sanitizeFilename(raw_base_filename)); std::string filename(sanitized_base_filename + (pagenumber > 0 ? "_" + bepaald::toString(pagenumber) : "") + ".html"); if (compact) [[unlikely]] { sanitized_base_filename.clear(); filename = bepaald::toString(pagenumber) + ".html"; } WIN_CHECK_PATH_LENGTH(directory + "/" + threaddir + "/" + filename); std::ofstream htmloutput(WIN_LONGPATH(directory + "/" + threaddir + "/" + filename), std::ios_base::binary); if (!htmloutput.is_open()) { Logger::error("Failed to open '", directory, "/", threaddir, "/", filename, "' for writing."); return false; } // create start of html (css, head, start of body HTMLwriteStart(htmloutput, thread_recipient_id, directory, threaddir, isgroup, is_note_to_self, is_releasechannel, all_recipients_ids, &recipient_info, &written_avatars, overwrite, append, lighttheme, themeswitching, searchpage, addexportdetails, pagemenu && totalpages > 1); while (messagecount < (max_msg_per_page * (pagenumber + 1)) && messages(messagecount, "periodsplit") == previous_period_split_string) { long long int msg_id = messages.getValueAs(messagecount, "_id"); long long int msg_recipient_id = messages.valueAsInt(messagecount, d_mms_recipient_id); if (msg_recipient_id == -1) [[unlikely]] { Logger::warning("Failed to get message recipient id. Skipping."); continue; } long long int original_message_id = (d_database.tableContainsColumn(d_mms_table, "original_message_id") ? messages.valueAsInt(messagecount, "original_message_id") : -1); std::string readable_date = bepaald::toDateString(messages.getValueAs(messagecount, "bubble_date") / 1000, //(/*(original_message_id != -1) ? "date_received" : */d_mms_date_sent)) / 1000, "%b %d, %Y %H:%M:%S"); std::string readable_date_day = bepaald::toDateString(messages.getValueAs(messagecount, "bubble_date") / 1000, //(/*(original_message_id != -1) ? "date_received" : */d_mms_date_sent)) / 1000, "%b %d, %Y"); bool incoming = !Types::isOutgoing(messages.getValueAs(messagecount, d_mms_type)); bool is_deleted = messages.getValueAs(messagecount, "remote_deleted") == 1; bool is_viewonce = messages.getValueAs(messagecount, "view_once") == 1; std::string body = messages.valueAsString(messagecount, "body"); std::string shared_contacts = messages.valueAsString(messagecount, "shared_contacts"); std::string quote_body = messages.valueAsString(messagecount, "quote_body"); long long int expires_in = messages.getValueAs(messagecount, "expires_in"); long long int type = messages.getValueAs(messagecount, d_mms_type); bool hasquote = !messages.isNull(messagecount, "quote_id") && messages.getValueAs(messagecount, "quote_id"); bool quote_missing = messages.valueAsInt(messagecount, "quote_missing", 0) != 0; bool story_reply = (d_database.tableContainsColumn(d_mms_table, "parent_story_id") ? messages.valueAsInt(messagecount, "parent_story_id", 0) : 0); long long int attachmentcount = messages.valueAsInt(messagecount, "attcount", 0); long long int reactioncount = messages.valueAsInt(messagecount, "reactioncount", 0); long long int mentioncount = messages.valueAsInt(messagecount, "mentioncount", 0); SqliteDB::QueryResults attachment_results; if (attachmentcount > 0) d_database.exec("SELECT " + d_part_table + "._id, " + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id"s : "-1 AS unique_id") + ", " + d_part_ct + ", " "file_name, " + d_part_pending + ", " + (d_database.tableContainsColumn(d_part_table, "caption") ? "caption, "s : std::string()) + "sticker_pack_id, " + d_mms_table + ".date_received AS date_received " "FROM " + d_part_table + " " "LEFT JOIN " + d_mms_table + " ON " + d_mms_table + "._id = " + d_part_table + "." + d_part_mid + " " "WHERE " + d_part_mid + " IS ? " "AND quote IS ? " " ORDER BY display_order ASC, " + d_part_table + "._id ASC", {msg_id, 0}, &attachment_results); // check attachments for long message body -> replace cropped body & remove from attachment results setLongMessageBody(&body, &attachment_results); SqliteDB::QueryResults quote_attachment_results; if (attachmentcount > 0) d_database.exec("SELECT " + d_part_table + "._id, " + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id"s : "-1 AS unique_id") + ", " + d_part_ct + ", " "file_name, " + d_part_pending + ", " + (d_database.tableContainsColumn(d_part_table, "caption") ? "caption, "s : std::string()) + "sticker_pack_id, " + d_mms_table + ".date_received AS date_received " "FROM " + d_part_table + " " "LEFT JOIN " + d_mms_table + " ON " + d_mms_table + "._id = " + d_part_table + "." + d_part_mid + " " "WHERE " + d_part_mid + " IS ? " "AND quote IS ? " " ORDER BY display_order ASC, " + d_part_table + "._id ASC", {msg_id, 1}, "e_attachment_results); SqliteDB::QueryResults mention_results; if (mentioncount > 0) d_database.exec("SELECT recipient_id, range_start, range_length FROM mention WHERE message_id IS ?", msg_id, &mention_results); SqliteDB::QueryResults reaction_results; if (reactioncount > 0) d_database.exec("SELECT emoji, author_id, DATETIME(date_sent / 1000, 'unixepoch', 'localtime') AS 'date_sent', DATETIME(date_received / 1000, 'unixepoch', 'localtime') AS 'date_received' " "FROM reaction WHERE message_id IS ?", msg_id, &reaction_results); SqliteDB::QueryResults edit_revisions; if (original_message_id != -1 && d_database.tableContainsColumn(d_mms_table, "revision_number")) d_database.exec("SELECT _id,body,date_received," + d_mms_date_sent + ",revision_number FROM " + d_mms_table + " WHERE _id = ?1 OR original_message_id = ?1 ORDER BY " + d_mms_date_sent + " ASC", // skip actual current message original_message_id, &edit_revisions); bool issticker = (attachment_results.rows() == 1 && !attachment_results.isNull(0, "sticker_pack_id")); IconType icon = IconType::NONE; if (Types::isStatusMessage(type)) { // identityVerified and identityDefault ("You marked your safety number with XXX (un)verified") are // a little different from other (nongroup) statusmessages, in that the name to be filled in is not // the FROM_recipient_id (as is the case with others: "XXX reset the secure session", "XXX set the // disappearing message timer to ..."), but the TO_recipient. For these message FROM is always self // ("YOU set YOUR safety number..."), but we need the TO_id to fill in the name. // Note this is even true in group threads, where outgoing messages are usually always from you and // to the group, but not for these two status messages. // But all of the above is only true for (newer) databases that _have_ from_ and to_recipient_ids, // in older databases the target-name is recipient_id in all cases... long long int target_rid = msg_recipient_id; if ((Types::isIdentityVerified(type) || Types::isIdentityDefault(type)) && messages.valueAsInt(messagecount, "to_recipient_id") != -1) [[unlikely]] target_rid = messages.valueAsInt(messagecount, "to_recipient_id"); // decode from body if (body not empty) OR (message_extras not available) if (!body.empty() || !(d_database.tableContainsColumn(d_mms_table, "message_extras") && messages.valueHasType, size_t>>(messagecount, "message_extras"))) body = decodeStatusMessage(body, messages.getValueAs(messagecount, "expires_in"), type, getRecipientInfoFromMap(&recipient_info, target_rid).display_name, &icon); else if (d_database.tableContainsColumn(d_mms_table, "message_extras") && messages.valueHasType, size_t>>(messagecount, "message_extras")) body = decodeStatusMessage(messages.getValueAs, size_t>>(messagecount, "message_extras"), messages.getValueAs(messagecount, "expires_in"), type, getRecipientInfoFromMap(&recipient_info, target_rid).display_name, &icon); } // prep body (scan emoji? -> in ) and handle mentions... // if (prepbody) std::vector> mentions; for (unsigned int mi = 0; mi < mention_results.rows(); ++mi) mentions.emplace_back(std::make_tuple(mention_results.getValueAs(mi, "recipient_id"), mention_results.getValueAs(mi, "range_start"), mention_results.getValueAs(mi, "range_length"))); std::pair, size_t> brdata(nullptr, 0); if (!messages.isNull(messagecount, d_mms_ranges)) brdata = messages.getValueAs, size_t>>(messagecount, d_mms_ranges); bool only_emoji = HTMLprepMsgBody(&body, mentions, &recipient_info, incoming, brdata, linkify, false /*isquote*/); bool nobackground = false; if ((only_emoji && !hasquote && !attachment_results.rows()) || // if no quote etc issticker) // or sticker nobackground = true; // same for quote_body! mentions.clear(); std::pair, size_t> quote_mentions{nullptr, 0}; if (!messages.isNull(messagecount, "quote_mentions")) quote_mentions = messages.getValueAs, size_t>>(messagecount, "quote_mentions"); HTMLprepMsgBody("e_body, mentions, &recipient_info, incoming, quote_mentions, linkify, true); // insert date-change message if (readable_date_day != previous_day_change) { htmloutput << R"(

)" << readable_date_day << R"(

)" << std::endl << std::endl; } previous_day_change = readable_date_day; previous_period_split_string = messages(messagecount, "periodsplit"); /* LINKIFY? Notes: - currently this matches 'yes.combine them please' as 'yes.com'. (maybe try to match per word?) - dont copy entire body, just match on stringview, and update it from suffix start? - this interacts with prepbody/escapehtml std::regex url_regex("(?:(?:(?:(?:(?:http|ftp|https|localhost):\\/\\/)|(?:www\\.)|(?:xn--)){1}(?:[\\w_-]+(?:(?:\\.[\\w_-]+)+))(?:[\\w.,@?^=%&:\\/~+#-]*[\\w@?^=%&\\/~+#-])?)|(?:(?:[\\w_-]{2,200}(?:(?:\\.[\\w_-]+)*))(?:(?:\\.[\\w_-]+\\/(?:[\\w.,@?^=%&:\\/~+#-]*[\\w@?^=%&\\/~+#-])?)|(?:\\.(?:(?:org|com|net|edu|gov|mil|int|arpa|biz|info|unknown|one|ninja|network|host|coop|tech)|(?:jp|br|it|cn|mx|ar|nl|pl|ru|tr|tw|za|be|uk|eg|es|fi|pt|th|nz|cz|hu|gr|dk|il|sg|uy|lt|ua|ie|ir|ve|kz|ec|rs|sk|py|bg|hk|eu|ee|md|is|my|lv|gt|pk|ni|by|ae|kr|su|vn|cy|am|ke))))))(?!(?:(?:(?:ttp|tp|ttps):\\/\\/)|(?:ww\\.)|(?:n--)))"); std::smatch url_match_result; std::string body2 = body; while (std::regex_search(body2, url_match_result, url_regex)) { for (const auto &res : url_match_result) std::cout << "FOUND URL: " << res << std::endl; body2 = url_match_result.suffix(); } */ // collect data needed by writeMessage() HTMLMessageInfo msg_info({only_emoji, is_deleted, is_viewonce, isgroup, incoming, nobackground, hasquote, quote_missing, originalfilenames, overwrite, append, story_reply, type, expires_in, msg_id, msg_recipient_id, original_message_id, messagecount, &messages, "e_attachment_results, &attachment_results, &reaction_results, &edit_revisions, body, quote_body, readable_date, directory, threaddir, filename, messages(messagecount, "link_preview_url"), messages(messagecount, "link_preview_title") , messages(messagecount, "link_preview_description"), shared_contacts, icon }); HTMLwriteMessage(htmloutput, msg_info, &recipient_info, searchpage, receipts, ignoremediatypes); if (searchpage && (!Types::isStatusMessage(msg_info.type) && !msg_info.body.empty())) { if (auto it = searchidx_page_idx_map.find(msg_info.threaddir + "/" + sanitized_base_filename); it != searchidx_page_idx_map.end()) searchidx_page_idx = it->second; else searchidx_page_idx_map.emplace(msg_info.threaddir + "/" + sanitized_base_filename, ++searchidx_page_idx); // because the body is already escaped for html at this point, we get it fresh from database (and have sqlite do the json formatting) if (!d_database.exec("SELECT json_object(" "'id', " + d_mms_table + "._id, " "'b', " + d_mms_table + ".body, " "'f', " + d_mms_table + "." + d_mms_recipient_id + ", " "'tr', thread." + d_thread_recipient_id + ", " "'o', (" + d_mms_table + "." + d_mms_type + " & 0x1F) IN (2,11,21,22,23,24,25,26), " "'d', (" + d_mms_table + ".date_received / 1000 - 1404165600), " // lose the last three digits (miliseconds, they are never displayed anyway). // subtract "2014-07-01". Signals initial release was 2014-07-29, negative // numbers should work otherwise anyway. "'p', ?, " "'n', ?) AS line," + d_part_table + "._id AS rowid, " + (d_database.tableContainsColumn(d_part_table, "unique_id") ? d_part_table + ".unique_id AS uniqueid" : "-1 AS uniqueid") + " FROM " + d_mms_table + " " "LEFT JOIN thread ON thread._id IS " + d_mms_table + ".thread_id " "LEFT JOIN " + d_part_table + " ON " + d_part_table + "." + d_part_mid + " IS " + d_mms_table + "._id AND " + d_part_table + "." + d_part_ct + " = 'text/x-signal-plain' AND " + d_part_table + ".quote = 0 " "WHERE " + d_mms_table + "._id = ?", {searchidx_page_idx, pagenumber, msg_info.msg_id}, &search_idx_results) || search_idx_results.rows() < 1) [[unlikely]] { Logger::warning("Search_idx query failed or no results"); } else { if (search_idx_results.rows() > 1) [[unlikely]] Logger::warning("Unexpected number of results from search_idx query (", search_idx_results.rows(), " results, using first)"); std::string line = search_idx_results("line"); if (!line.empty()) [[likely]] { if (search_idx_results.valueAsInt(0, "rowid") != -1 /* && search_idx_results.valueAsInt(0, "uniqueid") != -1*/) { long long int rowid = search_idx_results.valueAsInt(0, "rowid"); long long int uniqueid = search_idx_results.valueAsInt(0, "uniqueid"); AttachmentFrame *a = d_attachments.at({rowid, uniqueid}).get(); std::string longbody = std::string(reinterpret_cast(a->attachmentData()), a->attachmentSize()); a->clearData(); longbody = d_database.getSingleResultAs("SELECT json_set(?, '$.b', ?)", {line, longbody}, std::string()); if (!longbody.empty()) [[likely]] line = longbody; } if (searchidx_write_started) [[likely]] searchidx << "," << std::endl; searchidx << " " << line; searchidx_write_started = true; } } } // set daterangeidx (which range were we in for the just written message...) for (unsigned int dri = 0; dri < dateranges.size(); ++dri) if (messages.getValueAs(messagecount, "date_received") > bepaald::toNumber(dateranges[dri].first) && messages.getValueAs(messagecount, "date_received") <= bepaald::toNumber(dateranges[dri].second)) { daterangeidx = dri; break; } if (++messagecount >= messages.rows()) break; // // BREAK THE CONVERSATION BOX BETWEEN SEPARATE DATE RANGES ON THE SAME PAGE // std::cout << daterangeidx << std::endl; // std::cout << "curm: " << messages.getValueAs(messagecount, "date_received") << std::endl; // std::cout << "rhig: " << bepaald::toNumber(dateranges[daterangeidx].second) << std::endl; // std::cout << "rlow: " << bepaald::toNumber(dateranges[daterangeidx + 1].first) << std::endl; // std::cout << (messages.getValueAs(messagecount, "date_received") > bepaald::toNumber(dateranges[daterangeidx].second) && // messages.getValueAs(messagecount, "date_received") <= bepaald::toNumber(dateranges[daterangeidx + 1].first)) << std::endl; if (!dateranges.empty() && daterangeidx < dateranges.size() - 1 && // dont split if it's the last range messages.getValueAs(messagecount, "date_received") > bepaald::toNumber(dateranges[daterangeidx].second)) { if (messagecount < (max_msg_per_page * (pagenumber + 1))) // dont break convo-box if we are moving to a new page (because of --split) { // std::cout << "SPLITTING! (rangeend(" << daterangeidx << "): " << dateranges[daterangeidx].second << ")" << std::endl; // std::cout << " ! (rangeend(" << daterangeidx << "): " << dateranges[daterangeidx + 1].first << ")" << std::endl; // std::cout << " ! " << messages.getValueAs(messagecount, "date_received") << std::endl; htmloutput << " \n" "
\n" "\n"; } } } htmloutput << "
\n" // closes conversation-box " \n" " \n" // closes conversation-wrapper "\n"; if (totalpages > 1) { std::string sanitized_filename(sanitized_base_filename); HTMLescapeUrl(&sanitized_filename); htmloutput << " \n" " \n" "\n"; } htmloutput << " \n" // closes controls-wrapper "\n" " \n" " \n" "\n"; if (themeswitching || searchpage || (pagemenu && totalpages > 1)) { htmloutput << "
\n"; if (pagemenu && totalpages > 1) { std::string sanitized_filename(sanitized_base_filename); HTMLescapeUrl(&sanitized_filename); htmloutput << "
\n" "
\n" "
\n" " \n" "
\n" "
\n" "
\n"; for (int pn = 0; pn < static_cast(split_page_names.size()); ++pn) { if (pn == pagenumber) htmloutput << "
\n" "
" << split_page_names[pn] << "
\n" "
\n"; else htmloutput << " 0 ? "_" + bepaald::toString(pn) : "")) << ".html\">\n" "
\n" "
" << split_page_names[pn] << "
\n" "
\n" "
\n"; } htmloutput << "
\n" "
\n" "
\n" "
\n"; } if (searchpage) { htmloutput << "
\n" " \n" " \n" " \n" " \n" "
\n"; } if (themeswitching) { htmloutput << "
\n" " \n" "
\n"; } htmloutput << "
\n" "\n"; } htmloutput << " \n"; // closes div id=page (I think) if (addexportdetails) htmloutput << '\n' << exportdetails_html << '\n'; if (themeswitching) { htmloutput << R"( )"; } htmloutput << " \n"; htmloutput << "\n"; ++pagenumber; if (messagecount >= messages.rows()) break; } } if (searchpage) { if (searchidx_write_started) [[likely]] searchidx << std::endl << "];" << std::endl; // write recipient info: //std::map recipient_info; searchidx << "recipient_idx = [" << std::endl; for (auto r = recipient_info.begin(); r != recipient_info.end(); ++r) { std::string line = d_database.getSingleResultAs("SELECT json_object('_id', ?, 'display_name', ?)", {r->first, r->second.display_name}, std::string()); if (line.empty()) [[unlikely]] continue; searchidx << " " << line; if (std::next(r) != recipient_info.end()) [[likely]] searchidx << "," << std::endl; else searchidx << std::endl << "];" << std::endl; } // write page info: searchidx << "page_idx = [" << std::endl; for (auto pi = searchidx_page_idx_map.begin() ; pi != searchidx_page_idx_map.end(); ++pi) { std::string line = d_database.getSingleResultAs("SELECT json_object('_id', ?, 'bn', ?)", {pi->second, pi->first}, std::string()); if (line.empty()) [[unlikely]] continue; searchidx << " " << line; if (std::next(pi) != searchidx_page_idx_map.end()) [[likely]] searchidx << "," << std::endl; else searchidx << std::endl << "];" << std::endl; } } // write chat folders std::vector indexedthreads; std::set_difference(threads.begin(), threads.end(), excludethreads.begin(), excludethreads.end(), std::back_inserter(indexedthreads)); std::vector> chatfolders_list; // {_id, chatfolder name, filename(link)} if (chatfolders && d_database.containsTable("chat_folder")) { // get all folders /* FolderType: ALL(0), // Folder containing all 1:1 chats INDIVIDUAL(1), // Folder containing group chats GROUP(2), // Folder containing unread chats. UNREAD(3), // Folder containing custom chosen chats CUSTOM(4); */ SqliteDB::QueryResults cf_results; if (d_database.exec("SELECT _id, name, show_individual, show_groups FROM chat_folder " "WHERE folder_type IS NOT 0 ORDER BY position ASC", &cf_results)) [[likely]] { for (unsigned int i = 0; i < cf_results.rows(); ++i) { std::string filename = "chatfolder_" + cf_results(i, "_id") + "_" + cf_results(i, "name"); chatfolders_list.emplace_back(cf_results.valueAsInt(i, "_id"), cf_results(i, "name"), filename); } for (unsigned int i = 0; i < cf_results.rows(); ++i) { std::vector chatfolder_threads; // add 1-on-1 if (cf_results.valueAsInt(i, "show_individual")) { SqliteDB::QueryResults individual_threads; if (!d_database.exec("SELECT _id FROM thread WHERE recipient_id IN " "(SELECT _id FROM recipient WHERE type = 0)", &individual_threads)) [[unlikely]] continue; for (unsigned int j = 0; j < individual_threads.rows(); ++j) { long long int individual_thread_id = individual_threads.valueAsInt(j, "_id"); if (bepaald::contains(indexedthreads, individual_thread_id)) chatfolder_threads.push_back(individual_thread_id); } } // add groups if (cf_results.valueAsInt(i, "show_groups")) { SqliteDB::QueryResults group_threads; if (!d_database.exec("SELECT _id FROM thread WHERE recipient_id IN " "(SELECT _id FROM recipient WHERE type = 3)", &group_threads)) [[unlikely]] // consider type = 1,2,3 ? continue; for (unsigned int j = 0; j < group_threads.rows(); ++j) { long long int group_thread_id = group_threads.valueAsInt(j, "_id"); if (bepaald::contains(indexedthreads, group_thread_id)) chatfolder_threads.push_back(group_thread_id); } } /* membership_type // Chat that should be included in the chat folder INCLUDED(0), // Chat that should be excluded from the chat folder EXCLUDED(1) */ SqliteDB::QueryResults membership_results; if (!d_database.exec("SELECT thread_id FROM chat_folder_membership WHERE chat_folder_id = ? AND membership_type = 0", cf_results.value(i, "_id"), &membership_results)) [[unlikely]] continue; // add threads manually included for (unsigned int j = 0; j < membership_results.rows(); ++j) { long long int cf_thread_id = membership_results.valueAsInt(j, "thread_id"); if (bepaald::contains(indexedthreads, cf_thread_id)) chatfolder_threads.push_back(cf_thread_id); } if (!d_database.exec("SELECT thread_id FROM chat_folder_membership WHERE chat_folder_id = ? AND membership_type = 1", cf_results.value(i, "_id"), &membership_results)) [[unlikely]] continue; // remove threads manually excluded #if __cpp_lib_erase_if >= 202002L std::erase_if(chatfolder_threads, [&](long long int tid) { return membership_results.contains(tid); }); #else // I think I support c++17... auto it = std::remove_if(chatfolder_threads.begin(), chatfolder_threads.end(), [&](long long int tid) { return membership_results.contains(tid); }); chatfolder_threads.erase(it, chatfolder_threads.end()); #endif std::string filename = "chatfolder_" + cf_results(i, "_id") + "_" + cf_results(i, "name"); HTMLwriteChatFolder(chatfolder_threads, maxdate, directory, filename, &recipient_info, note_to_self_thread_id, calllog, searchpage, stickerpacks, blocked, fullcontacts, settings, overwrite, append, lighttheme, themeswitching, exportdetails_html, cf_results.valueAsInt(i, "_id"), chatfolders_list, compact); } } } HTMLwriteIndex(indexedthreads, maxdate, directory, &recipient_info, note_to_self_thread_id, calllog, searchpage, stickerpacks, blocked, fullcontacts, settings, overwrite, append, lighttheme, themeswitching, exportdetails_html, chatfolders_list, compact); if (calllog) HTMLwriteCallLog(threads, directory, datewhereclausecalllog, &recipient_info, note_to_self_thread_id, overwrite, append, lighttheme, themeswitching, exportdetails_html, compact); if (searchpage) HTMLwriteSearchpage(directory, lighttheme, themeswitching, compact); if (stickerpacks) HTMLwriteStickerpacks(directory, overwrite, append, lighttheme, themeswitching, exportdetails_html); if (blocked) HTMLwriteBlockedlist(directory, &recipient_info, overwrite, append, lighttheme, themeswitching, exportdetails_html, compact); if (fullcontacts) HTMLwriteFullContacts(directory, &recipient_info, overwrite, append, lighttheme, themeswitching, exportdetails_html, compact); if (settings) HTMLwriteSettings(directory, overwrite, append, lighttheme, themeswitching, exportdetails_html); Logger::message("All done!"); return true; } signalbackup-tools-20250313-1/signalbackup/exporttodir.cc000066400000000000000000000171261476450434500232720ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::exportBackupToDir(std::string const &directory, bool overwrite, bool keepattachmentdatainmemory, bool onlydb) { Logger::message("\nExporting backup into '", directory, "/'"); if (!prepareOutputDirectory(directory, overwrite)) return false; bool exportok = true; // export headerframe: if (!onlydb) { Logger::message("Writing HeaderFrame..."); if (!writeRawFrameDataToFile(directory + "/Header.sbf", d_headerframe)) [[unlikely]] { Logger::error("Failed to write headerframe"); exportok = false; } // export databaseversionframe Logger::message("Writing DatabaseVersionFrame..."); if (!writeRawFrameDataToFile(directory + "/DatabaseVersion.sbf", d_databaseversionframe)) [[unlikely]] { Logger::error("Failed to write databaseversionframe"); exportok = false; } // export attachments Logger::message("Writing Attachments..."); for (auto const &aframe : d_attachments) { AttachmentFrame *a = aframe.second.get(); uint64_t rowid = a->rowId(); int64_t uniqueid = a->attachmentId(); if (uniqueid == 0) uniqueid = -1; std::string attachment_basefilename = directory + "/Attachment_" + bepaald::toString(rowid) + "_" + bepaald::toString(uniqueid); // write frame if (!writeRawFrameDataToFile(attachment_basefilename + ".sbf", a)) [[unlikely]] { Logger::error("Failed to write attachmentframe"); exportok = false; continue; } // write actual attachment: std::ofstream attachmentstream(attachment_basefilename + ".bin", std::ios_base::binary); if (!attachmentstream.is_open()) [[unlikely]] { Logger::error("Failed to open file for writing: ", directory, attachment_basefilename, ".bin"); exportok = false; continue; } else { unsigned char const *data = a->attachmentData(); if (!data) [[unlikely]] { Logger::error("Failed to retrieve attachment data for attachment (rowid: ", rowid, " uniqueid: ", uniqueid, ")"); exportok = false; continue; } if (!attachmentstream.write(reinterpret_cast(data), a->attachmentSize())) [[unlikely]] { Logger::error("Failed write attachmentdata"); exportok = false; continue; } } if (!keepattachmentdatainmemory && a) { MEMINFO("BEFORE DROPPING ATTACHMENT DATA"); a->clearData(); MEMINFO("AFTER DROPPING ATTACHMENT DATA"); } } // export avatars Logger::message("Writing Avatars..."); #if __cplusplus > 201703L for (int count = 1; auto const &aframe : d_avatars) #else int count = 1; for (auto const &aframe : d_avatars) #endif { AvatarFrame *a = aframe.second.get(); std::string avatar_basefilename = directory + "/Avatar_" + std::string(bepaald::numDigits(d_avatars.size()) - bepaald::numDigits(count), '0') + bepaald::toString(count) + "_" + ((d_databaseversion < 33) ? a->name() : a->recipient()); ++count; // write frame if (!writeRawFrameDataToFile(avatar_basefilename + ".sbf", a)) [[unlikely]] { Logger::error("Failed to write avatarframe"); exportok = false; continue; } // write actual avatar: std::ofstream avatarstream(avatar_basefilename + ".bin", std::ios_base::binary); if (!avatarstream.is_open()) [[unlikely]] { Logger::error("Failed to open file for writing: ", directory, avatar_basefilename, ".bin"); exportok = false; continue; } else if (!avatarstream.write(reinterpret_cast(a->attachmentData()), a->attachmentSize())) [[unlikely]] { Logger::error("Failed to write avatar data"); exportok = false; continue; } } // export sharedpreferences Logger::message("Writing SharedPrefFrame(s)..."); #if __cplusplus > 201703L for (int count = 1; auto const &spframe : d_sharedpreferenceframes) #else count = 1; for (auto const &spframe : d_sharedpreferenceframes) #endif if (!writeRawFrameDataToFile(directory + "/SharedPreference_" + bepaald::toString(count++) + ".sbf", spframe)) [[unlikely]] { Logger::error("Failed to write sharedpreferenceframe"); exportok = false; continue; } // export keyvalues Logger::message("Writing KeyValueFrame(s)..."); #if __cplusplus > 201703L for (int count = 1; auto const &kvframe : d_keyvalueframes) #else count = 1; for (auto const &kvframe : d_keyvalueframes) #endif if (!writeRawFrameDataToFile(directory + "/KeyValue_" + bepaald::toString(count++) + ".sbf", kvframe)) [[unlikely]] { Logger::error("Failed to write keyvalueframe"); exportok = false; continue; } // export stickers Logger::message("Writing StickerFrames..."); #if __cplusplus > 201703L for (int count = 1; auto const &sframe : d_stickers) #else count = 1; for (auto const &sframe : d_stickers) #endif { StickerFrame *s = sframe.second.get(); std::string sticker_basefilename = directory + "/Sticker_" + bepaald::toString(count++) + "_" + bepaald::toString(s->rowId()); // write frame if (!writeRawFrameDataToFile(sticker_basefilename + ".sbf", s)) [[unlikely]] { Logger::error("Failed to write stickerframe"); exportok = false; continue; } // write actual sticker data std::ofstream stickerstream(sticker_basefilename + ".bin", std::ios_base::binary); if (!stickerstream.is_open()) [[unlikely]] { Logger::error("Failed to open file for writing: ", directory, sticker_basefilename, ".bin"); exportok = false; continue; } else if (!stickerstream.write(reinterpret_cast(s->attachmentData()), s->attachmentSize())) [[unlikely]] { Logger::error("Failed to write sticker data"); exportok = false; continue; } } // export endframe Logger::message("Writing EndFrame..."); if (!writeRawFrameDataToFile(directory + "/End.sbf", d_endframe)) [[unlikely]] { Logger::error("Failed to write endframe"); exportok = false; } } // export database Logger::message("Writing database..."); if (!d_database.saveToFile(directory + "/database.sqlite")) [[unlikely]] { Logger::error("Failed to write SQLite database"); exportok = false; } #ifdef BUILT_FOR_TESTING // if d_found_sqlite_sequence_in_database // write('BUILT_FOR_TESTING_FOUND_SQLITE_SEQUENCE'); if (d_found_sqlite_sequence_in_backup) { std::ofstream bft(directory + "/BUILT_FOR_TESTING_FOUND_SQLITE_SEQUENCE", std::ios_base::binary); bft.close(); } #endif Logger::message("Done!"); return exportok; } signalbackup-tools-20250313-1/signalbackup/exporttofile.cc000066400000000000000000000243201476450434500234250ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::exportBackupToFile(std::string const &filename, std::string const &passphrase, bool overwrite, bool keepattachmentdatainmemory) { Logger::message("\nExporting backup to '", filename, "'"); std::string newpw = passphrase; if (newpw == std::string()) newpw = d_passphrase; if (newpw == std::string()) { Logger::error("Need password to create encrypted backup file."); return false; } if (!overwrite && bepaald::fileOrDirExists(filename)) { Logger::error("File '", filename, "' exists, use --overwrite to overwrite"); return false; } if (!d_headerframe || !d_fe.init(newpw, d_headerframe->salt(), d_headerframe->salt_length(), d_headerframe->iv(), d_headerframe->iv_length(), d_headerframe->version(), d_verbose)) { Logger::error("Failed to initialize FileEncryptor"); return false; } std::ofstream outputfile(filename, std::ios_base::binary); // HEADER // Note: HeaderFrame is not encrypted. Logger::message("Writing HeaderFrame..."); if (!d_headerframe) { Logger::error("HeaderFrame not found"); return false; } std::pair framedata = d_headerframe->getData(); if (!framedata.first) { Logger::error("Failed to get HeaderFrame data"); return false; } bool writeok = writeFrameDataToFile(outputfile, framedata); delete[] framedata.first; if (!writeok) return false; // VERSION Logger::message("Writing DatabaseVersionFrame..."); if (!d_databaseversionframe) { Logger::error("DataBaseVersionFrame not found"); return false; } if (!writeEncryptedFrame(outputfile, d_databaseversionframe.get())) return false; // SQL DATABASE + ATTACHMENTS Logger::message("Writing SqlStatementFrame(s)..."); // get and write schema SqliteDB::QueryResults results; d_database.exec("SELECT sql, name, type FROM sqlite_master WHERE sql NOT NULL", &results); std::vector tables; for (unsigned int i = 0; i < results.rows(); ++i) { if (results.valueHasType(i, 1) && (results.getValueAs(i, 1) != "sms_fts" && STRING_STARTS_WITH(results.getValueAs(i, 1), "sms_fts"))) continue;//std::cout << "Skipping " << results[i][1].second << " because it is sms_ftssecrettable" << std::endl; if (results.valueHasType(i, 1) && (results.getValueAs(i, 1) != d_mms_table + "_fts" && STRING_STARTS_WITH(results.getValueAs(i, 1), d_mms_table + "_fts"))) continue;//std::cout << "Skipping " << results[i][1].second << " because it is mms_ftssecrettable" << std::endl; if (results.valueHasType(i, 1) && (results.getValueAs(i, 1) != "emoji_search" && STRING_STARTS_WITH(results.getValueAs(i, 1), "emoji_search"))) continue;//std::cout << "Skipping " << results[i][1].second << " because it is emoji_search_ftssecrettable" << std::endl; if (results.valueHasType(i, 1) && STRING_STARTS_WITH(results.getValueAs(i, 1), "sqlite_")) { // this is normally skipped, but for testing purposes we won't skip if it was found in input #ifdef BUILT_FOR_TESTING if (d_found_sqlite_sequence_in_backup) ; else #endif continue; } if (results.valueHasType(i, 2) && results.getValueAs(i, 2) == "table") tables.emplace_back(results.getValueAs(i, 1)); SqlStatementFrame newframe; newframe.setStatementField(results.getValueAs(i, 0)); //std::cout << "Writing SqlStatementFrame..." << std::endl; if (!writeEncryptedFrame(outputfile, &newframe)) return false; } // write contents of tables for (std::string const &table : tables) { if (table == "signed_prekeys" || table == "one_time_prekeys" || table == "sessions" || //table == "job_spec" || // this is in the official export. But it makes testing more difficult. it //table == "constraint_spec" || // should be ok to export these (if present in source), since we are only //table == "dependency_spec" || // dealing with exported backups (not from live installations) -> they should //table == "emoji_search" || // have been excluded + the official import should be able to deal with them //table == "sender_keys" || //table == "sender_key_shared" || //table == "pending_retry_receipts" || //table == "avatar_picker" || //table == "remapped_recipients" || //table == "remapped_threads" || STRING_STARTS_WITH(table, "sms_fts") || STRING_STARTS_WITH(table, d_mms_table + "_fts") || STRING_STARTS_WITH(table, "sqlite_")) continue; d_database.exec("SELECT * FROM " + table, &results); if (!d_showprogress) Logger::message_start(" Dealing with table '", table, "'... "); for (unsigned int i = 0; i < results.rows(); ++i) { if (d_showprogress) Logger::message_overwrite(" Dealing with table '", table, "'... ", i + 1, "/", results.rows(), " entries..."); SqlStatementFrame newframe = buildSqlStatementFrame(table, results.row(i)); //std::cout << "Writing SqlStatementFrame..." << std::endl; if (!writeEncryptedFrame(outputfile, &newframe)) return false; if (table == d_part_table) // find corresponding attachment { bool needuniqqueid = d_database.tableContainsColumn(d_part_table, "unique_id"); long long int rowid = 0, uniqueid = needuniqqueid ? 0 : -1; for (unsigned int j = 0; j < results.columns(); ++j) { if (results.header(j) == "_id" && results.valueHasType(i, j)) { rowid = results.getValueAs(i, j); if (rowid && (uniqueid || !needuniqqueid)) break; } else if (needuniqqueid && results.header(j) == "unique_id" && results.valueHasType(i, j)) { //std::cout << "UNIQUEID: " << std::any_cast(results[i][j].second) << std::endl; uniqueid = results.getValueAs(i, j); if (rowid && (uniqueid || !needuniqqueid)) break; } } auto attachment = d_attachments.find({rowid, uniqueid}); if (attachment != d_attachments.end()) [[likely]] { if (!writeEncryptedFrame(outputfile, attachment->second.get())) return false; if (!keepattachmentdatainmemory) { MEMINFO("BEFORE DROPPING ATTACHMENT DATA"); attachment->second.get()->clearData(); MEMINFO("AFTER DROPPING ATTACHMENT DATA"); } } else [[unlikely]] { if (!missingAttachmentExpected(rowid, uniqueid)) { Logger::warning("Attachment data not found (rowid: ", rowid, ", uniqueid: ", uniqueid, ")"); if (d_showprogress) Logger::message_overwrite(" Dealing with table '", table, "'... ", i + 1, "/", results.rows(), " entries..."); } } } else if (table == "sticker") // find corresponding sticker { uint64_t rowid = 0; for (unsigned int j = 0; j < results.columns(); ++j) if (results.header(j) == "_id" && results.valueHasType(i, j)) { rowid = results.getValueAs(i, j); break; } auto sticker = d_stickers.find(rowid); if (sticker != d_stickers.end()) { if (!writeEncryptedFrame(outputfile, sticker->second.get())) return false; if (!keepattachmentdatainmemory) sticker->second.get()->clearData(); } else { Logger::warning("Sticker data not found (rowid: ", rowid, ")"); if (d_showprogress) Logger::message_overwrite(" Dealing with table '", table, "'... ", i + 1, "/", results.rows(), " entries..."); } } } if (d_showprogress) Logger::message_overwrite(" Dealing with table '", table, "'... ", results.rows(), "/", results.rows(), " entries...done", Logger::Control::ENDOVERWRITE); else Logger::message_end("done"); } Logger::message("Writing SharedPrefFrame(s)..."); // SHAREDPREFS for (unsigned int i = 0; i < d_sharedpreferenceframes.size(); ++i) if (!writeEncryptedFrame(outputfile, d_sharedpreferenceframes[i].get())) return false; Logger::message("Writing KeyValueFrame(s)..."); // KEYVALUES for (unsigned int i = 0; i < d_keyvalueframes.size(); ++i) if (!writeEncryptedFrame(outputfile, d_keyvalueframes[i].get())) return false; // AVATAR Logger::message("Writing Avatars..."); for (auto const &a : d_avatars) { if (d_verbose && !a.second.get()) [[unlikely]] { Logger::error("ASKED TO WRITE NULLPTR-AVATAR. THIS SHOULD BE AN ERROR"); Logger::error_indent("BUT I'M PRETENDING IT DIDN'T HAPPEN TO FIND THE CAUSE OF IT"); Logger::error_indent("THE PROGRAM WILL LIKELY CRASH NOW..."); } if (!writeEncryptedFrame(outputfile, a.second.get())) return false; } // END Logger::message("Writing EndFrame..."); if (!d_endframe) { Logger::error("EndFrame not found."); return false; } if (!writeEncryptedFrame(outputfile, d_endframe.get())) return false; outputfile.flush(); Logger::message("Done! Wrote ", outputfile.tellp(), " bytes."); return true; } signalbackup-tools-20250313-1/signalbackup/exporttxt.cc000066400000000000000000000402561476450434500227700ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "msgrange.h" bool SignalBackup::exportTxt(std::string const &directory, std::vector const &limittothreads, std::vector const &daterangelist, std::string const &selfphone [[maybe_unused]], bool migrate, bool overwrite) { Logger::message("Starting plaintext export to '", directory, "'"); // v170 and above should work. Anything below will first migrate (I believe anything down to ~23 should more or less work) bool databasemigrated = false; MemSqliteDB backup_database; if (d_databaseversion < 170 || migrate) { SqliteDB::copyDb(d_database, backup_database); if (!migrateDatabase(d_databaseversion, 170)) // migrate == TRUE, but migration fails { Logger::error("Failed to migrate currently unsupported database version (", d_databaseversion, ")." " Please upgrade your database"); SqliteDB::copyDb(backup_database, d_database); return false; } databasemigrated = true; } // // >= 168 will work already? (not sure if 168 and 169 were ever in production, I don't have them at least) // if (d_databaseversion == 167) // { // SqliteDB::copyDb(d_database, backup_database); // if (!migrateDatabase(167, 170)) // { // Logger::error("Failed to migrate currently unsupported database version (", d_databaseversion, ")." // " Please upgrade your database"); // SqliteDB::copyDb(backup_database, d_database); // return false; // } // else // databasemigrated = true; // } // else if (d_databaseversion < 167) // { // if (!migrate) // { // Logger::error("Currently unsupported database version (", d_databaseversion, ")."); // Logger::error_indent("Please upgrade your database or append the `--migratedb' option to attempt to"); // Logger::error_indent("migrate this database to a supported version."); // return false; // } // SqliteDB::copyDb(d_database, backup_database); // if (!migrateDatabase(d_databaseversion, 170)) // migrate == TRUE, but migration fails // { // Logger::error("Failed to migrate currently unsupported database version (", d_databaseversion, ")." // " Please upgrade your database"); // SqliteDB::copyDb(backup_database, d_database); // return false; // } // else // databasemigrated = true; // } // check if dir exists, create if not if (!prepareOutputDirectory(directory, overwrite)) { if (databasemigrated) SqliteDB::copyDb(backup_database, d_database); return false; } // check and warn about selfid & note-to-self thread d_selfid = selfphone.empty() ? scanSelf() : d_database.getSingleResultAs("SELECT _id FROM recipient WHERE " + d_recipient_e164 + " = ?", selfphone, -1); if (d_selfid == -1) { if (!selfphone.empty()) Logger::warning("Failed to determine id of 'self'."); else Logger::warning("Failed to determine id of 'self'. Consider passing `--setselfid \"[phone]\"' to set it manually"); } else d_selfuuid = bepaald::toLower(d_database.getSingleResultAs("SELECT " + d_recipient_aci + " FROM recipient WHERE _id = ?", d_selfid, std::string())); std::vector threads = ((limittothreads.empty() || (limittothreads.size() == 1 && limittothreads[0] == -1)) ? threadIds() : limittothreads); std::map recipient_info; // set where-clause for date requested std::vector> dateranges; if (daterangelist.size() % 2 == 0) for (unsigned int i = 0; i < daterangelist.size(); i += 2) dateranges.push_back({daterangelist[i], daterangelist[i + 1]}); std::string datewhereclause; for (unsigned int i = 0; i < dateranges.size(); ++i) { bool needrounding = false; long long int startrange = dateToMSecsSinceEpoch(dateranges[i].first); long long int endrange = dateToMSecsSinceEpoch(dateranges[i].second, &needrounding); if (startrange == -1 || endrange == -1 || endrange < startrange) { Logger::error("Skipping range: '", dateranges[i].first, " - ", dateranges[i].second, "'. Failed to parse or invalid range."); Logger::error_indent(startrange, " ", endrange); continue; } Logger::message(" Using range: ", dateranges[i].first, " - ", dateranges[i].second, " (", startrange, " - ", endrange, ")"); if (needrounding)// if called with "YYYY-MM-DD HH:MM:SS" endrange += 999; // to get everything in the second specified... dateranges[i].first = bepaald::toString(startrange); dateranges[i].second = bepaald::toString(endrange); datewhereclause += (datewhereclause.empty() ? " AND (" : " OR ") + "date_received BETWEEN "s + dateranges[i].first + " AND " + dateranges[i].second; if (i == dateranges.size() - 1) datewhereclause += ')'; } std::sort(dateranges.begin(), dateranges.end()); // handle each thread for (int t : threads) { Logger::message("Dealing with thread ", t); //bool is_note_to_self = false;//(t == note_to_self_thread_id); // get recipient_id for thread; SqliteDB::QueryResults recid; long long int thread_recipient_id = -1; if (!d_database.exec("SELECT _id," + d_thread_recipient_id + " FROM thread WHERE _id = ?", t, &recid) || recid.rows() != 1 || (thread_recipient_id = recid.valueAsInt(0, d_thread_recipient_id)) == -1) { Logger::error("Failed to find recipient_id for thread (", t, ")... skipping"); continue; } long long int thread_id = recid.getValueAs(0, "_id"); bool isgroup = false; SqliteDB::QueryResults groupcheck; d_database.exec("SELECT group_id FROM recipient WHERE _id = ? AND group_id IS NOT NULL", thread_recipient_id, &groupcheck); if (groupcheck.rows()) isgroup = true; // now get all messages SqliteDB::QueryResults messages; if (!d_database.exec("SELECT "s "_id, " + d_mms_recipient_id + ", " + (d_database.tableContainsColumn(d_mms_table, "to_recipient_id") ? "to_recipient_id" : "-1") + " AS to_recipient_id, body, " "date_received, " + d_mms_type + ", " "attcount, reactioncount, mentioncount, " "IFNULL(remote_deleted, 0) AS remote_deleted, " "IFNULL(view_once, 0) AS view_once, " + (d_database.tableContainsColumn(d_mms_table, "message_extras") ? "message_extras, " : "") + "expires_in" " FROM " + d_mms_table + " " // get attachment count for message: "LEFT JOIN (SELECT " + d_part_mid + " AS message_id, COUNT(*) AS attcount FROM " + d_part_table + " GROUP BY message_id) AS attmnts ON " + d_mms_table + "._id = attmnts.message_id " // get reaction count for message: "LEFT JOIN (SELECT message_id, COUNT(*) AS reactioncount FROM reaction GROUP BY message_id) AS rctns ON " + d_mms_table + "._id = rctns.message_id " // get mention count for message: "LEFT JOIN (SELECT message_id, COUNT(*) AS mentioncount FROM mention GROUP BY message_id) AS mntns ON " + d_mms_table + "._id = mntns.message_id " "WHERE thread_id = ?" + datewhereclause + + (d_database.tableContainsColumn(d_mms_table, "latest_revision_id") ? " AND latest_revision_id IS NULL" : "") + " ORDER BY date_received ASC", t, &messages)) { Logger::error("Failed to query database for messages"); if (databasemigrated) SqliteDB::copyDb(backup_database, d_database); return false; } if (messages.rows() == 0) continue; // get all recipients in thread (group member (past and present), quote/reaction authors, mentions) std::set all_recipients_ids = getAllThreadRecipients(t); //try to set any missing info on recipients setRecipientInfo(all_recipients_ids, &recipient_info); // get conversation name, sanitize it and set outputfilename if (recipient_info.find(thread_recipient_id) == recipient_info.end()) { Logger::error("Failed set recipient info for thread (", t, ")... skipping"); continue; } std::string filename = /*(is_note_to_self ? "Note to self (_id"s + bepaald::toString(thread_id) + ")" : */sanitizeFilename(recipient_info[thread_recipient_id].display_name + " (_id" + bepaald::toString(thread_id) + ").txt")/*)*/; if (bepaald::fileOrDirExists(directory + "/" + filename)) { Logger::error("Refusing to overwrite existing file"); if (databasemigrated) SqliteDB::copyDb(backup_database, d_database); return false; } std::ofstream txtoutput(directory + "/" + filename, std::ios_base::binary); if (!txtoutput.is_open()) { Logger::error("Failed to open '", directory, "/", filename, "' for writing."); if (databasemigrated) SqliteDB::copyDb(backup_database, d_database); return false; } for (unsigned int i = 0; i < messages.rows(); ++i) { bool is_deleted = messages.getValueAs(i, "remote_deleted") == 1; bool is_viewonce = messages.getValueAs(i, "view_once") == 1; if (is_deleted || is_viewonce) continue; long long int type = messages.getValueAs(i, d_mms_type); long long int msg_id = messages.getValueAs(i, "_id"); //bool incoming = !Types::isOutgoing(messages.getValueAs(i, d_mms_type)); long long int msg_recipient_id = messages.valueAsInt(i, d_mms_recipient_id); if (isgroup && Types::isOutgoing(type)) msg_recipient_id = d_selfid; if (msg_recipient_id == -1) [[unlikely]] { Logger::warning("Failed to get message recipient id. Skipping."); continue; } std::string body = messages.valueAsString(i, "body"); std::string readable_date = bepaald::toDateString(messages.getValueAs(i, "date_received") / 1000, "%b %d, %Y %H:%M:%S"); SqliteDB::QueryResults attachment_results; if (messages.valueAsInt(i, "attcount", 0) > 0) d_database.exec("SELECT " "_id, " + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id"s : "-1 AS unique_id") + ", " + d_part_ct + ", " "file_name, " + d_part_pending + ", " + (d_database.tableContainsColumn(d_part_table, "caption") ? "caption, "s : std::string()) + "sticker_pack_id " "FROM " + d_part_table + " WHERE " + d_part_mid + " IS ? AND quote IS 0", msg_id, &attachment_results); // check attachments for long message body -> replace cropped body & remove from attachment results setLongMessageBody(&body, &attachment_results); SqliteDB::QueryResults mention_results; if (messages.valueAsInt(i, "mentioncount", 0) > 0) d_database.exec("SELECT recipient_id, range_start, range_length FROM mention WHERE message_id IS ?", msg_id, &mention_results); SqliteDB::QueryResults reaction_results; if (messages.valueAsInt(i, "reactioncount", 0) > 0) d_database.exec("SELECT emoji, author_id, DATETIME(date_sent / 1000, 'unixepoch', 'localtime') AS 'date_sent', " "DATETIME(date_received / 1000, 'unixepoch', 'localtime') AS 'date_received' " "FROM reaction WHERE message_id IS ?", msg_id, &reaction_results); if (Types::isStatusMessage(type) || Types::isCallType(type)) { // see note in exporthtml long long int target_rid = msg_recipient_id; if ((Types::isIdentityVerified(type) || Types::isIdentityDefault(type)) && messages.valueAsInt(i, "to_recipient_id") != -1) [[unlikely]] target_rid = messages.valueAsInt(i, "to_recipient_id"); std::string statusmsg; if (!body.empty() || !(d_database.tableContainsColumn(d_mms_table, "message_extras") && messages.valueHasType, size_t>>(i, "message_extras"))) statusmsg = decodeStatusMessage(body, messages.getValueAs(i, "expires_in"), type, getRecipientInfoFromMap(&recipient_info, target_rid).display_name); else if (d_database.tableContainsColumn(d_mms_table, "message_extras") && messages.valueHasType, size_t>>(i, "message_extras")) statusmsg = decodeStatusMessage(messages.getValueAs, size_t>>(i, "message_extras"), messages.getValueAs(i, "expires_in"), type, getRecipientInfoFromMap(&recipient_info, target_rid).display_name); txtoutput << "[" << readable_date << "] " << "***" << " " << statusmsg << '\n'; } else { // get originating username std::string user = getRecipientInfoFromMap(&recipient_info, msg_recipient_id).display_name; for (unsigned int a = 0; a < attachment_results.rows(); ++a) { std::string content_type = attachment_results.valueAsString(a, d_part_ct); if (content_type == "text/x-signal-plain") [[unlikely]] continue; std::string attachment_filename; if (!attachment_results.isNull(a, "file_name") && !attachment_results(a, "file_name").empty()) attachment_filename = '"' + attachment_results(a, "file_name") + '"'; else if (!content_type.empty()) attachment_filename = "of type " + content_type; txtoutput << "[" << readable_date << "] *** <" << user << "> sent file" << (attachment_filename.empty() ? "" : " " + attachment_filename); if (body.empty()) TXTaddReactions(&reaction_results, &txtoutput); txtoutput << '\n'; } if (!body.empty()) { // prep body for mentions... std::vector ranges; for (unsigned int m = 0; m < mention_results.rows(); ++m) { std::string displayname = getNameFromRecipientId(mention_results.getValueAs(m, "recipient_id")); if (displayname.empty()) continue; ranges.emplace_back(Range{mention_results.getValueAs(m, "range_start"), mention_results.getValueAs(m, "range_length"), "", "@" + displayname, "", false}); } applyRanges(&body, &ranges, nullptr); txtoutput << "[" << readable_date << "] <" << user << "> " << body; TXTaddReactions(&reaction_results, &txtoutput); txtoutput << '\n'; } } } } Logger::message("All done!"); if (databasemigrated) { Logger::message("restoring migrated database..."); SqliteDB::copyDb(backup_database, d_database); } return true; } signalbackup-tools-20250313-1/signalbackup/exportxml.cc000066400000000000000000001315551476450434500227540ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "msgrange.h" void SignalBackup::handleSms(SqliteDB::QueryResults const &results, std::ofstream &outputfile, std::string const &self [[maybe_unused]], int i) const { /* protocol - Protocol used by the message, its mostly 0 in case of SMS messages. */ /* OPTIONAL */ // removed from dbv166 long long int protocol = getIntOr(results, i, "protocol", 0); /* subject - Subject of the message, its always null in case of SMS messages. */ /* OPTIONAL */ std::string subject = getStringOr(results, i, "subject"); /* service_center - The service center for the received message, null in case of sent messages. */ /* OPTIONAL */ std::string service_center = getStringOr(results, i, "service_center"); /* read - Read Message = 1, Unread Message = 0. */ /* REQUIRED */ long long int read = getIntOr(results, i, "read", 0); /* status - None = -1, Complete = 0, Pending = 32, Failed = 64. */ /* REQUIRED */ long long int status = getIntOr(results, i, "status", 0); /* type - 1 = Received, 2 = Sent, 3 = Draft, 4 = Outbox, 5 = Failed, 6 = Queued */ /* REQUIRED */ long long int type = 5; long long int realtype = -1; if (results.valueHasType(i, "type")) { realtype = results.getValueAs(i, "type"); // skip status messages for now... if (Types::isStatusMessage(realtype)) { //std::cout << "Skipping status message " << realtype << std::endl; return; } // skip 'This group was updated to a New Group' if (realtype == Types::GV1_MIGRATION_TYPE) return; switch (realtype & Types::BASE_TYPE_MASK) { case Types::INCOMING_CALL_TYPE: case Types::BASE_INBOX_TYPE: type = 1; break; case Types::OUTGOING_CALL_TYPE: case Types::BASE_SENT_TYPE: type = 2; break; case Types::MISSED_CALL_TYPE: case Types::BASE_DRAFT_TYPE: type = 3; break; case Types::JOINED_TYPE: case Types::BASE_OUTBOX_TYPE: type = 4; break; case Types::UNSUPPORTED_MESSAGE_TYPE: case Types::BASE_SENT_FAILED_TYPE: type = 5; break; case Types::INVALID_MESSAGE_TYPE: case Types::BASE_SENDING_TYPE: case Types::BASE_PENDING_SECURE_SMS_FALLBACK: case Types::BASE_PENDING_INSECURE_SMS_FALLBACK: type = 6; break; } } /* date - The Java date representation (including millisecond) of the time when the message was sent/received. */ /* REQUIRED */ long long int date = 0; if (type > 1) // we assume outgoing { if (results.valueHasType(i, "date_sent")) date = results.getValueAs(i, "date_sent"); } else // incoming message { if (results.valueHasType(i, d_sms_date_received)) date = results.getValueAs(i, d_sms_date_received); } /* readable_date - Optional field that has the date in a human readable format. */ /* OPTIONAL */ std::string readable_date; if (results.valueHasType(i, d_sms_date_received)) { long long int datum = results.getValueAs(i, d_sms_date_received); std::time_t epoch = datum / 1000; std::ostringstream tmp; tmp << std::put_time(std::localtime(&epoch), "%b %d, %Y %H:%M:%S"); readable_date = tmp.str(); } /* address - The phone number of the sender/recipient. */ /* REQUIRED */ std::string address; if (results.valueHasType(i, d_sms_recipient_id) || results.valueHasType(i, d_sms_recipient_id)) { std::string rid = results.valueAsString(i, d_sms_recipient_id); if (d_databaseversion >= 24) { SqliteDB::QueryResults r2; d_database.exec("SELECT " + d_recipient_e164 + " FROM recipient WHERE _id = " + rid, &r2); if (r2.rows() == 1 && r2.valueHasType(0, d_recipient_e164)) address = r2.getValueAs(0, d_recipient_e164); else Logger::error("Failed to retrieve required field 'address' (sms database, type = ", realtype, ")"); } else address = rid; escapeXmlString(&address); } else Logger::error("Type mismatch while retrieving required field 'address'"); /* contact_name - Optional field that has the name of the contact. */ /* OPTIONAL */ std::string contact_name; if (results.valueHasType(i, d_sms_recipient_id) || results.valueHasType(i, d_sms_recipient_id)) { std::string rid = results.valueAsString(i, d_sms_recipient_id); SqliteDB::QueryResults r2; if (d_database.containsTable("recipient")) // d_databaseversion >= 24) { if (!d_database.exec("SELECT COALESCE(" + (d_database.tableContainsColumn("recipient", "nickname_joined_name") ? "NULLIF(recipient.nickname_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_system_joined_name + ", ''), " + (d_database.tableContainsColumn("recipient", "profile_joined_name") ? "NULLIF(recipient.profile_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_profile_given_name + ", ''), NULLIF(groups.title, ''), " + (d_database.containsTable("distribution_list") ? "NULLIF(distribution_list.name, ''), " : "") + "NULLIF(recipient." + d_recipient_e164 + ", ''), NULLIF(recipient." + d_recipient_aci + ", ''), " " recipient._id) AS 'contact_name' FROM recipient " "LEFT JOIN groups ON groups.recipient_id = recipient._id " + (d_database.containsTable("distribution_list") ? "LEFT JOIN distribution_list ON recipient._id = distribution_list.recipient_id " : "") + "WHERE recipient._id = ?", rid, &r2)) Logger::error("Failed to get contact_name"); } else d_database.exec("SELECT COALESCE(recipient_preferences.system_display_name, recipient_preferences.signal_profile_name) AS 'contact_name' FROM recipient_preferences WHERE recipient_ids = ?", rid, &r2); if (r2.rows() == 1 && r2.valueHasType(0, "contact_name")) contact_name = r2.getValueAs(0, "contact_name"); escapeXmlString(&contact_name); } /* body - The content of the message. */ /* REQUIRED */ long long int expiration = getIntOr(results, i, "expires_in", -1); std::string body; if (results.valueHasType(i, "body")) { body = results.getValueAs(i, "body"); body = decodeStatusMessage(body, expiration, realtype, contact_name); escapeXmlString(&body); } outputfile << " " << '\n'; } void SignalBackup::handleMms(SqliteDB::QueryResults const &results, std::ofstream &outputfile, std::string const &self, int i, bool keepattachmentdatainmemory) const { // msg_box - The type of message, 1 = Received, 2 = Sent, 3 = Draft, 4 = Outbox long long int msg_box = 5; long long int realtype = -1; if (results.valueHasType(i, d_mms_type)) { realtype = results.getValueAs(i, d_mms_type); // skip status messages for now... if (Types::isStatusMessage(realtype)) { //std::cout << "Skipping status message " << realtype << std::endl; return; } switch (realtype & Types::BASE_TYPE_MASK) { case 1: case 20: msg_box = 1; break; case 2: case 23: msg_box = 2; break; case 3: case 27: msg_box = 3; break; case 4: case 21: msg_box = 4; break; case 5: case 24: msg_box = 5; // INVALID? break; case 6: case 22: case 25: case 26: msg_box = 7; // INVALID? break; } } // date - The Java date representation (including millisecond) of the time when the message was sent/received. Check out www.epochconverter.com for information on how to do the conversion from other languages to Java. long long int date = getIntOr(results, i, "date_received", 0); long long int date_sent = getIntOr(results, i, d_mms_date_sent, 0) / 1000; // readable_date - Optional field that has the date in a human readable format. std::string readable_date; if (results.valueHasType(i, "date_received")) { long long int datum = results.getValueAs(i, "date_received"); std::time_t epoch = datum / 1000; std::ostringstream tmp; tmp << std::put_time(std::localtime(&epoch), "%b %d, %Y %H:%M:%S"); readable_date = tmp.str(); } // this needs to be redone: // get thread.thread_recipient_id from thread where _id = mms.thread_id // -> then get address/group_id from recipient table /* address - The phone number of the sender/recipient. */ /* for (outgoing) group messages, address is all phone numbers concatenated with ~'s in between */ bool isgroup = false; std::set memberphones; std::string thread_address; std::string address; { SqliteDB::QueryResults r2; if (d_database.exec("SELECT " + d_thread_recipient_id + " FROM thread WHERE _id = ?", results.value(i, "thread_id"), &r2) && r2.rows() == 1) { //r2.prettyPrint(); thread_address = r2.valueAsString(0, d_thread_recipient_id); SqliteDB::QueryResults r3; d_database.exec("SELECT " + d_recipient_e164 + ",group_id FROM recipient WHERE _id = " + thread_address, &r3); //r3.prettyPrint(); if (r3.rows() == 1 && r3.valueHasType(0, "group_id")) { isgroup = true; std::vector members; if (!getGroupMembersOld(&members, r3.getValueAs(0, "group_id"))) { Logger::error("Failed to get group members"); return; } for (auto const &id : members) { if (!d_database.exec("SELECT " + d_recipient_e164 + " FROM recipient WHERE _id = ?", id, &r3) || r3.rows() != 1) { Logger::error("Failed to get phone number for recipient: ", id); r3.prettyPrint(d_truncate); return; } memberphones.insert(r3.valueAsString(0, d_recipient_e164)); } #if __cplusplus > 201703L for (int count = memberphones.size(); auto const &p : memberphones) #else int count = memberphones.size(); for (auto const &p : memberphones) #endif { --count; address += p + (count ? "~" : ""); } //std::cout << "Got address: " << address << std::endl; } else if (r3.rows() == 1 && r3.valueHasType(0, d_recipient_e164)) { address = r3.getValueAs(0, d_recipient_e164); } else { Logger::error("Failed to retrieve required field 'address' (mms database, type = ", realtype, ")"); return; } } else { Logger::error("Failed to set field 'address'"); return; } } escapeXmlString(&address); // contact_name - Optional field that has the name of the contact. std::string contact_name; if (!isgroup) { std::string rid; if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id") || !Types::isOutgoing(realtype)) // this is the way for older dbs, or when message is not outgoing { if (results.valueHasType(i, d_mms_recipient_id) || results.valueHasType(i, d_mms_recipient_id)) rid = results.valueAsString(i, d_mms_recipient_id); } else if (d_database.tableContainsColumn(d_mms_table, "to_recipient_id") && Types::isOutgoing(realtype)) // on newer dbs, when message is outgoing, check the to_recipient if (results.valueHasType(i, "to_recipient_id") || results.valueHasType(i, "to_recipient_id")) rid = results.valueAsString(i, "to_recipient_id"); if (!rid.empty()) { /* SqliteDB::QueryResults r2; if (d_database.containsTable("recipient")) // d_databaseversion >= 24) d_database.exec("SELECT COALESCE(" + (d_database.tableContainsColumn("recipient", "nickname_joined_name") ? "NULLIF(recipient.nickname_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_system_joined_name + ", ''), " + (d_database.tableContainsColumn("recipient", "profile_joined_name") ? "NULLIF(recipient.profile_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_profile_given_name + ", ''), NULLIF(groups.title, ''), " + (d_database.containsTable("distribution_list") ? "NULLIF(distribution_list.name, ''), " : "") + "NULLIF(recipient." + d_recipient_e164 + ", ''), NULLIF(recipient." + d_recipient_aci + ", ''), " " recipient._id) AS 'contact_name' FROM recipient " "LEFT JOIN groups ON groups.recipient_id = recipient._id " + (d_database.containsTable("distribution_list") ? "LEFT JOIN distribution_list ON recipient._id = distribution_list.recipient_id " : "") + "WHERE recipient._id = ?", rid, &r2); else d_database.exec("SELECT COALESCE(recipient_preferences.system_display_name, recipient_preferences.signal_profile_name) AS 'contact_name' FROM recipient_preferences WHERE recipient_ids = ?", rid, &r2); if (r2.rows() == 1 && r2.valueHasType(0, "contact_name")) contact_name = r2.getValueAs(0, "contact_name"); */ contact_name = getNameFromRecipientId(bepaald::toNumber(rid)); } } else { /* SqliteDB::QueryResults r2; if (d_database.exec("SELECT title FROM groups WHERE group_id IS (SELECT group_id FROM recipient WHERE _id = ?)", thread_address, &r2) && r2.rows() == 1) contact_name = r2.valueAsString(0, "title"); */ contact_name = getNameFromRecipientId(bepaald::toNumber(thread_address)); } escapeXmlString(&contact_name); // sub - The subject of the message, if present. std::string sub = getStringOr(results, i, "sub"); // read - Has the message been read long long int read = getIntOr(results, i, "read", 0); // read_status - The read-status of the message. std::string read_status = getStringOr(results, i, "read_status"); // rr - The read-report of the message. long long int rr = getIntOr(results, i, "rr", 0); // ct_t - The Content-Type of the message, usually "application/vnd.wap.multipart.related" std::string ct_t = getStringOr(results, i, "ct_t", "application/vnd.wap.multipart.related"); std::string ct_cls = getStringOr(results, i, "ct_cls"); std::string sub_cs = getStringOr(results, i, "sub_cs"); long long int pri = getIntOr(results, i, "pri", 0); long long int v = getIntOr(results, i, "v", 0); // v = (msg_box == 1 // m_type == 132) ? 16 : (== 2 // == 128) 18 ???? // m_id - The Message-ID of the message std::string m_id = getStringOr(results, i, "m_id"); // m_size - The size of the message. std::string m_size = getStringOr(results, i, "m_size"); // m_type - The type of the message defined by MMS spec. long long int m_type = getIntOr(results, i, "m_type", 0); if (m_type == 0) // let's set this to 128/132 for outgoing/incoming to appease Google Messenger (#216) m_type = Types::isOutgoing(realtype) ? 128 : 132; // m_cls std::string m_cls = getStringOr(results, i, "m_cls"); // if address == __textsecuregroup_ -> "personal" ??? std::string retr_st = getStringOr(results, i, "retr_st"); std::string retr_txt = getStringOr(results, i, "retr_txt"); std::string retr_txt_cs = getStringOr(results, i, "retr_txt_cs"); std::string ct_l = getStringOr(results, i, "ct_l"); std::string d_tm = getStringOr(results, i, "d_tm"); std::string d_rpt = getStringOr(results, i, "d_rpt"); std::string exp = getStringOr(results, i, "exp"); std::string resp_txt = getStringOr(results, i, "resp_txt"); std::string rpt_a = getStringOr(results, i, "rpt_a"); std::string resp_st = getStringOr(results, i, "resp_st"); std::string st = getStringOr(results, i, "st"); std::string tr_id = getStringOr(results, i, "tr_id"); /* REQUIRED ints: seen|locked */ long long int seen = 0; long long int locked = 0; long long int text_only = 0; // attachment data long long int mid = getIntOr(results, i, "_id", -1); SqliteDB::QueryResults part_results; if (mid >= 0) d_database.exec("SELECT _id," + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id"s : "-1 AS unique_id"s) + ", " + (d_database.tableContainsColumn(d_part_table, "seq") ? "seq"s : "0 AS seq"s) + ", " + // seq was removed in dbv215 d_part_ct + ", file_name, " + d_part_cd + ", " + //"chset, " + // chset was removed in dbv215 (always null in earlier dbs) //"fn, " + // idem //"cid, " + // idem //"ctt_s, " + // idem //"ctt_t " // idem d_part_cl + " FROM " + d_part_table + " WHERE " + d_part_table + "." + d_part_mid + " = ?", mid, &part_results); if (part_results.rows() == 0) text_only = 1; outputfile << " " << '\n'; // << "=\"" << << "\" " /* PART */ /* text - The content of the message. */ long long int expiration = getIntOr(results, i, "expires_in", -1); std::string text; if (results.valueHasType(i, "body")) { text = results.getValueAs(i, "body"); SqliteDB::QueryResults mention_results; if (d_database.containsTable("mention")) d_database.exec("SELECT recipient_id, range_start, range_length FROM mention WHERE message_id = ?", mid, &mention_results); std::vector ranges; for (unsigned int m = 0; m < mention_results.rows(); ++m) { std::string displayname = getNameFromRecipientId(mention_results.getValueAs(m, "recipient_id")); if (displayname.empty()) continue; ranges.emplace_back(Range{mention_results.getValueAs(m, "range_start"), mention_results.getValueAs(m, "range_length"), "", "@" + displayname, "", false}); } applyRanges(&text, &ranges, nullptr); if (Types::isStatusMessage(realtype)) { if (!text.empty()) text = decodeStatusMessage(text, expiration, realtype, contact_name); else if (d_database.tableContainsColumn(d_mms_table, "message_extras") && results.valueHasType, size_t>>(i, "message_extras")) text = decodeStatusMessage(results.getValueAs, size_t>>(i, "message_extras"), expiration, realtype, contact_name); } escapeXmlString(&text); } else if (results.isNull(i, "body")) { if (Types::isStatusMessage(realtype) && d_database.tableContainsColumn(d_mms_table, "message_extras") && results.valueHasType, size_t>>(i, "message_extras")) text = decodeStatusMessage(results.getValueAs, size_t>>(i, "message_extras"), expiration, realtype, contact_name); escapeXmlString(&text); } outputfile << " " << '\n'; if (!text.empty()) { outputfile << " " << '\n'; } // attachments... for (unsigned int j = 0; j < part_results.rows(); ++j) { long long int seq = getIntOr(part_results, j, "seq", 0); std::string ct = getStringOr(part_results, j, d_part_ct, "null"); std::string name = getStringOr(part_results, j, "name", "null"); std::string chset = getStringOr(part_results, j, "chset", "null"); std::string cd = getStringOr(part_results, j, d_part_cd, "null"); std::string fn = getStringOr(part_results, j, "fn", "null"); std::string cid = getStringOr(part_results, j, "cid", "null"); std::string cl = getStringOr(part_results, j, d_part_cl, "null"); std::string ctt_s = getStringOr(part_results, j, "ctt_s", "null"); std::string ctt_t = getStringOr(part_results, j, "ctt_t", "null"); // seq - The order of the part. // ct - The content type of the part. // name - The name of the part. // chset - The charset of the part. // cl - The content location of the part. // data - The base64 encoded binary content of the part. // // // // // // // // // // // // long long int rowid = getIntOr(part_results, j, "_id", -1); long long int uniqueid = getIntOr(part_results, j, "unique_id", -1); auto attachment = d_attachments.find({rowid, uniqueid}); if (attachment != d_attachments.end()) { outputfile << " second->attachmentData(), attachment->second->attachmentSize())/*.substr(0, 50)*/ << "\" "; if (!keepattachmentdatainmemory) attachment->second.get()->clearData(); } outputfile << ">" << '\n'; } outputfile << " " << '\n'; // ADDR outputfile << " " << '\n'; // 1-on-1 chat -> get conversation partners phone: if (!isgroup) outputfile << " " << '\n'; // group chat -> for each phone number: above. Mark sender with 137, all others 151 else { for (auto const &mp : memberphones) { // get message originator // incoming message: mms.address // outgoing message: self std::string sender = self; if (msg_box == 1) // incoming message { SqliteDB::QueryResults r2; if (d_database.exec("SELECT " + d_recipient_e164 + " FROM recipient WHERE _id = ?", results.valueAsString(i, d_mms_recipient_id), &r2) && // should be ok to use d_mms_recipient_id, since msg_box = incoming r2.rows() == 1) sender = r2.valueAsString(0, d_recipient_e164); } outputfile << " " << '\n'; } } outputfile << " " << '\n'; outputfile << " " << '\n'; } bool SignalBackup::exportXml(std::string const &filename, bool overwrite, std::string self, bool includemms, bool keepattachmentdatainmemory) { if (d_databaseversion < 24) { Logger::error("Unsupported database version (", d_databaseversion, "). Please upgrade first."); return false; } // get own E164 if (self.empty()) { d_selfid = scanSelf(); if (d_selfid != -1) { SqliteDB::QueryResults r; if (d_database.exec("SELECT " + d_recipient_e164 + " FROM recipient WHERE _id = ?", d_selfid, &r) && r.rows() == 1) self = r.valueAsString(0, d_recipient_e164); } if (self.empty()) { Logger::error("Failed to determine own phone number. Please add it on the command line with `--setselfid`."); return false; } } if (d_selfid != -1) d_selfuuid = bepaald::toLower(d_database.getSingleResultAs("SELECT " + d_recipient_aci + " FROM recipient WHERE _id = ?", d_selfid, std::string())); Logger::message("\nExporting backup to '", filename, "'"); if (!overwrite && (bepaald::fileOrDirExists(filename) && !bepaald::isDir(filename))) { Logger::error("File ", filename, " exists, use --overwrite to overwrite"); return false; } // output header std::ofstream outputfile(filename, std::ios_base::binary); outputfile << "" << '\n'; outputfile << "" << '\n'; SqliteDB::QueryResults sms_results; if (d_database.containsTable("sms")) { if (d_database.tableContainsColumn("sms", "protocol") && d_database.tableContainsColumn("sms", "service_center") && d_database.tableContainsColumn("sms", "subject")) // removed in dbv166 d_database.exec("SELECT _id,thread_id,protocol,subject,service_center,read,status,date_sent," + d_sms_date_received + "," + d_sms_recipient_id + ",type,body,expires_in FROM sms " "WHERE " + d_sms_recipient_id + " IN (SELECT _id FROM recipient WHERE " + d_recipient_e164 + " IS NOT NULL OR group_id IS NOT NULL) " "AND " "(type & ?) == 0 AND ((type & 0x1F) == ? OR (type & 0x1F) == ? OR (type & 0x1F) == ? OR (type & 0x1F) == ? OR (type & 0x1F) == ? OR (type & 0x1F) == ? OR (type & 0x1F) == ? OR (type & 0x1F) == ?)", {Types::GROUP_UPDATE_BIT, Types::BASE_INBOX_TYPE, Types::BASE_OUTBOX_TYPE, Types::BASE_SENDING_TYPE, Types::BASE_SENT_TYPE, Types::BASE_SENT_FAILED_TYPE, Types::BASE_PENDING_SECURE_SMS_FALLBACK, Types::BASE_PENDING_INSECURE_SMS_FALLBACK, Types::BASE_DRAFT_TYPE}, &sms_results); else d_database.exec("SELECT _id,thread_id,read,status,date_sent," + d_sms_date_received + "," + d_sms_recipient_id + ",type,body,expires_in FROM sms " "WHERE " + d_sms_recipient_id + " IN (SELECT _id FROM recipient WHERE " + d_recipient_e164 + " IS NOT NULL OR group_id IS NOT NULL) " "AND " "(type & ?) == 0 AND ((type & 0x1F) == ? OR (type & 0x1F) == ? OR (type & 0x1F) == ? OR (type & 0x1F) == ? OR (type & 0x1F) == ? OR (type & 0x1F) == ? OR (type & 0x1F) == ? OR (type & 0x1F) == ?)", {Types::GROUP_UPDATE_BIT, Types::BASE_INBOX_TYPE, Types::BASE_OUTBOX_TYPE, Types::BASE_SENDING_TYPE, Types::BASE_SENT_TYPE, Types::BASE_SENT_FAILED_TYPE, Types::BASE_PENDING_SECURE_SMS_FALLBACK, Types::BASE_PENDING_INSECURE_SMS_FALLBACK, Types::BASE_DRAFT_TYPE}, &sms_results); } SqliteDB::QueryResults mms_results; if (includemms) { // at dbv 109 many columns were removed from the mms table. if (d_databaseversion >= 109) d_database.exec("SELECT _id,thread_id,date_received," + d_mms_date_sent + "," + d_mms_recipient_id + (d_database.tableContainsColumn(d_mms_table, "to_recipient_id") ? ",to_recipient_id" : "") + "," + d_mms_type + "," "(" + d_mms_type + " & " + bepaald::toString(Types::BASE_TYPE_MASK) + ") AS base_type,body,expires_in," + (d_database.tableContainsColumn(d_mms_table, "message_extras") ? "message_extras, " : "") + "read, ct_l, m_type, m_size, exp, tr_id, st FROM " + d_mms_table + " WHERE " + d_mms_recipient_id + " IN (SELECT _id FROM recipient WHERE " + d_recipient_e164 + " IS NOT NULL OR group_id IS NOT NULL) " "AND " + (d_database.tableContainsColumn(d_mms_table, "to_recipient_id") ? "to_recipient_id IN (SELECT _id FROM recipient WHERE " + d_recipient_e164 + " IS NOT NULL OR group_id IS NOT NULL) AND " : "") + "(" + d_mms_type + " & ?) == 0 AND (" + d_mms_type + " & ?) == 0 AND (" + d_mms_type + " & ?) == 0 AND (" + d_mms_type + " & ?) == 0 AND (" + d_mms_type + " & ?) == 0 AND " "(base_type IN (?, ?, ?, ?, ?, ?, ?, ?)) AND " "((" + d_mms_type + " & ?) NOT IN (?, ?, ?)) AND " "((" + d_mms_type + " & ?) NOT IN (?)) AND " //"((" + d_mms_type + " & ?) != 0) AND " "(base_type NOT IN (?, ?)) AND " "(" + d_mms_type + " NOT IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?))", {Types::GROUP_UPDATE_BIT, Types::GROUP_V2_BIT, Types::GROUP_QUIT_BIT, Types::END_SESSION_BIT, Types::EXPIRATION_TIMER_UPDATE_BIT, Types::BASE_INBOX_TYPE, Types::BASE_OUTBOX_TYPE, Types::BASE_SENDING_TYPE, Types::BASE_SENT_TYPE, Types::BASE_SENT_FAILED_TYPE, Types::BASE_PENDING_SECURE_SMS_FALLBACK, Types::BASE_PENDING_INSECURE_SMS_FALLBACK, Types::BASE_DRAFT_TYPE, Types::KEY_EXCHANGE_MASK, Types::KEY_EXCHANGE_IDENTITY_UPDATE_BIT, Types::KEY_EXCHANGE_IDENTITY_VERIFIED_BIT, Types::KEY_EXCHANGE_IDENTITY_DEFAULT_BIT, Types::SPECIAL_TYPES_MASK, Types::SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED, Types::PROFILE_CHANGE_TYPE, Types::JOINED_TYPE, Types::GROUP_CALL_TYPE, Types::INCOMING_CALL_TYPE, Types::OUTGOING_CALL_TYPE, Types::MISSED_CALL_TYPE, Types::INCOMING_VIDEO_CALL_TYPE, Types::OUTGOING_VIDEO_CALL_TYPE, Types::MISSED_VIDEO_CALL_TYPE, Types::GV1_MIGRATION_TYPE, Types::CHANGE_NUMBER_TYPE, Types::BOOST_REQUEST_TYPE}, &mms_results); else d_database.exec("SELECT _id,thread_id,date_received," + d_mms_date_sent + "," + d_mms_recipient_id + "," + d_mms_type + "," "(" + d_mms_type + " & " + bepaald::toString(Types::BASE_TYPE_MASK) + ") AS base_type,body,expires_in,read,m_id,sub,ct_t,ct_l,m_type,m_size,rr,read_status," "m_cls, sub_cs, ct_cls, v, pri, retr_st, retr_txt, retr_txt_cs, d_tm, d_rpt, exp, resp_txt, tr_id, st, resp_st, rpt_a FROM " + d_mms_table + " WHERE " + d_mms_recipient_id + " IN (SELECT _id FROM recipient WHERE " + d_recipient_e164 + " IS NOT NULL OR group_id IS NOT NULL) " "AND " "(" + d_mms_type + " & ?) == 0 AND (" + d_mms_type + " & ?) == 0 AND (" + d_mms_type + " & ?) == 0 AND (" + d_mms_type + " & ?) == 0 AND (" + d_mms_type + " & ?) == 0 AND " "(base_type IN (?, ?, ?, ?, ?, ?, ?, ?)) AND " "((" + d_mms_type + " & ?) NOT IN (?, ?, ?)) AND " "((" + d_mms_type + " & ?) NOT IN (?)) AND " //"((" + d_mms_type + " & ?) != 0) AND " "(base_type NOT IN (?, ?)) AND " "(" + d_mms_type + " NOT IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?))", {Types::GROUP_UPDATE_BIT, Types::GROUP_V2_BIT, Types::GROUP_QUIT_BIT, Types::END_SESSION_BIT, Types::EXPIRATION_TIMER_UPDATE_BIT, Types::BASE_INBOX_TYPE, Types::BASE_OUTBOX_TYPE, Types::BASE_SENDING_TYPE, Types::BASE_SENT_TYPE, Types::BASE_SENT_FAILED_TYPE, Types::BASE_PENDING_SECURE_SMS_FALLBACK, Types::BASE_PENDING_INSECURE_SMS_FALLBACK, Types::BASE_DRAFT_TYPE, Types::KEY_EXCHANGE_MASK, Types::KEY_EXCHANGE_IDENTITY_UPDATE_BIT, Types::KEY_EXCHANGE_IDENTITY_VERIFIED_BIT, Types::KEY_EXCHANGE_IDENTITY_DEFAULT_BIT, Types::SPECIAL_TYPES_MASK, Types::SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED, Types::PROFILE_CHANGE_TYPE, Types::JOINED_TYPE, Types::GROUP_CALL_TYPE, Types::INCOMING_CALL_TYPE, Types::OUTGOING_CALL_TYPE, Types::MISSED_CALL_TYPE, Types::INCOMING_VIDEO_CALL_TYPE, Types::OUTGOING_VIDEO_CALL_TYPE, Types::MISSED_VIDEO_CALL_TYPE, Types::GV1_MIGRATION_TYPE, Types::CHANGE_NUMBER_TYPE, Types::BOOST_REQUEST_TYPE}, &mms_results); } //std::string date; outputfile << "" << '\n'; << "\">\n"; unsigned int sms_row = 0; unsigned int mms_row = 0; while (sms_row < sms_results.rows() || mms_row < mms_results.rows()) { if (mms_row >= mms_results.rows() || (sms_row < sms_results.rows() && (sms_results.getValueAs(sms_row, d_sms_date_received) < mms_results.getValueAs(mms_row, "date_received")))) handleSms(sms_results, outputfile, self, sms_row++); else if (mms_row < mms_results.rows()) handleMms(mms_results, outputfile, self, mms_row++, keepattachmentdatainmemory); //std::cout << "Handled row! Indices now: " << sms_row << "/" << sms_results.rows() << " " << mms_row << "/" << mms_results.rows() << std::endl; } outputfile << "" << '\n'; return true; } // protocol - Protocol used by the message, its mostly 0 in case of SMS messages. // address - The phone number of the sender/recipient. // date - The Java date representation (including millisecond) of the time when the message was sent/received. Check out www.epochconverter.com for information on how to do the conversion from other languages to Java. // type - 1 = Received, 2 = Sent, 3 = Draft, 4 = Outbox, 5 = Failed, 6 = Queued // subject - Subject of the message, its always null in case of SMS messages. // body - The content of the message. // toa - n/a, defaults to null. // sc_toa - n/a, defaults to null. // service_center - The service center for the received message, null in case of sent messages. // read - Read Message = 1, Unread Message = 0. // status - None = -1, Complete = 0, Pending = 32, Failed = 64. // readable_date - Optional field that has the date in a human readable format. // contact_name - Optional field that has the name of the contact. // mms //* date - The Java date representation (including millisecond) of the time when the message was sent/received. Check out www.epochconverter.com for information on how to do the conversion from other languages to Java. //* ct_t - The Content-Type of the message, usually "application/vnd.wap.multipart.related" //* msg_box - The type of message, 1 = Received, 2 = Sent, 3 = Draft, 4 = Outbox //* rr - The read-report of the message. //* sub - The subject of the message, if present. //* read_status - The read-status of the message. //* address - The phone number of the sender/recipient. //* m_id - The Message-ID of the message //* read - Has the message been read //* m_size - The size of the message. //* m_type - The type of the message defined by MMS spec. //* readable_date - Optional field that has the date in a human readable format. //* contact_name - Optional field that has the name of the contact. // part // seq - The order of the part. // ct - The content type of the part. // name - The name of the part. // chset - The charset of the part. // cl - The content location of the part. // text - The text content of the part. // data - The base64 encoded binary content of the part. // addr // address - The phone number of the sender/recipient. // type - The type of address, 129 = BCC, 130 = CC, 151 = To, 137 = From // charset - Character set of this entry // Call Logs // number - The phone number of the call. // duration - The duration of the call in seconds. // date - The Java date representation (including millisecond) of the time when the message was sent/received. Check out www.epochconverter.com for information on how to do the conversion from other languages to Java. // type - 1 = Incoming, 2 = Outgoing, 3 = Missed, 4 = Voicemail, 5 = Rejected, 6 = Refused List. // presentation - caller id presentation info. 1 = Allowed, 2 = Restricted, 3 = Unknown, 4 = Payphone. // readable_date - Optional field that has the date in a human readable format. // contact_name - Optional field that has the name of the contact. // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // signalbackup-tools-20250313-1/signalbackup/fillthreadtablefrommessages.cc000066400000000000000000000277261476450434500264600ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ // #include "signalbackup.ih" // void SignalBackup::fillThreadTableFromMessages() // { // SqliteDB::QueryResults results; // std::cout << std::endl; // std::cout << " *** DEPRECATED ***" << std::endl; // std::cout << "THIS FUNCTION IS VERY OLD AND MAY BE BROKEN" << std::endl; // std::cout << "PLEASE OPEN AN ISSUE IF YOU NEED THIS" << std::endl; // std::cout << std::endl; // //d_database.exec("SELECT * FROM thread", &results); // //std::cout << "THREAD:" << std::endl; // //results.prettyPrint(); // /* // NOTE, 'address' was renamed 'recipient_id' in sms and mms tables // for mms database: // - One-on-one: incoming and outgoing have address == '+31612345678' number of conversation partner // - Groups: outgoing have address == '__textsecure_group__!xxxxxxxxxxxxxxxxxxxx' group_id // incoming have address == '+31612345678' number of sender of that specific message // for sms database: // - One-one-one: incoming and outgoing have address == '+31612345678' number of conversation partner // - Groups: outgoing NEVER IN sms? // incoming have address == '+31612345678' number of sender of that specific message // */ // std::cout << "Creating threads from 'mms' table data" << std::endl; // //std::cout << "Threadids in mms, not in thread" << std::endl; // d_database.exec("SELECT DISTINCT thread_id," + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE (" + d_mms_type + " & " + bepaald::toString(Types::BASE_TYPE_MASK) + // ") BETWEEN " + bepaald::toString(Types::BASE_OUTBOX_TYPE) + " AND " + // bepaald::toString(Types::BASE_PENDING_INSECURE_SMS_FALLBACK) + // " AND thread_id NOT IN (SELECT DISTINCT _id FROM thread)", &results); // //results.prettyPrint(); // for (unsigned int i = 0; i < results.rows(); ++i) // if (results.valueHasType(i, 0) && // (results.valueHasType(i, 1) || results.valueHasType(i, 1))) // d_database.exec("INSERT INTO thread (_id, " + d_thread_recipient_id + ") VALUES (?, ?)", {results.value(i, 0), results.value(i, 1)}); // //std::cout << "Threadids in mms, not in thread" << std::endl; // //d_database.exec("SELECT DISTINCT thread_id,address FROM mms WHERE (msg_box&0x1f) BETWEEN 21 AND 26 AND thread_id NOT IN (SELECT DISTINCT _id FROM thread)", &results); // //results.prettyPrint(); // if (d_database.containsTable("sms")) // { // std::cout << "Creating threads from 'sms' table data" << std::endl; // //std::cout << "Threadids in sms, not in thread" << std::endl; // d_database.exec("SELECT DISTINCT thread_id," + d_sms_recipient_id + " FROM sms WHERE (type & " + bepaald::toString(Types::BASE_TYPE_MASK) + // ") BETWEEN " + bepaald::toString(Types::BASE_OUTBOX_TYPE) + " AND " + // bepaald::toString(Types::BASE_PENDING_INSECURE_SMS_FALLBACK) + // " AND thread_id NOT IN (SELECT DISTINCT _id FROM thread)", &results); // //results.prettyPrint(); // for (unsigned int i = 0; i < results.rows(); ++i) // if (results.valueHasType(i, 0) && // (results.valueHasType(i, 1) || results.valueHasType(i, 1))) // d_database.exec("INSERT INTO thread (_id, " + d_thread_recipient_id + ") VALUES (?, ?)", {results.value(i, 0), results.value(i, 1)}); // // std::cout << "Threadids in sms, not in thread" << std::endl; // // d_database.exec("SELECT DISTINCT thread_id,address FROM sms WHERE (type&0x1f) BETWEEN 21 AND 26 AND thread_id NOT IN (SELECT DISTINCT _id FROM thread)", &results); // // results.prettyPrint(); // } // // deal with threads without outgoing messages // // get all thread_ids not yet in thread table, if there is only one recipient in that thread AND // // there is no other thread with that recipient, add it (though it COULD be a 2 person group with only incoming messages) // d_database.exec((d_database.containsTable("sms") ? "SELECT sms.thread_id AS union_thread_id, sms." + d_sms_recipient_id + " FROM 'sms' WHERE sms.thread_id NOT IN (SELECT DISTINCT _id FROM thread) UNION " : "") + // "SELECT " + d_mms_table + ".thread_id AS union_thread_id, " + d_mms_table + "." + d_mms_recipient_id + " FROM '" + d_mms_table + "' WHERE " + d_mms_table + ".thread_id NOT IN (SELECT DISTINCT _id FROM thread)", &results); // //std::cout << "Orphan threads in db: " << std::endl; // //results.prettyPrint(); // for (unsigned int i = 0; i < results.rows(); ++i) // { // long long int thread = std::any_cast(results.value(i, "union_thread_id")); // //std::cout << "Dealing with thread: " << thread << std::endl; // SqliteDB::QueryResults results2; // if (d_database.containsTable("sms")) // d_database.exec("SELECT DISTINCT sms." + d_sms_recipient_id + " AS union_address FROM 'sms' WHERE sms.thread_id == ? UNION " // "SELECT DISTINCT " + d_mms_table + "." + d_mms_recipient_id + " AS union_address FROM '" + d_mms_table + "' WHERE " + d_mms_table + ".thread_id == ?", {thread, thread}, &results2); // else // d_database.exec("SELECT DISTINCT " + d_mms_table + "." + d_mms_recipient_id + " AS union_address FROM '" + d_mms_table + "' WHERE " + d_mms_table + ".thread_id == ?", thread, &results2); // if (results2.rows() == 1) // { // SqliteDB::QueryResults results3; // d_database.exec("SELECT DISTINCT " + d_thread_recipient_id + " FROM thread WHERE " + d_thread_recipient_id + " = ?", results2.value(0, "union_address"), &results3); // if (results3.rows() == 0) // { // //std::cout << "Creating thread for address " << std::any_cast(results2.value(0, "union_address")) << "(id: " << thread << ")" << std::endl; // d_database.exec("INSERT INTO thread (_id, " + d_thread_recipient_id + ") VALUES (?, ?)", {thread, results2.value(0, "union_address")}); // } // else // std::cout << "Thread for this conversation partner already exists. This may be a group with only two members and " // << "only incoming messages. This case is not supported." << std::endl; // } // else // std::cout << "Too many addresses in orphaned thread, it appears to be group conversation without outgoing messages. This case is not supported." << std::endl; // } // d_database.exec((d_database.containsTable("sms") ? "SELECT sms.thread_id AS union_thread_id, sms." + d_sms_recipient_id + " FROM 'sms' WHERE sms.thread_id NOT IN (SELECT DISTINCT _id FROM thread) UNION " : "") + // "SELECT " + d_mms_table + ".thread_id AS union_thread_id, " + d_mms_table + "." + d_mms_recipient_id + " FROM '" + d_mms_table + "' WHERE " + d_mms_table + ".thread_id NOT IN (SELECT DISTINCT _id FROM thread)", &results); // if (results.rows() > 0) // { // std::cout << " !!! WARNING !!! Unable to generate thread data for messages belonging to this thread (no outgoing messages in conversation)" << std::endl; // results.prettyPrint(); // } // updateThreadsEntries(); // // d_database.exec("SELECT _id, date, message_count, " + d_thread_recipient_id + " , snippet, snippet_cs, type, snippet_type, snippet_uri FROM thread", &results); // // std::cout << "THREAD:" << std::endl; // // results.prettyPrint(); // // now for each group, try to determine members: // SqliteDB::QueryResults threadquery; // std::string query = "SELECT DISTINCT _id, " + d_thread_recipient_id + " FROM thread WHERE SUBSTR(" + d_thread_recipient_id + ", 0, 22) == \"__textsecure_group__!\""; // maybe || SUBSTR == "__signal_mms_group__!" // d_database.exec(query, &threadquery); // for (unsigned int i = 0; i < threadquery.rows(); ++i) // { // std::set groupmembers; // if (threadquery.valueHasType(i, 0)) // { // std::string threadid = bepaald::toString(threadquery.getValueAs(i, 0)); // d_database.exec(d_database.containsTable("sms") ? // "SELECT sms.date_sent AS union_date, sms.type AS union_type, sms.body AS union_body, sms." + d_sms_recipient_id + " AS union_address, sms._id AS [sms._id], '' AS [mms._id] " // "FROM 'sms' WHERE sms.thread_id = " + threadid + // " AND (sms.type & " + bepaald::toString(Types::GROUP_UPDATE_BIT) + " IS NOT 0" // " OR sms.type & " + bepaald::toString(Types::GROUP_QUIT_BIT) + " IS NOT 0) UNION " : "" // "SELECT " + d_mms_table + "." + d_mms_date_sent + " AS union_display_date, " + d_mms_table + "." + d_mms_type + " AS union_type, " + d_mms_table + ".body AS union_body, " + // d_mms_table + "." + d_mms_recipient_id + " AS union_address, '' AS [sms._id], " + d_mms_table + "._id AS [mms._id] " // "FROM " + d_mms_table + " WHERE " + d_mms_table + ".thread_id = " + threadid + // " AND (" + d_mms_table + "." + d_mms_type + " & " + bepaald::toString(Types::GROUP_UPDATE_BIT) + " IS NOT 0" // " OR " + d_mms_table + "." + d_mms_type + " & " + bepaald::toString(Types::GROUP_QUIT_BIT) + " IS NOT 0) ORDER BY union_date", &results); // //std::cout << "STATUS MSGS FROM THREAD: " << threadid << std::endl; // //results.prettyPrint(); // for (unsigned int j = 0; j < results.rows(); ++j) // { // std::string body = std::any_cast(results.value(j, "union_body")); // long long int type = std::any_cast(results.value(j, "union_type")); // std::string address = std::any_cast(results.value(j, "union_address")); // if (Types::isGroupUpdate(type) && !Types::isGroupV2(type)) // { // //std::cout << j << " GROUP UPDATE" << std::endl; // GroupContext statusmsg(body); // auto field4 = statusmsg.getField<4>(); // for (unsigned int k = 0; k < field4.size(); ++k) // { // //std::cout << j << " JOINED: " << field4[k] << std::endl; // groupmembers.insert(field4[k]); // } // } // else if (Types::isGroupQuit(type)) // { // // std::cout << j << " GROUP QUIT!" << std::endl; // // std::cout << j << " LEFT: " << address << std::endl; // groupmembers.erase(address); // } // } // } // std::string groupid = std::any_cast(threadquery.value(i, d_thread_recipient_id)); // std::string members; // //std::cout << "GROUP MEMBERS " << groupid << " : " << std::endl; // for (auto it = groupmembers.begin(); it != groupmembers.end(); ++it) // members += ((it != groupmembers.begin()) ? "," : "") + *it; // //std::cout << members << std::endl; // std::cout << "Creating groups information" << std::endl; // if (d_database.tableContainsColumn("groups", "members")) // d_database.exec("INSERT INTO groups (group_id, members) VALUES (?, ?)", {groupid, members}); // else // { // d_database.exec("INSERT INTO groups (group_id) VALUES (?)", groupid); // for (auto it = groupmembers.begin(); it != groupmembers.end(); ++it) // d_database.exec("INSERT INTO group_membership (group_id, recipient_id)", {groupid, it}); // } // } // } signalbackup-tools-20250313-1/signalbackup/findrecipient.cc000066400000000000000000000110401476450434500235170ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::findRecipient(long long int id) const { for (auto const &dbl : s_databaselinks) { if (dbl.table != "recipient") continue; if (!d_database.containsTable(dbl.table)) [[unlikely]] continue; for (auto const &c : dbl.connections) { if (d_database.containsTable(c.table) && d_database.tableContainsColumn(c.table, c.column)) { SqliteDB::QueryResults res; if (!d_database.exec("SELECT COUNT(*) AS 'count' FROM " + c.table + " WHERE " + c.column + " IS ?", id, &res) || res.rows() != 1) return false; long long int count = res.getValueAs(0, "count"); if (count) Logger::message("Found recipient ", id, " referenced in '", c.table, ".", c.column, "' (", count, " times)"); } } } // get phone and uuid std::string uuid; std::string phone; SqliteDB::QueryResults res; if (!d_database.exec("SELECT " + d_recipient_e164 + ", " + d_recipient_aci + " FROM recipient WHERE _id = ?", id, &res) || res.rows() != 1) return false; uuid = res(d_recipient_aci); phone = res(d_recipient_e164); // check in identities if (d_database.containsTable("identities") && d_database.exec("SELECT COUNT(*) AS 'count' FROM identities WHERE address = ? OR address = ?", {uuid, phone}, &res) && res.rows() == 1) Logger::message("Found recipient ", id, " referenced in 'identities.address' (by uuid/phone, ", res.getValueAs(0, "count"), " times)"); // check in quote mentions if (!uuid.empty()) { int count = 0; if (d_database.exec("SELECT DISTINCT quote_mentions FROM " + d_mms_table + " WHERE quote_mentions IS NOT NULL", &res)) { for (unsigned int i = 0; i < res.rows(); ++i) { auto brdata = res.getValueAs, size_t>>(i, "quote_mentions"); BodyRanges brsproto(brdata); auto brs = brsproto.getField<1>(); for (auto const &br : brs) { std::string mentionuuid = br.getField<3>().value_or(std::string()); if (mentionuuid == uuid) ++count; } } } if (count) Logger::message("Found recipient ", id, " referenced in '", d_mms_table, ".quote_mentions' (by uuid, ", count, " times)"); } // check in group updates std::vector mentioned_in_group_updates(getGroupUpdateRecipients()); if (bepaald::contains(mentioned_in_group_updates, id)) Logger::message("Found recipient ", id, " referenced in group updates"); // check former gv1 members std::set gv1migrationrec; getGroupV1MigrationRecipients(&gv1migrationrec); if (bepaald::contains(gv1migrationrec, id)) Logger::message("Found recipient ", id, " referenced in GV1 migration message"); // check old style? group members SqliteDB::QueryResults results; std::set oldstylegroupmembers; for (auto const &members : {"members"s, d_groups_v1_members}) { if (!d_database.tableContainsColumn("groups", members)) continue; d_database.exec("SELECT "s + members + " FROM groups WHERE " + members + " IS NOT NULL", &results); for (unsigned int i = 0; i < results.rows(); ++i) { std::string membersstr = results.getValueAs(i, members); std::stringstream ss(membersstr); while (ss.good()) { std::string substr; std::getline(ss, substr, ','); //Logger::message("ADDING ", members, " MEMBER: ", substr); oldstylegroupmembers.insert(bepaald::toNumber(substr)); } } } if (bepaald::contains(oldstylegroupmembers, id)) Logger::message("Found recipient ", id, " referenced as group member (old style)"); return true; } signalbackup-tools-20250313-1/signalbackup/getallthreadrecipients.cc000066400000000000000000000073361476450434500254370ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" std::set SignalBackup::getAllThreadRecipients(long long int t) const { std::set recipientlist; SqliteDB::QueryResults results; if (!d_database.exec("SELECT DISTINCT " + d_thread_recipient_id + " FROM thread WHERE _id = ?1 " "UNION " "SELECT DISTINCT " + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE thread_id = ?1 " + (d_database.tableContainsColumn(d_mms_table, "to_recipient_id") ? ("UNION " "SELECT DISTINCT "s + "to_recipient_id" + " FROM " + d_mms_table + " WHERE thread_id = ?1 ") : "" ) + "UNION " "SELECT DISTINCT quote_author FROM " + d_mms_table + " WHERE thread_id = ?1 AND quote_id IS NOT 0 " "UNION " "SELECT DISTINCT author_id FROM reaction WHERE message_id IN (SELECT _id FROM " + d_mms_table + " WHERE thread_id = ?1) " "UNION " "SELECT DISTINCT recipient_id FROM mention WHERE thread_id = ?1 ", t, &results)) return recipientlist; // put results in vector... for (unsigned int i = 0; i < results.rows(); ++i) if (!results.isNull(i, 0)) { if (results.valueHasType(i, 0)) recipientlist.insert(results.getValueAs(i, 0)); else //if (results.valueHasType(i, 0)) recipientlist.insert(bepaald::toNumber(results.valueAsString(i, 0))); } // check if thread is group std::string group_id; d_database.exec("SELECT group_id from recipient WHERE " "_id IS (SELECT " + d_thread_recipient_id + " FROM thread WHERE _id = ?) AND group_id IS NOT NULL", t, &results); if (results.rows() == 1) group_id = results.valueAsString(0, "group_id"); if (!group_id.empty()) { // get current group members and former v1 members std::vector groupmembers; getGroupMembersOld(&groupmembers, group_id, "members"); // for (long long int id : groupmembers) // std::cout << "INSERTING (0): " << id << std::endl; if (d_database.tableContainsColumn("groups", d_groups_v1_members)) getGroupMembersOld(&groupmembers, group_id, d_groups_v1_members); for (long long int id : groupmembers) { //std::cout << "INSERTING (1): " << id << std::endl; recipientlist.insert(id); } // get other possible former group members by parsing group updates std::vector group_update_recipients = getGroupUpdateRecipients(t); // append to recipientlist for (long long int id : group_update_recipients) { //std::cout << "INSERTING (2): " << id << std::endl; recipientlist.insert(id); } // get GV1 migration recipients... getGroupV1MigrationRecipients(&recipientlist, t); } return recipientlist; } signalbackup-tools-20250313-1/signalbackup/getavatarextension.cc000066400000000000000000000030551476450434500246160ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../scopeguard/scopeguard.h" std::string SignalBackup::getAvatarExtension(long long int recipient_id) const { std::string extension; auto it = std::find_if(d_avatars.begin(), d_avatars.end(), [recipient_id](auto const &p) { return p.first == bepaald::toString(recipient_id); }); if (it == d_avatars.end()) return extension; std::optional mimetype = it->second->mimetype(); if (!mimetype) // ensure that mimetype is set { it->second->attachmentData(); // counting on the side effect of setting mimetype ScopeGuard clear_avatar_data([&](){it->second->clearData();}); mimetype = it->second->mimetype(); if (!mimetype) mimetype = std::string(); } extension = MimeTypes::getExtension(*mimetype, "bin"); return extension; } signalbackup-tools-20250313-1/signalbackup/getcustomcolor.cc000066400000000000000000000126601476450434500237560ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include #if __cpp_lib_math_constants >= 201907L #include #endif std::pair SignalBackup::getCustomColor(std::pair, size_t> const &colordata) const { //std::cout << bepaald::bytesToHexString(colordata) << std::endl; /* message chatcolor/wallpaper { message SingleColor { int32 color = 1; } message LinearGradient { float rotation = 1; repeated int32 colors = 2; repeated float positions = 3; } message File { string uri = 1; } oneof wallpaper { SingleColor singleColor = 1; LinearGradient linearGradient = 2; File file = 3; // ONLY IN WALLPAPER } float dimLevelInDarkTheme = 4; // ONLY IN WALLPAPER } */ ProtoBufParser< ProtoBufParser, ProtoBufParser, ProtoBufParser, protobuffer::optional::FLOAT> color_proto(colordata); std::pair custom_colors; if (color_proto.getField<1>().has_value() && color_proto.getField<1>().value().getField<1>().has_value()) { uint32_t c = color_proto.getField<1>().value().getField<1>().value(); //std::cout << "Color value: " << c << std::endl; //uint8_t a = (c >> (3 * 8)) & 0xFF); uint8_t r = (c >> (2 * 8)) & 0xFF; uint8_t g = (c >> (1 * 8)) & 0xFF; uint8_t b = (c >> (0 * 8)) & 0xFF; std::stringstream ss; ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(r) << std::hex << std::setw(2) << std::setfill('0') << static_cast(g) << std::hex << std::setw(2) << std::setfill('0') << static_cast(b); custom_colors.first = ss.str(); //std::cout << custom_colors.first << std::endl; if (color_proto.getField<4>().has_value()) { double dimfactor = 1 - color_proto.getField<4>().value(); // to HSV double maxrgb = std::max(r, std::max(g, b)); double minrgb = std::min(r, std::min(g, b)); double v = maxrgb / 255; double s = (maxrgb > 0) ? 1 - minrgb / maxrgb : 0; #if __cpp_lib_math_constants >= 201907L double h = std::acos((r - 0.5 * g - 0.5 * b) / std::sqrt(r * r + g * g + b * b - r * g - r * b - g * b)) * 180 / std::numbers::pi; #else double h = std::acos((r - 0.5 * g - 0.5 * b) / std::sqrt(r * r + g * g + b * b - r * g - r * b - g * b)) * 180 / 3.14159265; #endif if (b > g) h = 360 - h; // apply dimming v *= dimfactor; // back to rgb: maxrgb = 255 * v; minrgb = maxrgb * (1 - s); double z = (maxrgb - minrgb) * (1 - std::abs(std::fmod((h / 60), 2) - 1)); std::stringstream ss2; if (h < 60) ss2 << std::hex << std::setfill('0') << std::setw(2) << std::lround(maxrgb) // r << std::hex << std::setfill('0') << std::setw(2) << std::lround(z + minrgb) // g << std::hex << std::setfill('0') << std::setw(2) << std::lround(minrgb); // b else if (h < 120) ss2 << std::hex << std::setfill('0') << std::setw(2) << std::lround(z + minrgb) // r << std::hex << std::setfill('0') << std::setw(2) << std::lround(maxrgb) // g << std::hex << std::setfill('0') << std::setw(2) << std::lround(minrgb); // b else if (h < 180) ss2 << std::hex << std::setfill('0') << std::setw(2) << std::lround(minrgb) // r << std::hex << std::setfill('0') << std::setw(2) << std::lround(maxrgb) // g << std::hex << std::setfill('0') << std::setw(2) << std::lround(z + minrgb); // b else if (h < 240) ss2 << std::hex << std::setfill('0') << std::setw(2) << std::lround(minrgb) // r << std::hex << std::setfill('0') << std::setw(2) << std::lround(z + minrgb) // g << std::hex << std::setfill('0') << std::setw(2) << std::lround(maxrgb); // b else if (h < 300) ss2 << std::hex << std::setfill('0') << std::setw(2) << std::lround(z + minrgb) // r << std::hex << std::setfill('0') << std::setw(2) << std::lround(minrgb) // g << std::hex << std::setfill('0') << std::setw(2) << std::lround(maxrgb); // b else //if (h < 360) ss2 << std::hex << std::setfill('0') << std::setw(2) << std::lround(maxrgb) // r << std::hex << std::setfill('0') << std::setw(2) << std::lround(minrgb) // g << std::hex << std::setfill('0') << std::setw(2) << std::lround(z + minrgb); // b custom_colors.second = ss2.str(); //std::cout << custom_colors.second << std::endl; } } return custom_colors; } signalbackup-tools-20250313-1/signalbackup/getdtreactions.cc000066400000000000000000000076121476450434500237250ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::getDTReactions(SqliteDB const &ddb, long long int rowid, long long int numreactions, std::vector> *reactions) const { SqliteDB::QueryResults results_emoji_reactions; //if (numreactions) // std::cout << " " << numreactions << " reactions." << std::endl; for (unsigned int k = 0; k < numreactions; ++k) { if (!ddb.exec("SELECT " "json_extract(messages.json, '$.reactions[" + bepaald::toString(k) + "].emoji') AS emoji," // not present in android database //"json_extract(messages.json, '$.reactions[" + bepaald::toString(k) + "].remove') AS remove," // THIS IS THE AUTHOR OF THE MESSAGE THATS REACTED TO //"json_extract(messages.json, '$.reactions[" + bepaald::toString(k) + "].targetAuthorUuid') AS target_author_uuid," //timestamp of message that reaction belongs to, dont know why this exists //"JSONLONG(json_extract(messages.json, '$.reactions[" + bepaald::toString(k) + "].targetTimestamp')) AS target_timestamp," "JSONLONG(json_extract(messages.json, '$.reactions[" + bepaald::toString(k) + "].timestamp')) AS timestamp," // THE ID OF THE CONVERSATION OF THE REACTION AUTHOR (conversation somewhat doubles android's recipient table) // ON OLDER DATABASES THIS IS PHONE NUMBER OF THE ACTUAL AUTHOR "json_extract(messages.json, '$.reactions[" + bepaald::toString(k) + "].fromId') AS from_id," //"json_extract(messages.json, '$.reactions[" + bepaald::toString(k) + "].source') AS source" // ??? "conversations." + d_dt_c_uuid + " AS uuid," "conversations.e164 AS phone" " FROM messages LEFT JOIN conversations ON" " (conversations.id IS json_extract(messages.json, '$.reactions[" + bepaald::toString(k) + "].fromId')" " OR " "conversations.e164 IS json_extract(messages.json, '$.reactions[" + bepaald::toString(k) + "].fromId'))" " WHERE rowid = ?", rowid, &results_emoji_reactions)) { Logger::error("Failed to get reaction data from desktop database. Skipping."); continue; } //std::cout << " Reaction " << k + 1 << "/" << numreactions << std::endl; //results_emoji_reactions.print(false); // DEBUG if (results_emoji_reactions.valueAsString(0, "uuid").empty() && results_emoji_reactions.valueAsString(0, "phone").empty()) [[unlikely]] { Logger::warning("Got empty author uuid, here is some additional info:"); ddb.print("SELECT json_extract(json, '$.reactions') FROM messages WHERE rowid = ?", rowid); results_emoji_reactions.printLineMode(); } reactions->emplace_back(std::vector{results_emoji_reactions.valueAsString(0, "emoji"), results_emoji_reactions.valueAsString(0, "timestamp"), results_emoji_reactions.valueAsString(0, "uuid"), results_emoji_reactions.valueAsString(0, "phone")}); } } signalbackup-tools-20250313-1/signalbackup/getfreedateformessage.cc000066400000000000000000000054461476450434500252440ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" long long int SignalBackup::getFreeDateForMessage(long long int targetdate, long long int thread_id, long long int from_recipient_id) const { //std::cout << "Getting free date for message: " << targetdate << " " << thread_id << " " << from_recipient_id << std::endl; // long long int freedate = d_database.getSingleResultAs("SELECT min(unused_date) AS unused_date FROM " // "(SELECT min(" + d_mms_date_sent + ") + 1 AS unused_date FROM " + d_mms_table + " AS t1 WHERE " // "thread_id = ? AND " // "from_recipient_id = ? " // "AND " + d_mms_date_sent + " >= ? AND " // "NOT EXISTS (SELECT * FROM message AS t2 WHERE t2." + d_mms_date_sent + " = t1." + d_mms_date_sent + " + 1 AND thread_id = ? AND from_recipient_id = ?) UNION " // "SELECT ? FROM " + d_mms_table + " WHERE NOT EXISTS (SELECT * FROM " + d_mms_table + " WHERE " + d_mms_date_sent + " = ?))", // {thread_id, from_recipient_id, targetdate, thread_id, from_recipient_id, targetdate, targetdate}, -1); int incr = 0; long long int freedate = -1; while ((freedate = d_database.getSingleResultAs("SELECT " + d_mms_date_sent + " FROM " + d_mms_table + " WHERE thread_id = ? AND from_recipient_id = ? AND " + d_mms_date_sent + " = ?", {thread_id, from_recipient_id, targetdate + incr}, -1)) != -1 && incr < 1000) { //std::cout << "date: " << freedate << " was taken" << std::endl; ++incr; } if (freedate != -1) return -1; return targetdate + incr; } signalbackup-tools-20250313-1/signalbackup/getgroupinfo.cc000066400000000000000000000234411476450434500234140ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::getGroupInfo(long long int rid, GroupInfo *groupinfo) const { std::pair, size_t> groupdata = d_database.tableContainsColumn("groups", "decrypted_group") ? d_database.getSingleResultAs, size_t>>("SELECT decrypted_group FROM groups WHERE recipient_id = ?", rid, {nullptr, 0}) : std::make_pair(nullptr, 0); if (!groupdata.first || !groupdata.second) return; /* message DecryptedGroup { string title = 2; string avatar = 3; DecryptedTimer disappearingMessagesTimer = 4; AccessControl accessControl = 5; uint32 revision = 6; repeated DecryptedMember members = 7; repeated DecryptedPendingMember pendingMembers = 8; repeated DecryptedRequestingMember requestingMembers = 9; bytes inviteLinkPassword = 10; string description = 11; EnabledState isAnnouncementGroup = 12; repeated DecryptedBannedMember bannedMembers = 13; } */ DecryptedGroup group_info(groupdata); //group_info.print(); // get announcementgroup if (group_info.getField<12>().has_value()) { /* enum EnabledState { UNKNOWN = 0; ENABLED = 1; DISABLED = 2; } */ long long int state = group_info.getField<12>().value(); if (state == 2) groupinfo->isannouncementgroup = false; else if (state == 1) groupinfo->isannouncementgroup = true; // 0 = unknown => false? } // get timer value: //std::cout << "=== TIMER:" << std::endl; if (group_info.getField<4>().has_value()) { /* message DecryptedTimer { uint32 duration = 1; } */ DecryptedTimer timerdata(group_info.getField<4>().value()); long long int timer = -1; if (timerdata.getField<1>().has_value()) timer = timerdata.getField<1>().value(); //std::cout << "Timer: " << timer << std::endl; if (timer != -1) groupinfo->expiration_timer = timer; } //std::cout << "===" << std::endl << std::endl; // get access control: //std::cout << "=== ACCESS CONTROL:" << std::endl; if (group_info.getField<5>().has_value()) { /* message AccessControl { enum AccessRequired { UNKNOWN = 0; ANY = 1; MEMBER = 2; ADMINISTRATOR = 3; UNSATISFIABLE = 4; } AccessRequired attributes = 1; AccessRequired members = 2; AccessRequired addFromInviteLink = 3; } */ auto enumToString = [] (int i) { switch (i) { case 1: return "Anyone"; case 2: return "All members"; case 3: return "Only admins"; case 4: return "No one"; case 0: default: return "Unknown"; } }; AccessControl acdata(group_info.getField<5>().value()); long long int attributes = 0; if (acdata.getField<1>().has_value()) attributes = acdata.getField<1>().value(); groupinfo->access_control_attributes = enumToString(attributes); long long int members = 0; if (acdata.getField<2>().has_value()) members = acdata.getField<2>().value(); groupinfo->access_control_members = enumToString(members); long long int addfrominvitelink = 0; if (acdata.getField<3>().has_value()) addfrominvitelink = acdata.getField<3>().value(); groupinfo->access_control_addfromlinkinvite = enumToString(addfrominvitelink); //std::cout << "Access control: " << attributes << " - " << members << " - " << addfrominvitelink << std::endl; } //std::cout << "===" << std::endl << std::endl; // get members: { //std::cout << "=== MEMBERS:" << std::endl; auto newmembers = group_info.getField<7>(); for (unsigned int i = 0; i < newmembers.size(); ++i) { /* message DecryptedMember { bytes uuid = 1; Member.Role role = 2; bytes profileKey = 3; uint32 joinedAtRevision = 5; bytes pni = 6; } enum Role { UNKNOWN = 0; DEFAULT = 1; ADMINISTRATOR = 2; } */ // uuid auto [uuid, uuid_size] = newmembers[i].getField<1>().value_or(std::make_pair(nullptr, 0)); // bytes if (uuid_size < 16) continue; std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); // role long long int role = -1; if (newmembers[i].getField<2>().has_value()) role = newmembers[i].getField<2>().value(); //std::cout << uuidstr << " (" << role << ")" << std::endl; if (role == 2) // ADMIN { long long int id = getRecipientIdFromUuidMapped(uuidstr, nullptr); if (id != -1) groupinfo->admin_ids.push_back(id); } } //std::cout << "===" << std::endl << std::endl; } // get pending members: { //std::cout << "=== PENDING MEMBERS:" << std::endl; auto pendingmembers = group_info.getField<8>(); for (unsigned int i = 0; i < pendingmembers.size(); ++i) { /* message DecryptedPendingMember { bytes uuid = 1; Member.Role role = 2; bytes addedByUuid = 3; uint64 timestamp = 4; bytes uuidCipherText = 5; } */ // uuid auto [uuid, uuid_size] = pendingmembers[i].getField<1>().value_or(std::make_pair(nullptr, 0)); // bytes if (uuid_size < 16) continue; std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); // // role // long long int role = -1; // if (pendingmembers[i].getField<2>().has_value()) // role = pendingmembers[i].getField<2>().value(); long long int id = getRecipientIdFromUuidMapped(uuidstr, nullptr); if (id != -1) groupinfo->pending_members.push_back(id); //std::cout << uuidstr << " (" << role << ")" << std::endl; } //std::cout << "===" << std::endl << std::endl; } // get requesting members: { //std::cout << "=== REQUESTING MEMBERS:" << std::endl; auto requestingmembers = group_info.getField<9>(); for (unsigned int i = 0; i < requestingmembers.size(); ++i) { /* message DecryptedRequestingMember { bytes uuid = 1; bytes profileKey = 2; uint64 timestamp = 4; } */ // uuid auto [uuid, uuid_size] = requestingmembers[i].getField<1>().value_or(std::make_pair(nullptr, 0)); // bytes if (uuid_size < 16) continue; std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); long long int id = getRecipientIdFromUuidMapped(uuidstr, nullptr); if (id != -1) groupinfo->requesting_members.push_back(id); //std::cout << uuidstr << std::endl; } //std::cout << "===" << std::endl << std::endl; } // get banned members: { //std::cout << "=== BANNED MEMBERS:" << std::endl; auto bannedmembers = group_info.getField<13>(); for (unsigned int i = 0; i < bannedmembers.size(); ++i) { /* message DecryptedBannedMember { bytes uuid = 1; uint64 timestamp = 2; } */ // uuid auto [uuid, uuid_size] = bannedmembers[i].getField<1>().value_or(std::make_pair(nullptr, 0)); // bytes if (uuid_size < 16) continue; std::string uuidstr = bepaald::bytesToHexString(uuid, uuid_size, true); uuidstr.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); long long int id = getRecipientIdFromUuidMapped(uuidstr, nullptr); if (id != -1) groupinfo->banned_members.push_back(id); //std::cout << uuidstr << std::endl; } //std::cout << "===" << std::endl << std::endl; } // get description //std::cout << "=== DESCRIPTION:" << std::endl; if (group_info.getField<11>().has_value()) { std::string desc = (group_info.getField<11>().value()); groupinfo->description = desc; //std::cout << desc << std::endl; } //std::cout << "===" << std::endl << std::endl; // get group invite password? //std::cout << "=== INVITE PW:" << std::endl; if (group_info.getField<10>().has_value()) { auto [pw, pwsize] = group_info.getField<10>().value(); //std::cout << bepaald::bytesToHexString(pw, pwsize) << std::endl; //std::cout << "(base64:) " << Base64::bytesToBase64String(pw, pwsize) << std::endl; if (pwsize) groupinfo->link_invite_enabled = true; } //std::cout << "===" << std::endl << std::endl; } signalbackup-tools-20250313-1/signalbackup/getgroupmembers.cc000066400000000000000000000047531476450434500241200ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::getGroupMembersModern(std::vector *members, std::string const &group_id) const { SqliteDB::QueryResults r; if (!d_database.containsTable("group_membership") || !d_database.exec("SELECT DISTINCT recipient_id FROM group_membership WHERE group_id = ?", group_id, &r)) return false; for (unsigned int i = 0; i < r.rows(); ++i) members->push_back(r.getValueAs(i, "recipient_id")); return true; } bool SignalBackup::getGroupMembersOld(std::vector *members, std::string const &group_id, std::string const &column) const { if (!members) return false; if (!d_database.tableContainsColumn("groups", column)) { if (column == "members") return getGroupMembersModern(members, group_id); else return false; } SqliteDB::QueryResults r; d_database.exec("SELECT " + column + " FROM groups WHERE group_id = ? AND " + column + " IS NOT NULL", group_id, &r); //r.prettyPrint(); if (r.rows() == 0) // no results return true; if (r.rows() > 1 || !r.valueHasType(0, column)) return false; // tokenize std::string membersstring(r.valueAsString(0, column)); std::regex comma(","); std::sregex_token_iterator iter(membersstring.begin(), membersstring.end(), comma, -1); std::transform(iter, std::sregex_token_iterator(), std::back_inserter(*members), [](std::string const &m) -> long long int { return bepaald::toNumber(m); }); // std::cout << "=====" << std::endl; // std::cout << "Set group members:" << std::endl; // for (auto const &id : *members) // std::cout << id << std::endl; // std::cout << "=====" << std::endl; return true; } signalbackup-tools-20250313-1/signalbackup/getgroupupdaterecipients.cc000066400000000000000000000124111476450434500260240ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" std::vector SignalBackup::getGroupUpdateRecipients(int thread) const { SqliteDB::QueryResults res; std::set uuids; std::vector queries{"SELECT "s + (d_database.tableContainsColumn(d_mms_table, "message_extras") ? "COALESCE(message_extras, body) AS groupctx" : "body AS groupctx") + " FROM " + d_mms_table + " WHERE (" + d_mms_type + " & ?) != 0 AND (" + d_mms_type + " & ?) != 0"s + (thread != -1 ? " AND thread_id = " + bepaald::toString(thread) : "")}; if (d_database.containsTable("sms")) queries.emplace_back("SELECT body AS groupctx FROM sms WHERE (type & ?) != 0 AND (type & ?) != 0"s + (thread != -1 ? " AND thread_id = " + bepaald::toString(thread) : "")); for (auto const &q : queries) { //d_database.exec("SELECT body FROM sms WHERE (type & ?) != 0 AND (type & ?) != 0", d_database.exec(q, {Types::GROUP_UPDATE_BIT, Types::GROUP_V2_BIT}, &res); for (unsigned int i = 0; i < res.rows(); ++i) { // std::cout << "GROUPCTX: " << "\"" << res.valueAsString(i, "groupctx") << "\"" << std::endl; if (res.valueHasType, size_t>>(i, "groupctx")) { //std::cout << "FROM BLOB 1" << std::endl; MessageExtras me(res.getValueAs, size_t>>(i, "groupctx")); //me.print(); //std::cout << "---" << std::endl; auto field1 = me.getField<1>(); if (field1.has_value()) { auto field1_1 = field1->getField<1>(); if (field1_1.has_value()) getGroupUpdateRecipientsFromGV2Context(*field1_1, &uuids); } // std::cout << "FROM BLOB 2" << std::endl; // sts2.print(); } else // valueHasType { getGroupUpdateRecipientsFromGV2Context(DecryptedGroupV2Context{res.valueAsString(i, "groupctx")}, &uuids); //std::cout << "FROM BASE64STRING" << std::endl; //sts2.print(); } } } // std::cout << "LIST OF FOUND UUIDS:" << std::endl; // for (auto &uuid : uuids) // std::cout << uuid << " (" << uuid.length() << ")" << std::endl; std::vector ids; if (uuids.size()) { std::string q = "SELECT DISTINCT _id FROM recipient WHERE LOWER(" + d_recipient_aci + ") IN ("; #if __cplusplus > 201703L for (int pos = 0; std::string uuid : uuids) #else int pos = 0; for (std::string uuid : uuids) #endif { if (uuid.length() < 32) [[unlikely]] continue; if (pos > 0) q += ", "; uuid.insert(8, 1, '-').insert(13, 1, '-').insert(18, 1, '-').insert(23, 1, '-'); q += "LOWER('" + uuid + "')"; ++pos; } q += ")"; d_database.exec(q, &res); for (unsigned int i = 0; i < res.rows(); ++i) ids.push_back(res.getValueAs(i, "_id")); } return ids; } void SignalBackup::getGroupUpdateRecipientsFromGV2Context(DecryptedGroupV2Context const &sts2, std::set *uuids) const { // NEW DATA auto field3 = sts2.getField<3>(); if (field3.has_value()) { auto field3_7 = field3->getField<7>(); for (unsigned int j = 0; j < field3_7.size(); ++j) { auto field3_7_1 = field3_7[j].getField<1>(); if (field3_7_1.has_value()) uuids->insert(bepaald::bytesToHexString(*field3_7_1, true)); // else // { // std::cout << "No members found in field 3" << std::endl; // sts2.print(); // } } // if (field3_7.size() == 0) // { // std::cout << "No members found in field 3" << std::endl; // sts2.print(); // } } // else // { // std::cout << "No members found in field 3" << std::endl; // sts2.print(); // } // OLD DATA? auto field4 = sts2.getField<4>(); if (field4.has_value()) { auto field4_7 = field4->getField<7>(); for (unsigned int j = 0; j < field4_7.size(); ++j) { auto field4_7_1 = field4_7[j].getField<1>(); if (field4_7_1.has_value()) uuids->insert(bepaald::bytesToHexString(*field4_7_1, true)); // else // { // std::cout << "No members found in field 4" << std::endl; // sts2.print(); // } } // if (field4_7.size() == 0) // { // std::cout << "No members found in field 4" << std::endl; // sts2.print(); // } } // else // { // std::cout << "No members found in field 4" << std::endl; // sts2.print(); // } } signalbackup-tools-20250313-1/signalbackup/getgroupv1migrationrecipients.cc000066400000000000000000000046451476450434500270140ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::getGroupV1MigrationRecipients(std::set *referenced_recipients, long long int thread) const { SqliteDB::QueryResults results; if (d_database.exec("SELECT body FROM "s + (d_database.containsTable("sms") ? "sms" : d_mms_table) + " WHERE " + (d_database.containsTable("sms") ? "type" : d_mms_type) + " == ?" + (thread != -1 ? " AND thread_id = " + bepaald::toString(thread) : ""), bepaald::toString(Types::GV1_MIGRATION_TYPE), &results)) { //results.prettyPrint(); for (unsigned int i = 0; i < results.rows(); ++i) { if (results.valueHasType(i, "body")) { //std::cout << results.getValueAs(i, "body") << std::endl; std::string body = results.getValueAs(i, "body"); std::string tmp; // to hold part of number while reading unsigned int body_idx = 0; while (true) { if (body_idx >= body.length() || !std::isdigit(body[body_idx])) // we are reading '|', ',' or end of string { // deal with any number we have if (tmp.size()) { referenced_recipients->insert(bepaald::toNumber(tmp)); if (d_verbose) [[unlikely]] Logger::message(" Got recipient from GV1_MIGRATION: ", tmp); tmp.clear(); } } else // we are reading (part of) a number tmp += body[body_idx]; ++body_idx; if (body_idx > body.length()) break; } } } } } signalbackup-tools-20250313-1/signalbackup/getminmaxusedid.cc000066400000000000000000000031751476450434500240750ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" long long int SignalBackup::getMinUsedId(std::string const &table, std::string const &col) const { if (!d_database.containsTable(table)) return 0; SqliteDB::QueryResults results; d_database.exec("SELECT MIN(" + col + ") FROM " + table, &results); if (results.rows() != 1 || results.columns() != 1 || !results.valueHasType(0, 0)) { return 0; } return results.getValueAs(0, 0); } long long int SignalBackup::getMaxUsedId(std::string const &table, std::string const &col) const { if (!d_database.containsTable(table)) return 0; SqliteDB::QueryResults results; d_database.exec("SELECT MAX(" + col + ") FROM " + table, &results); if (results.rows() != 1 || results.columns() != 1 || !results.valueHasType(0, 0)) { return 0; } return results.getValueAs(0, 0); } signalbackup-tools-20250313-1/signalbackup/getnamefromrecipientid.cc000066400000000000000000000045671476450434500254400ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" std::string SignalBackup::getNameFromRecipientId(long long int rid) const { SqliteDB::QueryResults results; if (d_database.exec("SELECT COALESCE(" + (d_database.tableContainsColumn("recipient", "nickname_joined_name") ? "NULLIF(recipient.nickname_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_system_joined_name + ", ''), " + (d_database.tableContainsColumn("recipient", "profile_joined_name") ? "NULLIF(recipient.profile_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_profile_given_name + ", ''), NULLIF(groups.title, ''), " + (d_database.containsTable("distribution_list") ? "NULLIF(distribution_list.name, ''), " : "") + "NULLIF(recipient." + d_recipient_aci + ", ''), NULLIF(recipient." + d_recipient_e164 + ", ''), " " recipient._id) AS 'display_name',recipient." + d_recipient_e164 + (d_database.tableContainsColumn("recipient", "username") ? ",recipient.username" : "") + ",recipient." + d_recipient_aci + " " "FROM recipient " "LEFT JOIN groups ON recipient.group_id = groups.group_id " + (d_database.containsTable("distribution_list") ? "LEFT JOIN distribution_list ON recipient._id = distribution_list.recipient_id " : "") + "WHERE recipient._id = ?", rid, &results) && results.rows() == 1 && results.valueHasType(0, "display_name")) return results.valueAsString(0, "display_name"); return std::string(); } signalbackup-tools-20250313-1/signalbackup/getrecipientidfrom.cc000066400000000000000000000075321476450434500245720ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" long long int SignalBackup::getRecipientIdFromName(std::string const &name, bool withthread) const { SqliteDB::QueryResults results; if (d_database.exec("SELECT recipient._id, thread._id " "FROM recipient " "LEFT JOIN groups ON recipient.group_id = groups.group_id " + (d_database.containsTable("distribution_list") ? "LEFT JOIN distribution_list ON recipient._id = distribution_list.recipient_id "s : ""s) + "LEFT JOIN thread ON recipient._id = thread." + d_thread_recipient_id + " WHERE " "COALESCE(" + (d_database.tableContainsColumn("recipient", "nickname_joined_name") ? "NULLIF(recipient.nickname_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_system_joined_name + ", ''), " + (d_database.tableContainsColumn("recipient", "profile_joined_name") ? "NULLIF(recipient.profile_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_profile_given_name + ", ''), NULLIF(groups.title, ''), " + (d_database.containsTable("distribution_list") ? "NULLIF(distribution_list.name, ''), " : "") + "NULLIF(recipient." + d_recipient_aci + ", ''), NULLIF(recipient." + d_recipient_e164 + ", ''), " " recipient._id) = ?" + (withthread ? " AND thread._id IS NOT NULL" : ""), name, &results)) { //results.prettyPrint(d_truncate); // no results if (results.rows() == 0) return -1; // multiple hits for 'name' if (results.rows() > 1) { Logger::warning("Got multiple results for recipient `", name, "'"); return -1; } // ok return results.valueAsInt(0, "_id"); } // some error executing query return -1; } long long int SignalBackup::getRecipientIdFromField(std::string const &field, std::string const &value, bool withthread) const { if (!d_database.tableContainsColumn("recipient", field)) return -1; SqliteDB::QueryResults results; if (d_database.exec("SELECT recipient._id, thread._id " "FROM recipient " "LEFT JOIN thread ON recipient._id = thread." + d_thread_recipient_id + " WHERE " + field + " = ?" + (withthread ? " AND thread._id IS NOT NULL" : ""), value, &results)) { //results.prettyPrint(d_truncate); // no results if (results.rows() == 0) return -1; // multiple hits for 'field' if (results.rows() > 1) { Logger::warning("Got multiple results for recipient `", field, "'"); return -1; } // ok return results.valueAsInt(0, "_id"); } // some error executing query return -1; } long long int SignalBackup::getRecipientIdFromPhone(std::string const &phone, bool withthread) const { return getRecipientIdFromField(d_recipient_e164, phone, withthread); } long long int SignalBackup::getRecipientIdFromUsername(std::string const &username, bool withthread) const { return getRecipientIdFromField("username", username, withthread); } signalbackup-tools-20250313-1/signalbackup/getrecipientidfrommapped.cc000066400000000000000000000062641476450434500257620ustar00rootroot00000000000000/* Copyright (C) 2022-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" long long int SignalBackup::getRecipientIdFromUuidMapped(std::string const &uuid, std::map *savedmap, bool suppresswarning) const { if (uuid.empty()) { Logger::error("Asked to find recipient._id for empty uuid. Refusing"); return -1; } if (savedmap) if (auto found = savedmap->find(uuid); found != savedmap->end()) return found->second; // either savedmap does not exist, or uuid is not in there: std::string printable_uuid(makePrintable(uuid)); SqliteDB::QueryResults res; if (!d_database.exec("SELECT recipient._id FROM recipient WHERE " + d_recipient_aci + " = ?1 COLLATE NOCASE OR group_id = ?1 COLLATE NOCASE", uuid, &res) || res.rows() != 1 || !res.valueHasType(0, 0)) { if (!suppresswarning) // we can suppress this warning, if we are creating the contact after not finding it... Logger::warning("Failed to find recipient for uuid: ", printable_uuid); return -1; } //res.prettyPrint(); if (savedmap) (*savedmap)[uuid] = res.getValueAs(0, 0); return res.getValueAs(0, 0); } long long int SignalBackup::getRecipientIdFromPhoneMapped(std::string const &phone, std::map *savedmap, bool suppresswarning) const { if (phone.empty()) { Logger::error("Asked to find recipient._id for empty e164. Refusing"); return -1; } if (savedmap) if (auto found = savedmap->find(phone); found != savedmap->end()) return found->second; // either savedmap does not exist, or phone is not in there: std::string printable_phone(phone); unsigned int offset = 3; if (offset < phone.size()) [[likely]] std::replace_if(printable_phone.begin() + offset, printable_phone.end(), [](char c){ return std::isdigit(c); }, 'x'); else printable_phone = "xxx"; SqliteDB::QueryResults res; if (!d_database.exec("SELECT recipient._id FROM recipient WHERE " + d_recipient_e164 + " = ? COLLATE NOCASE", phone, &res) || res.rows() != 1 || !res.valueHasType(0, 0)) { if (!suppresswarning) Logger::warning("Failed to find recipient for phone: ", printable_phone); return -1; } //res.prettyPrint(); if (savedmap) (*savedmap)[phone] = res.getValueAs(0, 0); return res.getValueAs(0, 0); } signalbackup-tools-20250313-1/signalbackup/getthreadidfromrecipient.cc000066400000000000000000000027711476450434500257620ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../common_be.h" long long int SignalBackup::getThreadIdFromRecipient(std::string const &recipient) const { // note: for d_database < 24, recipient == "+316xxxxxxxx" || "__text_secure__!..." // >= 24, recipient is just an int id long long int tid = -1; SqliteDB::QueryResults results; d_database.exec("SELECT _id FROM thread WHERE " + d_thread_recipient_id + " = ?", recipient, &results); if (results.rows() == 1 && results.columns() == 1 && results.valueHasType(0, 0)) tid = results.getValueAs(0, 0); return tid; } long long int SignalBackup::getThreadIdFromRecipient(long long int recipientid) const { return getThreadIdFromRecipient(bepaald::toString(recipientid)); } signalbackup-tools-20250313-1/signalbackup/gettranslatedname.cc000066400000000000000000000024541476450434500244070ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" std::string SignalBackup::getTranslatedName(std::string const &table, std::string const &old_column_name) const { if (auto it = s_columnaliases.find(table); it != s_columnaliases.end()) for (auto const &v : it->second) if (bepaald::contains(v, old_column_name)) for (auto const &col_name : v) if (d_database.tableContainsColumn(table, col_name)) { //Logger::message("TRANSLATING COLUMN NAME: ", old_column_name, " -> ", col_name); return col_name; } return std::string(); } signalbackup-tools-20250313-1/signalbackup/groupinfo.h000066400000000000000000000023461476450434500225570ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.h" #include struct GroupInfo { std::list admin_ids; long long int expiration_timer = 0; bool link_invite_enabled = false; std::string description; std::vector pending_members; std::vector requesting_members; std::vector banned_members; std::string access_control_attributes; std::string access_control_members; std::string access_control_addfromlinkinvite; bool isannouncementgroup = false; }; signalbackup-tools-20250313-1/signalbackup/handledtcalltypemessage.cc000066400000000000000000000247471476450434500256040ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::handleDTCallTypeMessage(SqliteDB const &ddb, std::string const &callid, long long int rowid, long long int ttid, long long int address, bool insertincompletedataforexport) const { SqliteDB::QueryResults calldetails; uint64_t calltype = 0; std::any body = nullptr; if (ddb.containsTable("callsHistory")) { // new style! get details from this table /* sqlite> SELECT * from callsHistory; */ if (!ddb.exec("SELECT " "timestamp AS sent_at, " "mode, " "type, " "peerId, " "direction, " "status" " FROM callsHistory WHERE callId = ?", callid, &calldetails)) { Logger::error("Failed to get call details from desktop database. Skipping."); return false; } if (calldetails.valueAsString(0, "mode") == "Direct") { /* sqlite> SELECT * from callsHistory; callId = 4690462596594407498 peerId = 93722273-78e3-4136-8640-c8261969714c ringerId = mode = Direct type = Audio direction = Incoming status = Accepted timestamp = 1693839545545 */ if (calldetails.valueAsString(0, "type") == "Video") { if (calldetails.valueAsString(0, "direction") == "Incoming") { if (calldetails.valueAsString(0, "status") == "Accepted") calltype = Types::INCOMING_VIDEO_CALL_TYPE; else calltype = Types::MISSED_VIDEO_CALL_TYPE; } else // direction = outgoing calltype = Types::OUTGOING_VIDEO_CALL_TYPE; } else // type == "Audio" { if (calldetails.valueAsString(0, "direction") == "Incoming") { if (calldetails.valueAsString(0, "status") == "Accepted") calltype = Types::INCOMING_CALL_TYPE; else calltype = Types::MISSED_CALL_TYPE; } else// direction = outgoing calltype = Types::OUTGOING_CALL_TYPE; } } else if (calldetails.valueAsString(0, "mode") == "Group") { /* callId = 13523070632477333339 peerId = 8FqrnHI/W7ZKnVmfmQEGX3oxRrFeriGy3KT2Ih/dAww= ringerId = mode = Group type = Group direction = Incoming status = Accepted timestamp = 1693840579538 */ // NOTE THE ANDROID BACKUP NEEDS THE CALL INITIATOR ADDRESS. THIS IS NOT AVAILABLE IN THE DESKTOP DATABASE ANYMORE if (!insertincompletedataforexport) return true; calltype = Types::GROUP_CALL_TYPE; // always video? ProtoBufParser groupcallbody; groupcallbody.addField<3>(calldetails.getValueAs(0, "sent_at")); // cheating body = groupcallbody.getDataString(); } } else { if (!ddb.exec("SELECT " "COALESCE(sent_at, json_extract(json, '$.sent_at'), json_extract(json, '$.received_at_ms'), received_at, json_extract(json, '$.received_at')) AS sent_at," "json_extract(json, '$.callHistoryDetails.callMode') AS mode," "json_extract(json, '$.callHistoryDetails.creatorUuid') AS creator_uuid," "json_extract(json, '$.callHistoryDetails.eraId') AS era_id," "json_extract(json, '$.callHistoryDetails.startedTime') AS started_time," "IFNULL(json_extract(json, '$.callHistoryDetails.wasIncoming'), false) AS incoming," "IFNULL(json_extract(json, '$.callHistoryDetails.wasVideoCall'), false) AS video," "IFNULL(json_extract(json, '$.callHistoryDetails.wasDeclined'), false) AS declined," "IFNULL(json_extract(json, '$.callHistoryDetails.acceptedTime'), -1) AS accepted" " FROM messages WHERE rowid = ?", rowid, &calldetails)) { Logger::error("Failed to get call details from desktop database. Skipping."); return false; } //calldetails.prettyPrint(); if (calldetails.valueAsString(0, "mode") == "Direct") { if (calldetails.getValueAs(0, "video")) { if (calldetails.getValueAs(0, "incoming")) { if (calldetails.getValueAs(0, "accepted") >= 0) calltype = Types::INCOMING_VIDEO_CALL_TYPE; else calltype = Types::MISSED_VIDEO_CALL_TYPE; } else calltype = Types::OUTGOING_VIDEO_CALL_TYPE; } else { if (calldetails.getValueAs(0, "incoming")) { if (calldetails.getValueAs(0, "accepted") >= 0) calltype = Types::INCOMING_CALL_TYPE; else calltype = Types::MISSED_CALL_TYPE; } else calltype = Types::OUTGOING_CALL_TYPE; } } else if (calldetails.valueAsString(0, "mode") == "Group") { calltype = Types::GROUP_CALL_TYPE; // always video? //"callHistoryDetails":{"callMode":"Group","creatorUuid":"93722273-78e3-4136-8640-c8261969714c","eraId":"5d36bc8b0d6a1c5d","startedTime":1669314425500} // -> // _id = 18 // thread_id = 4 // address = 4 // address_device_id = 1 // person = // date = 1669314409536 // date_sent = 1669314409536 // date_server = -1 // protocol = // read = 1 // status = -1 // type = 12 // reply_path_present = // delivery_receipt_count = 0 // subject = // body = ChA1ZDM2YmM4YjBkNmExYzVkEiQ5MzcyMjI3My03OGUzLTQxMzYtODY0MC1jODI2MTk2OTcxNGMYwOiR18ow // mismatched_identities = // service_center = // subscription_id = -1 // expires_in = 0 // expire_started = 0 // notified = 0 // read_receipt_count = 0 // unidentified = 0 // reactions_unread = 0 // reactions_last_seen = 1669314426630 // remote_deleted = 0 // notified_timestamp = 1669314422531 // server_guid = // receipt_timestamp = -1 // export_state = // exported = 0 // // BODY PROTO // message GroupCallUpdateDetails { // string eraId = 1; // string startedCallUuid = 2; // int64 startedCallTimestamp = 3; // repeated string inCallUuids = 4; // bool isCallFull = 5; // } // // body = // Field #1: 0A String Length = 16, Hex = 10, UTF8 = "5d36bc8b0d6a1c5d" // Field #2: 12 String Length = 36, Hex = 24, UTF8 = "93722273-78e3-41 ..." (total 36 chars) // Field #3: 18 Varint Value = 1669314409536, Hex = C0-E8-91-D7-CA-30 ProtoBufParser groupcallbody; groupcallbody.addField<1>(calldetails.valueAsString(0, "era_id")); groupcallbody.addField<2>(calldetails.valueAsString(0, "creator_uuid")); groupcallbody.addField<3>(calldetails.getValueAs(0, "started_time")); body = groupcallbody.getDataString(); } } // if (d_databaseversion < 170) //{ if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) { if (!insertRow(d_database.containsTable("sms") ? "sms" : d_mms_table, {{"thread_id", ttid}, {d_database.containsTable("sms") ? d_sms_recipient_id : d_mms_recipient_id, address}, {d_database.containsTable("sms") ? d_sms_date_received : "date_received", calldetails.value(0, "sent_at")}, {"date_sent", calldetails.value(0, "sent_at")}, {"type", calltype}, {"body", body}})) { Logger::warning("Failed inserting into ", (d_database.containsTable("sms") ? "sms" : d_mms_table), ": call type message."); return false; } } else { // newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so // we try to get the first free date_sent long long int freedate = getFreeDateForMessage(calldetails.getValueAs(0, "sent_at"), ttid, Types::isOutgoing(calltype) ? d_selfid : address); if (freedate == -1) { Logger::error("Getting free date for call type message"); return false; } if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_recipient_id, Types::isOutgoing(calltype) ? d_selfid : address}, {"to_recipient_id", Types::isOutgoing(calltype) ? address : d_selfid}, {"date_received", freedate},//calldetails.value(0, "sent_at")}, {"date_sent", freedate},//calldetails.value(0, "sent_at")}, {"type", calltype}, {"body", body}})) { Logger::warning("Failed inserting into ", d_mms_table, ": call type message."); return false; } } //} /* else // dbv >=170 -> call into 'call' table???, or also in mms -> just additional details? { insertRow("call", {{call_id, ???}, {message_id, ???}, {peer, address?}, {type, calltype?}, {direction, ...}, {event, ???} } } */ return true; } signalbackup-tools-20250313-1/signalbackup/handledtexpirationchangemessage.cc000066400000000000000000000171041476450434500273040ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::handleDTExpirationChangeMessage(SqliteDB const &ddb, long long int rowid, long long int ttid, long long int sent_at, long long int address) const { SqliteDB::QueryResults timer_results; if (!ddb.exec("SELECT " "type, " "conversationId, " "IFNULL(json_extract(json,'$.expirationTimerUpdate.fromGroupUpdate'), false) AS fromgroupupdate, " "IFNULL(json_extract(json,'$.expirationTimerUpdate.fromSync'), false) AS fromsync, " "IFNULL(json_extract(json,'$.expirationTimerUpdate.expireTimer'), 0) AS expiretimer, " "json_extract(json,'$.expirationTimerUpdate.source') AS source, " "COALESCE(json_extract(json,'$.expirationTimerUpdate.sourceServiceId'), json_extract(json,'$.expirationTimerUpdate.sourceUuid')) AS sourceuuid " "FROM messages WHERE rowid = ?", rowid, &timer_results)) { Logger::error("Querying database"); return false; } // 'from sync' timer updates do not have any info on who set the timer. // On Android, the message must be either incoming or outgoing, but I can // only guess. 50-50 of having correct or incorrect info in the database, // let's just skip. if (timer_results.valueAsString(0, "fromsync") != "0") { Logger::warning("Unsupported message type 'timer-notification (fromSync=true)'. Skipping..."); return true; // non-fatal error } // get details (who sent this, what's the new timer value long long int timer = timer_results.getValueAs(0, "expiretimer"); bool incoming = (timer_results.valueAsString(0, "type") == "incoming"); if (!incoming) { // source is often uuid/phone of whoever set the timer? (maybe not on old messages std::string source = timer_results.valueAsString(0, "source"); SqliteDB::QueryResults convresults; if (ddb.exec("SELECT id FROM conversations WHERE e164 = ?1 OR " + d_dt_c_uuid + " = ?1", source, &convresults) && convresults.rows() == 1) if (convresults.valueAsString(0, "id") == timer_results.valueAsString(0, "conversationId")) { //std::cout << convresults(0, "id") << "=" << timer_results(0, "conversationId") << std::endl; incoming = true; } // if it is outgoing, source would be a conversationId in conversations SqliteDB::QueryResults sourceresults; if (ddb.exec("SELECT " + d_dt_c_uuid + " FROM conversations WHERE id IS ?1 OR e164 = ?1", source, &sourceresults) && sourceresults.rows() != 1) incoming = true; } // 10747927 (outgoing type) = PUSH_MESSAGE_BIT | SECURE_MESSAGE_BIT | EXPIRATION_TIMER_UPDATE_BIT | BASE_SENT_TYPE // 10747924 (incoming type) = PUSH_MESSAGE_BIT | SECURE_MESSAGE_BIT | EXPIRATION_TIMER_UPDATE_BIT | BASE_INBOX_TYPE // std::cout << rowid // << "|" << (incoming ? "incoming" : "outgoing") // << "|" << ttid // << "|" << address // << "|" << timer // << std::endl; if (d_database.containsTable("sms")) { if (!insertRow("sms", {{"thread_id", ttid}, {"date_sent", sent_at}, {d_sms_date_received, sent_at}, {"type", Types::PUSH_MESSAGE_BIT | Types::SECURE_MESSAGE_BIT | Types::EXPIRATION_TIMER_UPDATE_BIT | (incoming ? Types::BASE_INBOX_TYPE : Types::BASE_SENT_TYPE)}, {"expires_in", timer * 1000}, {"read", 1}, // hardcoded to 1 in Signal Android (for profile-change) {d_sms_recipient_id, address}})) { Logger::error("Inserting expiration-timer-update into sms"); return false; } } else { if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) { if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, sent_at}, {"date_received", sent_at}, {d_mms_type, Types::PUSH_MESSAGE_BIT | Types::SECURE_MESSAGE_BIT | Types::EXPIRATION_TIMER_UPDATE_BIT | (incoming ? Types::BASE_INBOX_TYPE : Types::BASE_SENT_TYPE)}, {"m_type", (incoming ? 132 : 128)}, {"expires_in", timer * 1000}, {"read", 1}, // hardcoded to 1 in Signal Android (for profile-change) {d_mms_recipient_id, address}})) { Logger::error("Inserting expiration-timer-update into mms"); return false; } } else { // newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so // we try to get the first free date_sent long long int freedate = getFreeDateForMessage(sent_at, ttid, incoming ? address : d_selfid); if (freedate == -1) { Logger::error("Getting free date for inserting expiration-timer-update message into mms"); return false; } if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, freedate},//sent_at}, {"date_received", freedate},//sent_at}, {d_mms_type, Types::PUSH_MESSAGE_BIT | Types::SECURE_MESSAGE_BIT | Types::EXPIRATION_TIMER_UPDATE_BIT | (incoming ? Types::BASE_INBOX_TYPE : Types::BASE_SENT_TYPE)}, {"m_type", (incoming ? 132 : 128)}, {"expires_in", timer * 1000}, {"read", 1}, // hardcoded to 1 in Signal Android (for profile-change) {d_mms_recipient_id, incoming ? address : d_selfid}, {"to_recipient_id", incoming ? d_selfid : address}})) { Logger::message("Inserting expiration-timer-update into mms"); return false; } } } return true; } /* (in group converstations recipient uuid of contact setting the timer = sourceUuid, but this is group-update message on Android, so not handled here) in 1-on-1: json$.expirationTimerUpdate.source == conversationId of person setting the timer (IF SELF!) json$.expirationTimerUpdate.source == recipientUuid of person setting the timer (IF OTHER!) json$.expirationTimerUpdate = not null json$.expirationTimerUpdate.expireTimer = some value (in seconds or milliseconds?) or not present when disabling timer IF json$.expirationTimerUpdate.fromSync = true -> SOURCE WILL BE UNKNOWN */ signalbackup-tools-20250313-1/signalbackup/handledtgroupchangemessage.cc000066400000000000000000000375531476450434500262700ustar00rootroot00000000000000/* Copyright (C) 2022-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" /* It seems the desktop message does not contain most of the info of the phone message. For example the creation message: ("type":"group-v2-change","groupV2Change":{"from":"0d70b7f4-fe4a-41af-9fd5-e74268d13f6e","details":[{"type":"create"}]}" has no group title, no group memberlist (uuids, profilekeys, roles, accesscontrol...) Also, the same rules for sms/mms database apply for incoming/outgoing messages (since these are all group messages), but the desktop database does not say if the messages are incoming or outgoing. Only the source uuid ('from'), but we don't know without scanning other messages (and possibly cant know if unlucky), which uuid is self and which are others. */ bool SignalBackup::handleDTGroupChangeMessage(SqliteDB const &ddb, long long int rowid, long long int thread_id, long long int address, long long int date, std::map *adjusted_timestamps, std::map *savedmap, std::string const &databasedir, bool istimermessage, bool createcontacts, bool createvalidcontacts, bool *warn) { if (date == -1) { // print wrn return false; } if (istimermessage) { SqliteDB::QueryResults timer_results; if (!ddb.exec("SELECT " "type, " "conversationId, " "IFNULL(json_extract(json,'$.expirationTimerUpdate.fromGroupUpdate'), false) AS fromgroupupdate, " "IFNULL(json_extract(json,'$.expirationTimerUpdate.fromSync'), false) AS fromsync, " "IFNULL(json_extract(json,'$.expirationTimerUpdate.expireTimer'), 0) AS expiretimer, " "json_extract(json,'$.expirationTimerUpdate.source') AS source, " "COALESCE(json_extract(json,'$.expirationTimerUpdate.sourceServiceId'), json_extract(json,'$.expirationTimerUpdate.sourceUuid')) AS sourceuuid " "FROM messages WHERE rowid = ?", rowid, &timer_results)) { Logger::error("Querying database"); return false; } bool incoming = bepaald::toLower(timer_results("sourceuuid")) != d_selfuuid; long long int timer = timer_results.getValueAs(0, "expiretimer"); long long int groupv2type = Types::SECURE_MESSAGE_BIT | Types::PUSH_MESSAGE_BIT | Types::GROUP_V2_BIT | Types::GROUP_UPDATE_BIT | (incoming ? Types::BASE_INBOX_TYPE : Types::BASE_SENDING_TYPE); // at this point address is the group_recipient. This is good for outgoing messages, // but incoming should have individual_recipient if (timer_results("sourceuuid").empty()) return false; if (incoming) { address = getRecipientIdFromUuidMapped(timer_results("sourceuuid"), savedmap); if (address == -1) { if (createcontacts) { if ((address = dtCreateRecipient(ddb, timer_results("sourceuuid"), std::string(), std::string(), databasedir, savedmap, createvalidcontacts, warn)) == -1) { Logger::error("Failed to create group-v2-expiration-timer contact (1), skipping"); return false; } } else { Logger::error("Failed to create group-v2-expiration-timer contact (2), skipping"); return false; } } } //std::cout << "Got timer message: " << timer << std::endl; DecryptedTimer dt; dt.addField<1>(timer); DecryptedGroupChange groupchange; groupchange.addField<12>(dt); DecryptedGroupV2Context groupv2ctx; groupv2ctx.addField<2>(groupchange); std::pair groupchange_data(groupv2ctx.data(), groupv2ctx.size()); std::string groupchange_data_b64 = Base64::bytesToBase64String(groupchange_data); // add message to database // if (d_database.containsTable("sms")) // not going through the trouble // else // { if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) { if (!insertRow(d_mms_table, {{"thread_id", thread_id}, {d_mms_date_sent, date}, {"date_received", date}, {"body", groupchange_data_b64}, {d_mms_type, groupv2type}, {d_mms_recipient_id, address}, {"m_type", incoming ? 132 : 128}, {"read", 1}})) // hardcoded to 1 in Signal Android { Logger::error("Inserting verified-change into mms"); return false; } } else { //newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so //we try to get the first free date_sent long long int freedate = getFreeDateForMessage(date, thread_id, Types::isOutgoing(groupv2type) ? d_selfid : address); if (freedate == -1) { Logger::error("Getting free date for inserting verified-change message into mms"); return false; } if (date != freedate) (*adjusted_timestamps)[date] = freedate; std::any newmms_id; if (!insertRow(d_mms_table, {{"thread_id", thread_id}, {d_mms_date_sent, freedate}, {"date_received", freedate}, {"body", groupchange_data_b64}, {d_mms_type, groupv2type}, {d_mms_recipient_id, incoming ? address : d_selfid}, {"to_recipient_id", incoming ? d_selfid : address}, {"m_type", incoming ? 132 : 128}, {"read", 1}}, "_id", &newmms_id)) // hardcoded to 1 in Signal Android { Logger::error("Inserting verified-change into mms"); return false; } } return true; } // !istimermessage SqliteDB::QueryResults res; if (!ddb.exec("SELECT " "LOWER(json_extract(json, '$.groupV2Change.from')) AS source," "IFNULL(json_array_length(json, '$.groupV2Change.details'), 0) AS numchanges" " FROM messages WHERE rowid = ?", rowid, &res)) return false; //res.prettyPrint(); long long int numchanges = res.getValueAs(0, "numchanges"); if (numchanges == 0) return false; std::string source_uuid = res("source"); if (STRING_STARTS_WITH(source_uuid, "pni")) // get real uuid if source was a "PNI:" type id...? { std::string realuuid = ddb.getSingleResultAs("SELECT " + d_dt_c_uuid + " FROM conversations WHERE LOWER(json_extract(json, '$.pni')) IS ?", source_uuid, std::string()); if (!realuuid.empty()) source_uuid = std::move(realuuid); } bool incoming = source_uuid != d_selfuuid; long long int groupv2type = Types::SECURE_MESSAGE_BIT | Types::PUSH_MESSAGE_BIT | Types::GROUP_V2_BIT | Types::GROUP_UPDATE_BIT | (incoming ? Types::BASE_INBOX_TYPE : Types::BASE_SENDING_TYPE); if (incoming) { address = getRecipientIdFromUuidMapped(source_uuid, savedmap); if (address == -1) { if (createcontacts) { if ((address = dtCreateRecipient(ddb, source_uuid, std::string(), std::string(), databasedir, savedmap, createvalidcontacts, warn)) == -1) { Logger::error("Failed to create group-v2-update contact (1), skipping"); return false; } } else { Logger::error("Failed to create group-v2-update contact (2), skipping"); return false; } } } DecryptedGroupV2Context groupv2ctx; DecryptedGroupChange groupchange; bool addchange = false; // add each group change... for (unsigned int i = 0; i < numchanges; ++i) { if (!ddb.exec("SELECT " "json_extract(json, '$.groupV2Change.details[' || ? || '].type') AS type," "COALESCE(json_extract(json, '$.groupV2Change.details[' || ? || '].aci'), json_extract(json, '$.groupV2Change.details[' || ? || '].uuid')) AS uuid," "json_extract(json, '$.groupV2Change.details[' || ? || '].newTitle') AS title," "json_extract(json, '$.groupV2Change.details[' || ? || '].description') AS description," "json_extract(json, '$.groupV2Change.details[' || ? || '].avatar') AS avatar," "json_extract(json, '$.groupV2Change.details[' || ? || '].removed') AS removed" " FROM messages WHERE rowid = ?", {i, i, i, i, i, i, i, rowid}, &res)) continue; std::string changetype = res("type"); if (changetype == "title") { DecryptedString newtitle; newtitle.addField<1>(res("title")); groupchange.addField<10>(newtitle); addchange = true; //Logger::message("new title '", title, "'"); } else if (changetype == "description") { //bool removed [[maybe_unused]] = res.valueAsInt(0, "removed"); DecryptedString newdescription; newdescription.addField<1>(res("description")); groupchange.addField<20>(newdescription); addchange = true; //Logger::message("new description: '", description, "' (", removed, ")"); } else if (changetype == "avatar") { //bool removed [[maybe_unused]] = res.valueAsInt(0, "removed"); DecryptedString newavatar; newavatar.addField<1>("new_avatar"); groupchange.addField<11>(newavatar); addchange = true; //Logger::message("new avatar (", removed, ")"); } else if (changetype == "member-add") { std::string uuid = res("uuid"); if (static_cast(uuid.size()) != STRLEN("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") || static_cast(source_uuid.size()) != STRLEN("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")) continue; unsigned int uuid_bytes_size = (STRLEN("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") - STRLEN("----")) / 2; std::unique_ptr uuid_bytes(new unsigned char[uuid_bytes_size]); bepaald::hexStringToBytes(source_uuid, uuid_bytes.get(), uuid_bytes_size); groupchange.addField<1>(std::make_pair(uuid_bytes.get(), uuid_bytes_size)); DecryptedMember newmember; uuid_bytes.reset(new unsigned char[uuid_bytes_size]); bepaald::hexStringToBytes(uuid, uuid_bytes.get(), uuid_bytes_size); newmember.addField<1>(std::make_pair(uuid_bytes.get(), uuid_bytes_size)); groupchange.addField<3>(newmember); addchange = true; //Logger::message("member add: ", uuid); } else if (changetype == "member-remove") { std::string uuid = res("uuid"); if (uuid == source_uuid) // xxx left the group groupv2type |= Types::GROUP_QUIT_BIT; //else // xxx removed yyy if (static_cast(uuid.size()) != STRLEN("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")) continue; unsigned int uuid_bytes_size = (STRLEN("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") - STRLEN("----")) / 2; std::unique_ptr uuid_bytes(new unsigned char[uuid_bytes_size]); bepaald::hexStringToBytes(uuid, uuid_bytes.get(), uuid_bytes_size); groupchange.addField<4>(std::make_pair(uuid_bytes.get(), uuid_bytes_size)); addchange = true; //Logger::message("member remove: ", uuid); } else if (changetype == "create") { if (static_cast(source_uuid.size()) != STRLEN("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")) continue; // set source = source_uuid unsigned int uuid_bytes_size = (STRLEN("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") - STRLEN("----")) / 2; std::unique_ptr uuid_bytes(new unsigned char[uuid_bytes_size]); bepaald::hexStringToBytes(source_uuid, uuid_bytes.get(), uuid_bytes_size); groupchange.addField<1>(std::make_pair(uuid_bytes.get(), uuid_bytes_size)); // set new member = also source_uuid DecryptedMember newmember; newmember.addField<1>(std::make_pair(uuid_bytes.get(), uuid_bytes_size)); groupchange.addField<3>(newmember); // explicitly set revision 0 GroupContextV2 groupctx; groupctx.addField<2>(0); groupv2ctx.addField<1>(groupctx); addchange = true; //Logger::message("member add: ", uuid); } else { Logger::warnOnce("Unsupported message type 'group-v2-change:" + changetype + "'. Skipping... (this warning will be shown only once)"); continue; } //res.prettyPrint(d_truncate); } if (addchange) { groupv2ctx.addField<2>(groupchange); std::pair groupchange_data(groupv2ctx.data(), groupv2ctx.size()); std::string groupchange_data_b64 = Base64::bytesToBase64String(groupchange_data); // add message to database // if (d_database.containsTable("sms")) // not going through the trouble // else // { if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) { if (!insertRow(d_mms_table, {{"thread_id", thread_id}, {d_mms_date_sent, date}, {"date_received", date}, {"body", groupchange_data_b64}, {d_mms_type, groupv2type}, {d_mms_recipient_id, address}, {"m_type", incoming ? 132 : 128}, {"read", 1}})) // hardcoded to 1 in Signal Android { Logger::error("Inserting verified-change into mms"); return false; } } else { //newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so //we try to get the first free date_sent long long int freedate = getFreeDateForMessage(date, thread_id, Types::isOutgoing(groupv2type) ? d_selfid : address); if (freedate == -1) { Logger::error("Getting free date for inserting verified-change message into mms"); return false; } if (date != freedate) (*adjusted_timestamps)[date] = freedate; //std::cout << "ADDING NEW GROUPV2 MESSAGE AT DATE: " << bepaald::toDateString(freedate / 1000, "%Y-%m-%d %H:%M:%S") << std::endl; std::any newmms_id; if (!insertRow(d_mms_table, {{"thread_id", thread_id}, {d_mms_date_sent, freedate}, {"date_received", freedate}, {"body", groupchange_data_b64}, {d_mms_type, groupv2type}, {d_mms_recipient_id, incoming ? address : d_selfid}, {"to_recipient_id", incoming ? d_selfid : address}, {"m_type", incoming ? 132 : 128}, {"read", 1}}, "_id", &newmms_id)) // hardcoded to 1 in Signal Android { Logger::error("Inserting verified-change into mms"); return false; } } } return true; } signalbackup-tools-20250313-1/signalbackup/handledtgroupv1migration.cc000066400000000000000000000216751476450434500257340ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::handleDTGroupV1Migration(SqliteDB const &ddb, long long int rowid, long long int thread_id, long long int timestamp, long long int address, std::map *recipientmap, bool createcontacts, std::string const &databasedir, bool create_valid_contacts, bool *warned_createcontacts) { // get a list of dropped members (I _think_ these are not recipient uuid's but conversationUuid's...) std::string dropped_members; SqliteDB::QueryResults results_droppedmembers; if (ddb.exec("SELECT value AS droppedmember FROM messages, json_each(messages.json, '$.groupMigration.droppedMemberIds') WHERE messages.rowid = ?", rowid, &results_droppedmembers)) { for (unsigned int dm = 0; dm < results_droppedmembers.rows(); ++dm) { std::string convuuid = results_droppedmembers.valueAsString(dm, "droppedmember"); SqliteDB::QueryResults dm_id; if (!ddb.exec("SELECT COALESCE(" + d_dt_c_uuid + ",e164) AS rid FROM conversations WHERE id IS ?", convuuid, &dm_id) || dm_id.rows() != 1) continue; long long int recid = getRecipientIdFromUuidMapped(dm_id.valueAsString(0, "rid"), recipientmap, createcontacts); if (recid < 0) recid = getRecipientIdFromPhoneMapped(dm_id.valueAsString(0, "rid"), recipientmap, createcontacts); if (recid < 0) { // let's just check the uuid's aren't recipient uuid's to make sure // this can go when we know it's working SqliteDB::QueryResults test_results; if (ddb.exec("SELECT " + d_dt_c_uuid + " FROM conversations WHERE " + d_dt_c_uuid + " IS ?", dm_id.valueAsString(0, "rid"), &test_results)) if (test_results.rows()) Logger::message(" *** NOTE FOR DEV: id was not found as conversationId but does appear as recipientUuid (droppedMembers) ***"); if (createcontacts) recid = dtCreateRecipient(ddb, dm_id.valueAsString(0, "rid"), dm_id.valueAsString(0, "rid"), std::string(), databasedir, recipientmap, create_valid_contacts, warned_createcontacts); if (recid < 0) continue; } dropped_members += (dropped_members.empty() ? bepaald::toString(recid) : ("," + bepaald::toString(recid))); } } // get a list of invited members // // invited members looks like this: // invitedMembers = [{"addedByUserId":"2e2axxxx-xxxx-xxxx-xxxx-xxxxxxxxxxbe","conversationId":"d608xxxx-xxxx-xxxx-xxxx-xxxxxxxxxx6a","timestamp":1614770366146,"role":2},{...] // or this: // invitedMembers = [{"addedByUserId":"000bxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxe6","uuid":"40d2xxxx-xxxx-xxxx-xxxx-xxxxxxxxxx71","timestamp":1651801628714,"role":2}] // // I'm assuming both are conversationUuid, but uuid might actually be recipients' uuid directly? // // SELECT json_extract(value, '$.conversationId'), json_extract(value, '$.uuid') FROM messages, json_each(messages.json, '$.groupMigration.invitedMembers') AS TREE WHERE messages.rowid = ?; std::string invited_members; SqliteDB::QueryResults results_invitedmembers; //if (ddb.exec("SELECT json_extract(value, '$.conversationId') AS conversationId, json_extract(value, '$.uuid') AS uuid" // " FROM messages, json_each(messages.json, '$.groupMigration.invitedMembers') AS TREE WHERE messages.rowid = ?", rowid, &results_invitedmembers)) if (ddb.exec("SELECT COALESCE(json_extract(value, '$.conversationId'), COALESCE(json_extract(value, '$.aci'), json_extract(value, '$.uuid'))) AS convuuid, " "json_extract(value, '$.conversationId') IS NULL AS is_uuid " // just to remember if this was gotten from "conversationId' or 'uuid' for testing "FROM messages, json_each(messages.json, '$.groupMigration.invitedMembers') AS TREE WHERE messages.rowid = ?", rowid, &results_invitedmembers)) { for (unsigned int im = 0; im < results_invitedmembers.rows(); ++im) { std::string convuuid = results_invitedmembers.valueAsString(im, "convuuid"); if (!convuuid.empty()) { SqliteDB::QueryResults im_id; if (!ddb.exec("SELECT COALESCE(" + d_dt_c_uuid + ", e164) AS rid FROM conversations WHERE id IS ?", convuuid, &im_id) || im_id.rows() != 1) continue; long long int recid = getRecipientIdFromUuidMapped(im_id.valueAsString(0, "rid"), recipientmap, createcontacts); if (recid < 0) recid = getRecipientIdFromPhoneMapped(im_id.valueAsString(0, "rid"), recipientmap, createcontacts); if (recid < 0) { // let's just check the uuid's aren't recipient uuid's to make sure // this can go when we know it's working (and the SELECT can be shortened!) SqliteDB::QueryResults test_results; if (ddb.exec("SELECT " + d_dt_c_uuid + " FROM conversations WHERE " + d_dt_c_uuid + " IS ?", im_id.valueAsString(0, "rid"), &test_results)) if (test_results.rows()) Logger::message(" *** NOTE FOR DEV: id was not found as conversationId but does appear as recipientUuid (invitedMembers, uuid: ", im_id.valueAsString(0, "is_uuid"), ") ***"); if (createcontacts) recid = dtCreateRecipient(ddb, im_id.valueAsString(0, "rid"), im_id.valueAsString(0, "rid"), std::string(), databasedir, recipientmap, create_valid_contacts, warned_createcontacts); if (recid < 0) continue; } invited_members += (invited_members.empty() ? bepaald::toString(recid) : ("," + bepaald::toString(recid))); } } } std::string body; if (!invited_members.empty() || !dropped_members.empty()) body = invited_members + '|' + dropped_members; if (d_database.containsTable("sms")) { if (!insertRow("sms", {{"thread_id", thread_id}, {"date_sent", timestamp}, {d_sms_date_received, timestamp}, {"type", Types::GV1_MIGRATION_TYPE}, {d_sms_recipient_id, address}, {"body", body}, {"read", 1}})) { Logger::error("Inserting group-v1-migration into sms"); return false; } } else { if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) { if (!insertRow(d_mms_table, {{"thread_id", thread_id}, {d_mms_date_sent, timestamp}, {"date_received", timestamp}, {d_mms_type, Types::GV1_MIGRATION_TYPE}, {d_mms_recipient_id, address}, {"body", body}, {d_mms_recipient_device_id, 1}, {"read", 1}})) { Logger::error("Inserting group-v1-migration into mms"); return false; } } else { // newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so // we try to get the first free date_sent long long int freedate = getFreeDateForMessage(timestamp, thread_id, Types::isOutgoing(Types::GV1_MIGRATION_TYPE) ? d_selfid : address); if (freedate == -1) { Logger::error("Getting free date for inserting group-v1-migration message into mms"); return false; } if (!insertRow(d_mms_table, {{"thread_id", thread_id}, {d_mms_date_sent, freedate}, {"date_received", freedate}, {d_mms_type, Types::GV1_MIGRATION_TYPE}, {d_mms_recipient_id, Types::isOutgoing(Types::GV1_MIGRATION_TYPE) ? d_selfid : address}, {"to_recipient_id", Types::isOutgoing(Types::GV1_MIGRATION_TYPE) ? address : d_selfid}, {"body", body}, {d_mms_recipient_device_id, 1}, {"read", 1}})) { Logger::error("Inserting group-v1-migration into mms"); return false; } } } return true; } signalbackup-tools-20250313-1/signalbackup/handlewamessage.cc000066400000000000000000000105021476450434500240260ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ // DEPRECATED // #include "signalbackup.ih" // bool SignalBackup::handleWAMessage(long long int thread_id, long long int time, std::string const &chatname, std::string const &author, std::string const &message, // std::string const &selfid, bool isgroup, std::map const &name_to_recipientid) // { // //std::cout << "Dealing with message:" << std::endl; // //std::cout << "Time: '" << time << "'" << std::endl; // //std::cout << "Author: '" << author << "'" << std::endl; // //std::cout << "Message: '" << message << "'" << std::endl; // bool mark_as_read = false; // bool outgoing = author == selfid || (!isgroup && author != chatname); // std::string address; // // author's address not in map yet // std::string addresstofind = (isgroup && outgoing) ? chatname : author; // if (name_to_recipientid.find(addresstofind) == name_to_recipientid.end()) // { // std::cout << "Error finding recipient_id for " << addresstofind << std::endl; // return false; // } // address = name_to_recipientid.at(addresstofind); // // maybe make (secure_message_bit|pushmessage) an option? // long long int type = (outgoing ? Types::BASE_SENT_TYPE : Types::BASE_INBOX_TYPE) | Types::SECURE_MESSAGE_BIT | Types::PUSH_MESSAGE_BIT; // // delivery_receipt_count // // 0 for incoming // // if outgoing -> 1 for 1-on-1 chats // // -> group size for group chats -> also append to 'group_receipts' table (_id|mms_id|address|status|timestamp|unidentified) // long long int delivery_receipt_count = outgoing ? 1 : 0; // if (outgoing && isgroup) // { // delivery_receipt_count = name_to_recipientid.size() - 1; // delivered to all members, -1 because map also contains chat name // if (name_to_recipientid.find(selfid) != name_to_recipientid.end()) // likely, map also contains self // --delivery_receipt_count; // } // long long int read_receipt_count = (mark_as_read && outgoing ? 1 : 0); // if (mark_as_read && outgoing && isgroup) // { // read_receipt_count = name_to_recipientid.size() - 1; // delivered to all members, -1 because map also contains chat name // if (name_to_recipientid.find(selfid) != name_to_recipientid.end()) // likely, map also contains self // --read_receipt_count; // } // // TODO: // // scan message for mentions, edit body, update mentions-table // // read_receipt_count? -> 0 default, 1 on option && outgoing // std::string statement = "INSERT INTO "; // if (isgroup && author == selfid) // statement += "mms (thread_id, " + d_mms_recipient_id + ", " + d_mms_date_sent + ", date_received, date_server, read, " + d_mms_type; // else // statement += "sms (thread_id, " + d_sms_recipient_id + ", " + d_sms_date_received + ", date_sent, date_server, read, type"; // statement += ", body, delivery_receipt_count, read_receipt_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; // if (!d_database.exec(statement, {thread_id, address, time, time, outgoing ? -1ll : time, 1ll, type, message, delivery_receipt_count, read_receipt_count})) // return false; // // update delivery_receipt table // if (outgoing && isgroup) // { // long long int last_insert_id = d_database.lastInsertRowid(); // for (auto const &a : name_to_recipientid) // { // if (a.first == selfid || a.first == chatname) // continue; // if (!d_database.exec("INSERT INTO group_receipts (mms_id, address, timestamp, status) VALUES (?, ?, ?, ?)", {last_insert_id, a.second, time, (mark_as_read ? 2ll : 1ll)})) // return false; // } // } // return true; // } signalbackup-tools-20250313-1/signalbackup/htmlescapestring.cc000066400000000000000000000065561476450434500242700ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" std::string SignalBackup::HTMLescapeString(std::string const &body) const { std::string result(body); HTMLescapeString(&result); return result; } void SignalBackup::HTMLescapeString(std::string *body, std::set const *const positions_excluded_from_escape) const { // escape special html chars second, so the span's added by emojifinder (next) aren't escaped int positions_added = 0; for (unsigned int i = 0; i < body->length(); ++i) { //std::cout << "I, POSITIONS_ADDED: " << i << "," << positions_added << std::endl; switch ((*body)[i]) { case '&': if (!positions_excluded_from_escape || //!positions_excluded_from_escape->contains(i - positions_added) !bepaald::contains(positions_excluded_from_escape, (i - positions_added))) { body->replace(i, 1, "&"); positions_added += STRLEN("&") - 1; i += STRLEN("&") - 1; //changed = true; } break; case '<': if (!positions_excluded_from_escape || //!positions_excluded_from_escape->contains(i - positions_added !bepaald::contains(positions_excluded_from_escape, (i - positions_added))) { body->replace(i, 1, "<"); positions_added += STRLEN("<") - 1; i += STRLEN("<") - 1; //changed = true; } break; case '>': if (!positions_excluded_from_escape || //!positions_excluded_from_escape->contains(i - positions_added)) !bepaald::contains(positions_excluded_from_escape, (i - positions_added))) { body->replace(i, 1, ">"); i += STRLEN(">") - 1; positions_added += STRLEN(">") - 1; //changed = true; } break; case '"': if (!positions_excluded_from_escape || //!positions_excluded_from_escape->contains(i - positions_added)) !bepaald::contains(positions_excluded_from_escape, (i - positions_added))) { body->replace(i, 1, """); i += STRLEN(""") - 1; positions_added += STRLEN(""") - 1; //changed = true; } break; case '\'': if (!positions_excluded_from_escape || //!positions_excluded_from_escape->contains(i - positions_added)) !bepaald::contains(positions_excluded_from_escape, (i - positions_added))) { body->replace(i, 1, "'"); i += STRLEN("'") - 1; positions_added += STRLEN("'") - 1; //changed = true; } break; } } } signalbackup-tools-20250313-1/signalbackup/htmlescapeurl.cc000066400000000000000000000031161476450434500235510ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" std::string SignalBackup::HTMLescapeUrl(std::string const &in) const { std::string result(in); HTMLescapeUrl(&result); return result; } void SignalBackup::HTMLescapeUrl(std::string *in) const { for (unsigned int i = 0; i < in->size(); ++i) { if (!((*in)[i] >= 'A' && (*in)[i] <= 'Z') && // A-Z !((*in)[i] >= 'a' && (*in)[i] <= 'z') && // a-z !((*in)[i] >= '0' && (*in)[i] <= '9') && // 0-9 !((*in)[i] >= '\'' && (*in)[i] <= '*') && // ' ( ) * (*in)[i] != '!' && (*in)[i] != '-' && (*in)[i] != '.' && (*in)[i] != '_' && (*in)[i] != '~') { // it is not an allowed character, escape it std::string escape = "%" + bepaald::toHexString(static_cast((*in)[i]) & 0xff); in->replace(i, 1, escape); i += escape.size() - 1; } } } signalbackup-tools-20250313-1/signalbackup/htmlgetemojipos.cc000066400000000000000000000045121476450434500241140ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" std::vector> SignalBackup::HTMLgetEmojiPos(std::string const &str) const { // for (char c : str) // { // if (std::isprint(c)) // std::cout << c; // else // std::cout << std::hex << static_cast(c & 0xff); // } // std::cout << "" << std::endl; std::vector> results; for (unsigned int i = 0; i < std::max(static_cast(str.size()), s_emoji_min_size) - s_emoji_min_size; ++i) { //std::cout << "Checking byte " << std::dec << i << ": " << std::hex << static_cast(str[i] & 0xff) << std::endl; if (bepaald::contains(s_emoji_first_bytes, str[i])) { for (char const *const emoji_string : s_emoji_unicode_list) { int emoji_size = std::strlen(emoji_string); if (i <= (str.size() - emoji_size) && std::strncmp(str.data() + i, emoji_string, emoji_size) == 0) { // std::cout << "matched emoji: "; // for (unsigned int c = 0; c < emoji_size; ++c) // std::cout << std::hex << static_cast(emoji_string[c] & 0xff); // std::cout << "" << std::endl; results.emplace_back(std::make_pair(i, emoji_size)); i += emoji_size - 1; // minus one because ++i in for loop //str->insert(i, "<*>"); //str->insert(i + 3 + emoji_size, "<*>"); //i += 3 + emoji_size + 3; //std::cout << *str << std::endl; } } } //else if ((*str)[i] != ' ') // spaces don't count // all_emoji = false; } return results; } signalbackup-tools-20250313-1/signalbackup/htmlicontypes.h000066400000000000000000000017751476450434500234560ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef HTMLICONTYPES_H_ #define HTMLICONTYPES_H_ enum class IconType { NONE, TIMER_UPDATE, TIMER_DISABLE, MEGAPHONE, MEMBERS, MEMBER_APPROVED, MEMBER_REJECTED, MEMBER_ADD, MEMBER_REMOVE, PENCIL, AVATAR_UPDATE, THREAD, //AVATAR_REMOVE, }; #endif signalbackup-tools-20250313-1/signalbackup/htmllinkify.cc000066400000000000000000000144771476450434500232470ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "msgrange.h" void SignalBackup::HTMLLinkify(std::string const &body, std::vector *ranges) const { bool possible_link = false; long long unsigned int pos = 0; while ((pos = body.find('.', pos)) != std::string::npos) { ++pos; if (pos < body.size() && //((body[pos] >= 'A' && body[pos] <= 'Z') || // this is slightly faster, but likely prevents //(body[pos] >= 'a' && body[pos] <= 'z'))) // some urls from linkifying (with unicode body[pos] >= 'A') // char in TLD) { possible_link = true; break; } } if (!possible_link) [[likely]] return; pos = 0; std::smatch url_match_result; std::string bodycopy(body); while (std::regex_search(bodycopy, url_match_result, s_linkify_pattern)) { //std::cout << "MATCH : " << url_match_result[0] << " (" << url_match_result.size() << " matches total)" // << " : " << pos + url_match_result.position(0) << " " << url_match_result.length(0) << std::endl; // for (const auto &res : url_match_result) // std::cout << (res.str().empty() ? "0" : "1"); // std::cout << std::endl; // get offset+length if string was utf16 long long int match_start = 0; for (unsigned int i = 0; i < pos + url_match_result.position(0); ) { int utf8size = bytesToUtf8CharSize(body, i); match_start += utf16CharSize(body, i); i += utf8size; } //std::cout << "startpos : " << match_start << std::endl; long long int match_length = 0; for (unsigned int i = pos + url_match_result.position(0); i < pos + url_match_result.position(0) + url_match_result.length(0); ) { int utf8size = bytesToUtf8CharSize(body, i); match_length += utf16CharSize(body, i); i += utf8size; } //std::cout << "match length : " << match_length << std::endl; // url_match_result.length(2) > 0 -> EMAIL // url_match_result.length(3) > 0 -> URL_WITH_PROTOCOL // url_match_result.length(9/10) > 0 -> URL_NO_PROTOCOL std::string match_link(url_match_result.str(0)); /* This really shouldn't happen I think, but I have a link with multiple # signs in my backup. This is not valid, and causes the HTML to not be valid, so we escape it. Other such issues may also appear in the future */ size_t escapepos = 0; if ((escapepos = match_link.find('#')) != std::string::npos) [[unlikely]] { size_t start_pos = escapepos; while ((start_pos = match_link.find('#', start_pos + 1)) != std::string::npos) { match_link.replace(start_pos, 1, "%23"); start_pos += STRLEN("%23"); } } if (url_match_result.length(3) > 0) // -> URL_WITH_PROTOCOL ranges->emplace_back(Range{match_start, //static_cast(pos) + url_match_result.position(0), match_length, //url_match_result.length(0), "", "", "", true}); else if (url_match_result.length(9) > 0) // -> URL_WITHOUT_PROTOCOL /* Here, we add the protocol manually (guessing it to be https), without a protocol, the 'link' will be interpreted as a location in the current domain (file://HTMLDIR/Conversation/Page.html/www.example.com), a workaround, using just "//" as the protocol does signal that the link is at a new root, but automatically uses the current protocol (which is file://, which is not correct. */ ranges->emplace_back(Range{match_start, //static_cast(pos) + url_match_result.position(0), match_length, //url_match_result.length(0), "", "", "", true}); else if (url_match_result.length(2) > 0) // -> EMAIL ranges->emplace_back(Range{match_start, //static_cast(pos) + url_match_result.position(0), match_length, //url_match_result.length(0), "", "", "", true}); pos += url_match_result.position(0) + url_match_result.length(0); bodycopy = url_match_result.suffix(); } } /* foo://example.com:8042/over/there?name=ferret#nose \_/ \______________/\_________/ \_________/ \__/ | | | | | scheme authority path query fragment - The scheme may not contain a hash sign (only ALPHA *( ALPHA / DIGIT / "+" / "-" / ".") - The autority may not contain a hash. It is terminated by the next slash ("/"), question mark ("?"), or number sign ("#"). - The path consists of a sequence of path segments separated by a slash ("/") character. The path segments in turn can only consist of pchars. It will also be terminated by the first question mark ("?") or number sign ("#"), or by the end of the URI. - The query part (indicated by the first "?") may only consist of pchar, "/" or "?" and will be terminated by a number sign ("#") character or by the end of the URI. - The fragment is indicated by the presence of a number sign ("#")' and also consists only of pchar, "/" or "?". It is terminated by the end of the URI. */ signalbackup-tools-20250313-1/signalbackup/htmlmessageinfo.h000066400000000000000000000033211476450434500237260ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.h" #include "htmlicontypes.h" struct HTMLMessageInfo { bool only_emoji; bool is_deleted; bool is_viewonce; bool isgroup; bool incoming; bool nobackground; bool hasquote; bool quote_missing; bool orig_filename; bool overwrite; bool append; bool story_reply; long long int type; long long int expires_in; long long int msg_id; long long int msg_recipient_id; long long int original_message_id; unsigned int idx; SqliteDB::QueryResults *messages; SqliteDB::QueryResults *quote_attachment_results; SqliteDB::QueryResults *attachment_results; SqliteDB::QueryResults *reaction_results; SqliteDB::QueryResults *edit_revisions; std::string body; std::string quote_body; std::string readable_date; std::string directory; std::string threaddir; std::string filename; std::string link_preview_url; std::string link_preview_title; std::string link_preview_description; std::string shared_contacts; IconType icon; }; signalbackup-tools-20250313-1/signalbackup/htmlpreplinkpreviewdescription.cc000066400000000000000000000025411476450434500272610ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../common_be.h" std::string SignalBackup::HTMLprepLinkPreviewDescription(std::string const &in) const { // link preview can contain html, this is problematic for the export + // in the app the tags are stripped, and underscores are replaced with spaces // for some reason std::string cleaned = in; while (cleaned.find("<") != std::string::npos) { auto startpos = cleaned.find("<"); auto endpos = cleaned.find(">") + 1; if (endpos != std::string::npos) cleaned.erase(startpos, endpos - startpos); } bepaald::replaceAll(&cleaned, "_", " "); return cleaned; } signalbackup-tools-20250313-1/signalbackup/htmlprepmsgbody.cc000066400000000000000000000141771476450434500241320ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "msgrange.h" bool SignalBackup::HTMLprepMsgBody(std::string *body, std::vector> const &mentions, std::map *recipient_info, bool incoming, std::pair, size_t> const &brdata, bool linkify, bool isquote) const { if (body->empty()) return false; std::vector ranges; // First, do mentions for (auto const &m : mentions) { // m0 : recipient_id, m1: start, m2: length std::string author = getRecipientInfoFromMap(recipient_info, std::get<0>(m)).display_name; if (!author.empty()) { ranges.emplace_back(Range{std::get<1>(m), std::get<2>(m), (isquote ? "" : ""), "@" + author, (isquote ? "" : ""), true}); } } // now do other stylings? bool hasstyledlinks = false; if (brdata.second) { BodyRanges brsproto(brdata); //brsproto.print(); auto brs = brsproto.getField<1>(); for (auto const &br : brs) { int start = br.getField<1>().value_or(0); int length = br.getField<2>().value_or(0); if (!length) // maybe legal? no length == rest of string? (like no start is beg) continue; // get mention std::string mentionuuid = br.getField<3>().value_or(std::string()); if (!mentionuuid.empty()) { long long int authorid = getRecipientIdFromUuidMapped(mentionuuid, nullptr); std::string author = getRecipientInfoFromMap(recipient_info, authorid).display_name; if (!author.empty()) ranges.emplace_back(Range{start, length, (isquote ? "" : ""), "@" + author, (isquote ? "" : ""), true}); } // get style int style = br.getField<4>().value_or(-1); // get link std::string link = br.getField<5>().value_or(std::string()); if (style > -1) { //std::cout << "Adding style to range [" << start << "-" << start+length << "] : " << style << std::endl; switch (style) { case 0: // BOLD { ranges.emplace_back(Range{start, length, "", "", "", false}); break; } case 1: // ITALIC { ranges.emplace_back(Range{start, length, "", "", "", false}); break; } case 2: // SPOILER { ranges.emplace_back(Range{start, length, "", "", "", true}); break; } case 3: // STRIKETHROUGH { ranges.emplace_back(Range{start, length, "", "", "", false}); // or ? or ? break; } case 4: // MONOSPACE { ranges.emplace_back(Range{start, length, "", "", "", false}); break; } default: { Logger::warning("Unsupported range-style: ", style); } } } if (!link.empty()) { //std::cout << "Adding link to range [" << start << "-" << start+length << "] '" << link << "'" << std::endl; ranges.emplace_back(Range{start, length, "", "", "", true}); hasstyledlinks = true; } } } // scan for links (somehow skipping the styled links above!), then // ranges.emplace_back(Range{start, length, ", "", "", true}); if (linkify && !hasstyledlinks) HTMLLinkify(*body, &ranges); std::set positions_excluded_from_escape; applyRanges(body, &ranges, &positions_excluded_from_escape); HTMLescapeString(body, &positions_excluded_from_escape); // now do the emoji std::vector> emoji_pos = HTMLgetEmojiPos(*body); // check if body is only emoji bool all_emoji = true; if (emoji_pos.size() > 5) all_emoji = false; // could technically still be only emoji, but it gets a bubble in html else { for (unsigned int i = 0, posidx = 0; i < body->size(); ++i) { if (posidx >= emoji_pos.size() || i != emoji_pos[posidx].first) { if ((*body)[i] == ' ') // spaces dont count continue; all_emoji = false; break; } else // body[i] == pos[posidx].first i += emoji_pos[posidx++].second - 1; // minus 1 for the ++i in loop } } // surround emoji with span std::string pre = ""; std::string post = ""; int moved = 0; for (auto const &p : emoji_pos) { if (!bepaald::contains(positions_excluded_from_escape, p.first)) [[likely]] { body->insert(p.first + moved, pre); body->insert(p.first + p.second + pre.size() + moved, post); moved += pre.size() + post.size(); } } return all_emoji; // if (changed) // { // std::cout << "ORIG: " << orig << std::endl; // std::cout << "NEW : " << *body << std::endl; // } } signalbackup-tools-20250313-1/signalbackup/htmlwrite.cc000066400000000000000000003725551476450434500227400ustar00rootroot00000000000000/* * IMPORTANT LICENSE NOTICE * * The HTML and CSS produced by these functions is (heavily) modified from the * template found in https://github.com/GjjvdBurg/signal2html. * * To adhere to the license of that project, the code in this file can be * considered to fall under the same MIT license. The full license text from * the original project is copied below. */ /* Copyright 2020-2021, signal2html contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* Many thanks to Gertjan van den Burg (https://github.com/GjjvdBurg) for his original project (used with permission) without which this function would not have come together so quickly (if at all). */ #include "signalbackup.ih" bool SignalBackup::HTMLwriteStart(std::ofstream &file, long long int thread_recipient_id, std::string const &directory, std::string const &threaddir, bool isgroup, bool isnotetoself, bool isreleasechannel, std::set const &recipient_ids, std::map *recipient_info, std::map *written_avatars, bool overwrite, bool append, bool light, bool themeswitch, bool searchpage, bool exportdetails, bool pagemenu) const { std::vector groupmembers; if (isgroup) { SqliteDB::QueryResults results; d_database.exec("SELECT group_id from recipient WHERE _id IS ?", thread_recipient_id, &results); if (results.rows() == 1) getGroupMembersOld(&groupmembers, results.valueAsString(0, "group_id")); } GroupInfo groupinfo; if (isgroup) getGroupInfo(thread_recipient_id, &groupinfo); // sort group members by admin and name std::sort(groupmembers.begin(), groupmembers.end(), [this, &groupinfo, &recipient_info](auto left, auto right) { return (bepaald::contains(groupinfo.admin_ids, left) && !bepaald::contains(groupinfo.admin_ids, right)) || ((bepaald::contains(groupinfo.admin_ids, left) == bepaald::contains(groupinfo.admin_ids, right)) && (getRecipientInfoFromMap(recipient_info, left).display_name < getRecipientInfoFromMap(recipient_info, right).display_name));}); std::string thread_avatar = bepaald::contains(written_avatars, thread_recipient_id) ? (*written_avatars)[thread_recipient_id] : ((*written_avatars)[thread_recipient_id] = HTMLwriteAvatar(thread_recipient_id, directory, threaddir, overwrite, append)); std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); //file << "\n"; std::string title = (isnotetoself ? "Note to Self" : getRecipientInfoFromMap(recipient_info, thread_recipient_id).display_name); HTMLescapeString(&title); bool ismuted = getRecipientInfoFromMap(recipient_info, thread_recipient_id).mute_until == 0x7FFFFFFFFFFFFFFF; bool isblocked = getRecipientInfoFromMap(recipient_info, thread_recipient_id).blocked; file << R"( )" << title << R"( )"; if (themeswitch) { file << R"( )"; } // set expiration timer string std::string exptimer = "Off"; std::string exptimer_short; long long int expiration_timer = isgroup ? groupinfo.expiration_timer : getRecipientInfoFromMap(recipient_info, thread_recipient_id).message_expiration_time; if (expiration_timer) { if (expiration_timer < 60) // less than full minute { exptimer = bepaald::toString(expiration_timer) + " second" + (expiration_timer == 1 ? "" : "s"); exptimer_short = bepaald::toString(expiration_timer) + "s"; } else if (expiration_timer < 60 * 60) // less than full hour { exptimer = bepaald::toString(expiration_timer / 60) + " minute" + (expiration_timer / 60 == 1 ? "" : "s"); exptimer_short = bepaald::toString(expiration_timer / 60) + "m"; } else if (expiration_timer < 24 * 60 * 60) // less than full day { exptimer = bepaald::toString(expiration_timer / (60 * 60)) + " hour" + (expiration_timer / (60 * 60) == 1 ? "" : "s"); exptimer_short = bepaald::toString(expiration_timer / (60 * 60)) + "h"; } else if (expiration_timer < 7 * 24 * 60 * 60) // less than full week { exptimer = bepaald::toString(expiration_timer / (24 * 60 * 60)) + " day" + (expiration_timer / (24 * 60 * 60) == 1 ? "" : "s"); exptimer_short = bepaald::toString(expiration_timer / (24 * 60 * 60)) + "d"; } else // show expiration_timer in number of weeks { exptimer = bepaald::toString(expiration_timer / (7 * 24 * 60 * 60)) + " week" + (expiration_timer / (7 * 24 * 60 * 60) == 1 ? "" : "s"); exptimer_short = bepaald::toString(expiration_timer / (7 * 24 * 60 * 60)) + "w"; } } file << R"(
)"; if (thread_avatar.empty() || isnotetoself) { if (isgroup) { file << R"(
)"; } if (isnotetoself) { file << R"(
)"; } if (!isgroup && !isnotetoself) { file << R"(
)" << getRecipientInfoFromMap(recipient_info, thread_recipient_id).initial << R"(
)"; } } else { file << R"( )"; } file << "\n" "
";

  if (isblocked)
    file << "";
  else if (ismuted)
    file << "";

  file << title;

  if (expiration_timer)
    file << "" + exptimer_short + "";
  if (isnotetoself || isreleasechannel)
    file << "";

  file <<
    "
\n"; if (!isnotetoself && getRecipientInfoFromMap(recipient_info, thread_recipient_id).verified) file << "
\n" " verified\n" "
\n"; file << "
\n"; if (isgroup) { file << groupmembers.size() << " member" << (groupmembers.size() != 1 ? "s" : "") << "\n" " \n" " \n"; } else // !isgroup file << (getRecipientInfoFromMap(recipient_info, thread_recipient_id).display_name == getRecipientInfoFromMap(recipient_info, thread_recipient_id).phone ? "" : getRecipientInfoFromMap(recipient_info, thread_recipient_id).phone) << '\n'; file << R"(
)"; return true; } void SignalBackup::HTMLwriteAttachmentDiv(std::ofstream &htmloutput, SqliteDB::QueryResults const &attachment_results, int indent, std::string const &directory, std::string const &threaddir, bool use_original_filenames, bool is_image_preview, bool overwrite, bool append, std::vector const &ignoremediatypes) const { for (unsigned int a = 0; a < attachment_results.rows(); ++a) { // long text body if (attachment_results(a, d_part_ct) == "text/x-signal-plain") continue; long long int rowid = attachment_results.getValueAs(a, "_id"); long long int uniqueid = attachment_results.getValueAs(a, "unique_id"); long long int pending_push = attachment_results.getValueAs(a, d_part_pending); if (pending_push != 0) { htmloutput << std::string(indent, ' ') << "
\n"; htmloutput << std::string(indent, ' ') << "
\n"; htmloutput << std::string(indent, ' ') << " (attachment not downloaded)\n"; htmloutput << std::string(indent, ' ') << "
\n"; htmloutput << std::string(indent, ' ') << "
\n"; return; } std::string content_type = attachment_results.valueAsString(a, d_part_ct); std::string original_filename; if (!attachment_results.isNull(a, "file_name") && !attachment_results(a, "file_name").empty()) { original_filename = attachment_results(a, "file_name"); HTMLescapeString(&original_filename); } std::string extension(MimeTypes::getExtension(content_type, "bin")); std::string attachment_filename_on_disk = "Attachment_" + bepaald::toString(rowid) + "_" + bepaald::toString(uniqueid) + "." + extension; if (use_original_filenames) { attachment_filename_on_disk = sanitizeFilename(attachment_results(a, "file_name")); if (attachment_filename_on_disk.empty()) // filename was not set in database or was not impossible { // to sanitize (eg reserved name in windows 'COM1') long long int datum = attachment_results.valueAsInt(a, "date_received", -1); std::ostringstream tmp; if (datum != -1) { // get datestring std::time_t epoch = datum / 1000; tmp << std::put_time(std::localtime(&epoch), "signal-%Y-%m-%d-%H%M%S"); } else tmp << "signal"; attachment_filename_on_disk = tmp.str() + "." + extension; } if (!makeFilenameUnique(directory + "/" + threaddir + "/media", &attachment_filename_on_disk)) { Logger::error("Getting unique filename for '", directory, "/", threaddir, "/media/", attachment_filename_on_disk, "'"); continue; } } // write the attachment data if (!HTMLwriteAttachment(directory, threaddir, rowid, uniqueid, attachment_filename_on_disk, attachment_results.valueAsInt(a, "date_received", -1), overwrite, append)) continue; if (use_original_filenames) HTMLescapeUrl(&attachment_filename_on_disk); // check if content type must not be handled as media: bool ignoremedia = false; if (bepaald::contains(ignoremediatypes, content_type)) [[unlikely]] ignoremedia = true; htmloutput << std::string(indent, ' ') << "
\n"; if (STRING_STARTS_WITH(content_type, "image/") && !ignoremedia) { htmloutput << std::string(indent, ' ') << "
\n"; htmloutput << std::string(indent, ' ') << " \n"; htmloutput << std::string(indent, ' ') << " \n"; if (attachment_results.hasColumn("caption") && !attachment_results.isNull(a, "caption")) htmloutput << std::string(indent, ' ') << "
" << attachment_results(a, "caption") << "
\n"; htmloutput << std::string(indent, ' ') << "
\n"; } else if ((STRING_STARTS_WITH(content_type, "video/") || STRING_STARTS_WITH(content_type, "audio/")) && !ignoremedia) { htmloutput << std::string(indent, ' ') << "
\n"; htmloutput << std::string(indent, ' ') << " <" << std::string_view(content_type.data(), 5) << " controls>\n"; htmloutput << std::string(indent, ' ') << " \n"; //htmloutput << std::string(indent, ' ') << " Media of type " << content_type << "🠟\n"; htmloutput << std::string(indent, ' ') << " \n"; if (attachment_results.hasColumn("caption") && !attachment_results.isNull(a, "caption")) htmloutput << std::string(indent, ' ') << "
" << attachment_results(a, "caption") << "
\n"; htmloutput << std::string(indent, ' ') << "
\n"; } else { htmloutput << std::string(indent, ' ') << "
" << (extension.size() <= 5 ? extension : "") << "
\n"; if (content_type.empty()) { if (original_filename.empty()) htmloutput << std::string(indent, ' ') << "
[Attachment of unknown type]🠟
\n"; else htmloutput << std::string(indent, ' ') << "
[Attachment '" << original_filename << "']🠟
\n"; // the following does not work, because URIs on file:// are cross-origin // << attachment_filename_on_disk << "\" download=\"" << original_filename << "\">🠟\n"; } else // content-type not empty, but not 'image/', 'audio/' or 'video/', or ignored as media on user-request { if (original_filename.empty()) htmloutput << std::string(indent, ' ') << "
[Attachment of type \"" << content_type << "\"]🠟
\n"; else htmloutput << std::string(indent, ' ') << "
[Attachment '" << original_filename << "']🠟
\n"; } } htmloutput << std::string(indent, ' ') << "
\n"; } } void SignalBackup::HTMLwriteSharedContactDiv(std::ofstream &htmloutput, std::string const &shared_contact, int indent, std::string const &directory, std::string const &threaddir, bool overwrite, bool append) const { if (d_database.getSingleResultAs("SELECT json_array_length(?, '$')", shared_contact, 0) > 0) { std::string contact_name = "Unknown contact"; std::string contact_info; SqliteDB::QueryResults sc; d_database.exec("SELECT " "json_extract('" + shared_contact + "', '$[0].name.displayName') AS display_name, " "IFNULL(json_array_length('" + shared_contact + "', '$[0].phoneNumbers'), 0) AS num_numbers, " "IFNULL(json_array_length('" + shared_contact + "', '$[0].emails'), 0) AS num_emails, " "json_extract('" + shared_contact + "', '$[0].avatar.attachmentId.rowId') AS avatar_rowid, " "json_extract('" + shared_contact + "', '$[0].avatar.attachmentId.uniqueId') AS avatar_uniqueid", &sc ); if (!sc("display_name").empty()) contact_name = sc("display_name"); long long int rowid = sc.valueAsInt(0, "avatar_rowid", -1); long long int uniqueid = sc.valueAsInt(0, "avatar_uniqueid", -1); std::string extension("bin"); if (rowid >= 0 && uniqueid >= 0) { // write the attachment data HTMLwriteAttachment(directory, threaddir, rowid, uniqueid, extension, -1, overwrite, append); } // prefer phone number int phones = sc.valueAsInt(0, "num_numbers", 0); int emails = sc.valueAsInt(0, "num_emails", 0); if (phones > 0) { // prefer 'MOBILE' (-> 'HOME' -> 'WORK' ?) for (int i = 0; i < phones; ++i) { d_database.exec("SELECT " "json_extract('" + shared_contact + "', '$[0].phoneNumbers[" + bepaald::toString(i) + "].number') AS number, " "json_extract('" + shared_contact + "', '$[0].phoneNumbers[" + bepaald::toString(i) + "].type') AS type", &sc); if (sc("type") == "CUSTOM" && contact_info.empty()) contact_info = sc("number"); else if (sc("type") == "WORK" && contact_info.empty()) contact_info = sc("number"); else if (sc("type") == "HOME") contact_info = sc("number"); else if (sc("type") == "MOBILE") { contact_info = sc("number"); break; } } } else if (emails > 0) { // prefer 'HOME' (-> 'WORK' -> 'MOBILE' ?) for (int i = 0; i < emails; ++i) { d_database.exec("SELECT " "json_extract('" + shared_contact + "', '$[0].emails[" + bepaald::toString(i) + "].email') AS email, " "json_extract('" + shared_contact + "', '$[0].emails[" + bepaald::toString(i) + "].type') AS type", &sc); if (sc("type") == "CUSTOM" && contact_info.empty()) contact_info = sc("email"); else if (sc("type") == "OTHER" && contact_info.empty()) contact_info = sc("email"); else if (sc("type") == "WORK") contact_info = sc("email"); else if (sc("type") == "HOME") { contact_info = sc("email"); break; } } } //htmloutput << std::string(indent, ' ') << "
\n"; htmloutput << std::string(indent, ' ') << "
\n"; if (rowid > -1 && uniqueid > -1) { htmloutput << std::string(indent, ' ') << "
\n"; htmloutput << std::string(indent, ' ') << " \n"; htmloutput << std::string(indent, ' ') << " \n"; htmloutput << std::string(indent, ' ') << "
\n"; } else htmloutput << std::string(indent, ' ') << "
\n"; htmloutput << std::string(indent, ' ') << "
\n"; htmloutput << std::string(indent, ' ') << " " << HTMLescapeString(contact_name) << "\n"; htmloutput << std::string(indent, ' ') << "
" << HTMLescapeString(contact_info) << "
\n"; htmloutput << std::string(indent, ' ') << "
\n"; htmloutput << std::string(indent, ' ') << "
\n"; } } void SignalBackup::HTMLwriteMessage(std::ofstream &htmloutput, HTMLMessageInfo const &msg_info, std::map *recipient_info, bool searchpage, bool writereceipts, std::vector const &ignoremediatypes) const { int extraindent = 0; // insert message long long int quote_author_id = bepaald::toNumber(msg_info.messages->valueAsString(msg_info.idx, "quote_author")); htmloutput << " \n"; if (searchpage) // output an anchor to link to in search results htmloutput << " \n"; // for incoming group (normal) message: insert avatar with initial if (msg_info.isgroup && msg_info.incoming && !msg_info.is_deleted && !Types::isStatusMessage(msg_info.type)) { htmloutput << "
\n"; htmloutput << "
"; if (!getRecipientInfoFromMap(recipient_info, msg_info.msg_recipient_id).hasavatar) { htmloutput << '\n'; htmloutput << " " << getRecipientInfoFromMap(recipient_info, msg_info.msg_recipient_id).initial << "\n"; htmloutput << " "; } htmloutput << "
\n"; extraindent = 2; } // msg bubble htmloutput << std::string(extraindent, ' ') << "
\n"; else htmloutput << "msg-" << (msg_info.incoming ? "incoming" : "outgoing") << (!msg_info.incoming ? " msg-sender-" + bepaald::toString(msg_info.msg_recipient_id) : "") << (msg_info.nobackground ? " no-bg-bubble" : "") << (msg_info.is_viewonce ? " msg-viewonce" : "") << ((msg_info.is_deleted && !msg_info.is_viewonce) ? " deleted-msg" : "") << (msg_info.reaction_results->rows() ? " msg-with-reaction" : "")<< "\">\n"; // for incoming group (normal) message: Senders name before message content if (msg_info.isgroup && msg_info.incoming && !msg_info.is_deleted && !Types::isStatusMessage(msg_info.type)) htmloutput << std::string(extraindent, ' ') << " " << HTMLescapeString(getRecipientInfoFromMap(recipient_info, msg_info.msg_recipient_id).display_name) << "\n"; // for incoming story reply message: 'Reacted to your story' before message content if (msg_info.story_reply && msg_info.incoming && !msg_info.is_deleted && !Types::isStatusMessage(msg_info.type)) htmloutput << std::string(extraindent, ' ') << " Reacted to your story\n"; // insert quote if (msg_info.hasquote) { htmloutput << std::string(extraindent, ' ') << "
\n"; // quote message htmloutput << std::string(extraindent, ' ') << "
\n"; htmloutput << std::string(extraindent, ' ') << " " << HTMLescapeString(getRecipientInfoFromMap(recipient_info, quote_author_id).display_name) << (msg_info.story_reply ? " · Story" : "") << "\n"; if (!msg_info.quote_body.empty()) htmloutput << std::string(extraindent, ' ') << "
" << msg_info.quote_body << "
\n"; if (msg_info.story_reply && msg_info.quote_missing) htmloutput << std::string(extraindent, ' ') << "
No longer available
\n"; htmloutput << std::string(extraindent, ' ') << "
\n"; // quote attachment if (msg_info.quote_attachment_results->rows()) { htmloutput << std::string(extraindent, ' ') << "
\n"; HTMLwriteAttachmentDiv(htmloutput, *msg_info.quote_attachment_results, 16 + extraindent, msg_info.directory, msg_info.threaddir, msg_info.orig_filename, false, msg_info.overwrite, msg_info.append, ignoremediatypes); htmloutput << "
\n"; } htmloutput << std::string(extraindent, ' ') << "
\n"; } // insert attachment? if (!msg_info.shared_contacts.empty()) [[unlikely]] // if we have an attachment with a shared contact, it's an avatar HTMLwriteSharedContactDiv(htmloutput, msg_info.shared_contacts, 12 + extraindent, msg_info.directory, msg_info.threaddir, msg_info.overwrite, msg_info.append); else if (STRING_STARTS_WITH(msg_info.link_preview_url, "https://signal.link/call/#key=")) [[unlikely]] HTMLwriteCallLinkDiv(htmloutput, 12 + extraindent, msg_info.link_preview_url, msg_info.link_preview_title, msg_info.link_preview_description/*, msg_info.directory, msg_info.threaddir, msg_info.overwrite, msg_info.append*/); else HTMLwriteAttachmentDiv(htmloutput, *msg_info.attachment_results, 12 + extraindent, msg_info.directory, msg_info.threaddir, msg_info.orig_filename, (!msg_info.link_preview_title.empty() || !msg_info.link_preview_description.empty()), msg_info.overwrite, msg_info.append, ignoremediatypes); // insert link_preview data? (if not call link) if ((!msg_info.link_preview_title.empty() || !msg_info.link_preview_description.empty()) && !STRING_STARTS_WITH(msg_info.link_preview_url, "https://signal.link/call/#key=")) { htmloutput << "
\n"; if (!msg_info.link_preview_title.empty()) { htmloutput << "
\n" " " << HTMLescapeString(msg_info.link_preview_title) << "\n" "
\n"; } std::string cleaned_link_preview_description = HTMLprepLinkPreviewDescription(msg_info.link_preview_description); if (!cleaned_link_preview_description.empty()) { htmloutput << "
\n" " " << cleaned_link_preview_description << "\n" "
\n"; } htmloutput << "
\n"; } //insert body if (!msg_info.body.empty()) { htmloutput << std::string(extraindent, ' ') << " \n"; htmloutput << std::string(extraindent, ' ') << "
";
    if (Types::isEndSession(msg_info.type) || Types::isIdentityDefault(msg_info.type)) // info-icon
      htmloutput << "";
    else if (Types::isIdentityUpdate(msg_info.type))
      htmloutput << "";
    else if (Types::isIdentityVerified(msg_info.type))
      htmloutput << "";
    else if (Types::isGroupQuit(msg_info.type))
      htmloutput << "";
    else if (Types::isProfileChange(msg_info.type))
      htmloutput << "";
    else if (Types::isExpirationTimerUpdate(msg_info.type))
    {
      if (msg_info.body.find("disabled disappearing messages") != std::string::npos)
        htmloutput << "";
      else
        htmloutput << "";
    }
    else if (Types::isIncomingCall(msg_info.type))
      htmloutput << "";
    else if (Types::isOutgoingCall(msg_info.type))
      htmloutput << "";
    else if (Types::isMissedCall(msg_info.type))
      htmloutput << "";
    else if (Types::isIncomingVideoCall(msg_info.type))
      htmloutput << "";
    else if (Types::isOutgoingVideoCall(msg_info.type))
      htmloutput << "";
    else if (Types::isMissedVideoCall(msg_info.type))
      htmloutput << "";
    else if (Types::isGroupCall(msg_info.type))
      htmloutput << "";
    else if (Types::isJoined(msg_info.type))
      htmloutput << "";
    else if (Types::isMessageRequestAccepted(msg_info.type))
      htmloutput << "";
    else if (msg_info.type == Types::GV1_MIGRATION_TYPE)
    {
      if (msg_info.icon == IconType::MEMBER_ADD)
        htmloutput << "";
      else if (msg_info.icon == IconType::MEMBER_REMOVE)
        htmloutput << "";
      // dont know, never seen this...
      // else if (msg_info.icon == IconType::MEMBERS)
      //   htmloutput << "";
      else
        htmloutput << "";
    }
    else if (Types::isGroupUpdate(msg_info.type) && !Types::isGroupV2(msg_info.type))
      htmloutput << "";
    else if (Types::isNumberChange(msg_info.type))
      htmloutput << "";

    // group v2 status msgs
    else if (Types::isGroupV2(msg_info.type) && msg_info.icon == IconType::TIMER_UPDATE)
      htmloutput << "";
    else if (Types::isGroupV2(msg_info.type) && msg_info.icon == IconType::TIMER_DISABLE)
      htmloutput << "";
    else if (Types::isGroupV2(msg_info.type) && msg_info.icon == IconType::PENCIL)
      htmloutput << "";
    else if (Types::isGroupV2(msg_info.type) && msg_info.icon == IconType::THREAD)
      htmloutput << "";
    else if (Types::isGroupV2(msg_info.type) && msg_info.icon == IconType::MEGAPHONE)
      htmloutput << "";
    else if (Types::isGroupV2(msg_info.type) && msg_info.icon == IconType::MEMBERS)
      htmloutput << "";
    else if (Types::isGroupV2(msg_info.type) && msg_info.icon == IconType::MEMBER_APPROVED)
      htmloutput << "";
    else if (Types::isGroupV2(msg_info.type) && msg_info.icon == IconType::MEMBER_REJECTED)
      htmloutput << "";
    else if (Types::isGroupV2(msg_info.type) && msg_info.icon == IconType::MEMBER_ADD)
      htmloutput << "";
    else if (Types::isGroupV2(msg_info.type) && msg_info.icon == IconType::MEMBER_REMOVE)
      htmloutput << "";
    else if (Types::isGroupV2(msg_info.type) && msg_info.icon == IconType::AVATAR_UPDATE)
      htmloutput << "";

    // else if (Types::isGroupV2(msg_info.type) && msg_info.icon == IconType::)
    //   htmloutput << "";


    htmloutput << msg_info.body << "
\n"; htmloutput << std::string(extraindent, ' ') << "
\n"; } else if (msg_info.is_viewonce) { htmloutput << "
\n"; if (msg_info.incoming) htmloutput << "
"
                 << (msg_info.is_deleted ? "Viewed" : "View-once media")
                 << "
\n"; else htmloutput << "
Media
\n"; htmloutput << "
\n"; } else if (msg_info.is_deleted) { htmloutput << "
\n"; if (msg_info.incoming) htmloutput << "
This message was deleted.
\n"; else htmloutput << "
You deleted this message.
\n"; htmloutput << "
\n"; } // insert msg-footer (date & checkmarks) htmloutput << std::string(extraindent, ' ') << "
\n"; if (msg_info.original_message_id != -1) // : is_edited = true { htmloutput << std::string(extraindent, ' ') << "
edited"; // add edit revisions... if (msg_info.edit_revisions->rows()) { htmloutput << "
"; for (unsigned int i = 0; i < msg_info.edit_revisions->rows() - 1; ++i) // -1, skip last one: it is current message { if (i == 0) htmloutput << "
Edit history
"; // add earlier revision HTMLwriteRevision(msg_info.edit_revisions->valueAsInt(i, "_id"), htmloutput, msg_info, recipient_info, false, ignoremediatypes); if (i < msg_info.edit_revisions->rows() - 2) htmloutput << "
"; // htmloutput << "body: " // << "
" << msg_info.edit_revisions->valueAsString(i, "body") << "
" << "
" // << "date: " // << bepaald::toDateString(msg_info.edit_revisions->valueAsInt(i, d_mms_date_sent) / 1000, // "%b %d, %Y %H:%M:%S"); // if (i == 0) // htmloutput << "
Edit history
"; // else if (i < msg_info.edit_revisions->rows() - 1) // htmloutput << "
"; } htmloutput << "
\n"; } htmloutput << "
\n"; } htmloutput << std::string(extraindent, ' ') << " " << msg_info.readable_date << "\n"; if (!Types::isStatusMessage(msg_info.type)) { if (msg_info.expires_in > 0) htmloutput << std::string(extraindent, ' ') << "
\n"; if (!msg_info.incoming && !Types::isCallType(msg_info.type) && !msg_info.is_deleted) // && received, read? { htmloutput << std::string(extraindent, ' ') << "
getValueAs(msg_info.idx, d_mms_read_receipts) > 0) htmloutput << "read"; else if (msg_info.messages->getValueAs(msg_info.idx, d_mms_delivery_receipts) > 0) htmloutput << "received"; else // if something? type != failed? -> check for failed before outputting 'checkmarks-' htmloutput << "sent"; htmloutput << "\">\n"; // msg receipt details if (writereceipts && (msg_info.messages->valueAsInt(msg_info.idx, d_mms_delivery_receipts, -1) > 0 || msg_info.messages->valueAsInt(msg_info.idx, d_mms_read_receipts, -1) > 0)) HTMLwriteMsgReceiptInfo(htmloutput, recipient_info, msg_info.msg_id, msg_info.isgroup, msg_info.messages->valueAsInt(msg_info.idx, d_mms_read_receipts, 0), msg_info.messages->valueAsInt(msg_info.idx, d_mms_delivery_receipts, 0), msg_info.messages->valueAsInt(msg_info.idx, "receipt_timestamp", -1), extraindent); htmloutput << std::string(extraindent, ' ') << "
\n"; } } htmloutput << std::string(extraindent, ' ') << "
\n"; // insert reaction if (msg_info.reaction_results->rows()) { htmloutput << std::string(extraindent, ' ') << "
\n"; std::set skip; for (unsigned int r = 0; r < msg_info.reaction_results->rows(); ++r) { std::string emojireaction = msg_info.reaction_results->valueAsString(r, "emoji"); if (bepaald::contains(skip, emojireaction)) continue; skip.insert(emojireaction); // count occurences of this emoji, and set info int count = 0; std::string reaction_info; for (unsigned int r2 = r; r2 < msg_info.reaction_results->rows(); ++r2) if (emojireaction == msg_info.reaction_results->valueAsString(r2, "emoji")) { ++count; reaction_info += (reaction_info.empty() ? "" : "
") + "From: "s + getRecipientInfoFromMap(recipient_info, msg_info.reaction_results->getValueAs(r2, "author_id")).display_name + "
Sent: " + msg_info.reaction_results->valueAsString(r2, "date_sent") + "
Received: " + msg_info.reaction_results->valueAsString(r2, "date_received"); } htmloutput << std::string(extraindent, ' ') << "
" << emojireaction << "" << (count > 1 ? "" + bepaald::toString(count) + "": "") << "
" << reaction_info << "
\n"; } htmloutput << std::string(extraindent, ' ') << "
\n"; } // end message htmloutput << std::string(extraindent, ' ') << "
\n"; if (msg_info.isgroup && msg_info.incoming && !msg_info.is_deleted && !Types::isStatusMessage(msg_info.type)) htmloutput << "
\n"; htmloutput << '\n'; } signalbackup-tools-20250313-1/signalbackup/htmlwriteattachment.cc000066400000000000000000000074701476450434500250000ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include bool SignalBackup::HTMLwriteAttachment(std::string const &directory, std::string const &threaddir, long long int rowid, long long int uniqueid, //std::string const &ext, std::string const &attachment_filename, long long int timestamp, bool overwrite, bool append) const { auto attachmentfound = d_attachments.find({rowid, uniqueid}); if (attachmentfound == d_attachments.end()) [[unlikely]] return false; // directory + threaddir is guaranteed to exist at this point, check/create 'media' if (!bepaald::fileOrDirExists(directory + "/" + threaddir + "/media")) { if (!bepaald::createDir(directory + "/" + threaddir + "/media")) { Logger::error("Failed to create directory `", directory, "/", threaddir, "/media"); return false; } } else if (!bepaald::isDir(directory + "/" + threaddir + "/media")) { Logger::error("Failed to create directory `", directory, "/", threaddir, "/media"); return false; } // check actual attachmentfile file std::string attachment_filename_full = directory + "/" + threaddir + "/media/" + attachment_filename; // "/media/Attachment_" + bepaald::toString(rowid) + "_" + bepaald::toString(uniqueid) + "." + ext; WIN_CHECK_PATH_LENGTH(attachment_filename_full); if (bepaald::fileOrDirExists(attachment_filename_full)) { if (append) // file already exists, but we were asked to just use the existing file, so we're done return true; if (!overwrite) // file already exists, but we were no asked to overwrite -> error! { //std::cout << attachment_filename << std::endl; Logger::error("Attachment file exists. Not overwriting"); return false; } } AttachmentFrame *a = attachmentfound->second.get(); // write actual attachment: std::ofstream attachmentstream(WIN_LONGPATH(attachment_filename_full), std::ios_base::binary); if (!attachmentstream.is_open()) { Logger::error("Failed to open file for writing: '", attachment_filename_full, "'", " (errno: ", std::strerror(errno), ")"); // note: errno is not required to be set by std // temporary !! { std::error_code error; std::filesystem::space_info const si = std::filesystem::space(directory, error); if (!error) { Logger::message("Space available: ", static_cast(si.available), "\nAttachment size: ", a->attachmentSize()); } } return false; } else { if (!attachmentstream.write(reinterpret_cast(a->attachmentData()), a->attachmentSize())) return false; // write was succesfull. drop attachment data a->clearData(); attachmentstream.close(); // need to close, or the auto-close will change files mtime again. if (timestamp >= 0) if (!setFileTimeStamp(attachment_filename_full, timestamp)) [[unlikely]] Logger::warning("Failed to set timestamp for attachment '", attachment_filename_full, "'"); } return true; } signalbackup-tools-20250313-1/signalbackup/htmlwriteavatar.cc000066400000000000000000000063721476450434500241260ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../scopeguard/scopeguard.h" std::string SignalBackup::HTMLwriteAvatar(long long int recipient_id, std::string const &directory, std::string const &threaddir, bool overwrite, bool append) const { std::string avatar; decltype(d_avatars.end()) pos; if ((pos = std::find_if(d_avatars.begin(), d_avatars.end(), [recipient_id](auto const &p) { return p.first == bepaald::toString(recipient_id); })) != d_avatars.end()) { AvatarFrame *a = pos->second.get(); ScopeGuard clear_avatar_data([&](){a->clearData();}); std::optional mimetype = a->mimetype(); if (!mimetype) a->attachmentData(); // get the data, so the mimetype gets set. std::string ext("bin"); mimetype = a->mimetype(); if (mimetype) ext = MimeTypes::getExtension(*mimetype, "bin"); avatar = "media/Avatar_" + pos->first + "." + ext; // directory + threaddir is guaranteed to exist at this point, check/create 'media' if (!bepaald::fileOrDirExists(directory + "/" + threaddir + "/media")) { if (!bepaald::createDir(directory + "/" + threaddir + "/media")) { Logger::error("Failed to create directory `", directory, "/", threaddir, "/media"); return std::string(); } } else if (!bepaald::isDir(directory + "/" + threaddir + "/media")) { Logger::error("Failed to create directory `", directory, "/", threaddir, "/media"); return std::string(); } // check actual avatar file if (bepaald::fileOrDirExists(directory + "/" + threaddir + "/" + avatar)) { if (append) // file already exists, but we were asked to just use the existing file, so we're done return avatar; if (!overwrite) // file already exists, but we were no asked to overwrite -> error! { Logger::error("Avatar file exists. Not overwriting"); return std::string(); } } // directory exists, now write avatar std::ofstream avatarstream(WIN_LONGPATH(directory + "/" + threaddir + "/" + avatar), std::ios_base::binary); if (!avatarstream.is_open()) { Logger::error("Failed to open file for writing: '", directory, "/", threaddir, "/", avatar, "'"); return std::string(); } else { unsigned char const *avatardata = a->attachmentData(); if (!avatardata || !avatarstream.write(reinterpret_cast(avatardata), a->attachmentSize())) return std::string(); } } return avatar; } signalbackup-tools-20250313-1/signalbackup/htmlwriteblockedlist.cc000066400000000000000000000505321476450434500251440ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::HTMLwriteBlockedlist(std::string const &dir, std::map *recipient_info, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails, bool compact) const { Logger::message("Writing blockedlist.html..."); if (bepaald::fileOrDirExists(dir + "/blockedlist.html")) { if (!overwrite && !append) { Logger::error("'", dir, "/blockedlist.html' exists. Use --overwrite to overwrite."); return false; } } std::ofstream outputfile(WIN_LONGPATH(dir + "/blockedlist.html"), std::ios_base::binary); if (!outputfile.is_open()) { Logger::error("Failed to open '", dir, "/blockedlist.html' for writing."); return false; } SqliteDB::QueryResults results; if (!d_database.exec("SELECT _id FROM recipient WHERE blocked = 1", &results)) { Logger::error("Failed to query database for blocked contacts."); return false; } bool listempty = false; if (results.rows() == 0) listempty = true; // write start of html std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); outputfile << "\n" "\n" "\n" " \n" " \n" " Signal blocked contacts list\n" " \n" " \n"; // BODY outputfile << " \n"; if (themeswitching) { outputfile << R"( )"; } outputfile << "\n" " \n" "\n" "
\n" "\n" "
\n" " Signal blocked contacts\n" "
\n" "\n" "
\n" "\n"; // write blocked list for (unsigned int i = 0; i < results.rows(); ++i) { long long int rec_id = results.valueAsInt(i, "_id"); bool hasavatar = getRecipientInfoFromMap(recipient_info, rec_id).hasavatar; bool isgroup = d_database.getSingleResultAs("SELECT COUNT(*) FROM groups WHERE group_id = (SELECT IFNULL(group_id, 0) FROM recipient WHERE _id = ?)", rec_id, -1) == 1; bool emoji_initial = getRecipientInfoFromMap(recipient_info, rec_id).initial_is_emoji; //Logger::message(rec_id, " : ", sanitizeFilename(getRecipientInfoFromMap(recipient_info, rec_id).display_name)); outputfile << "
\n" "
\n" << ((!hasavatar && !isgroup) ? " " + getRecipientInfoFromMap(recipient_info, rec_id).initial + "\n" : "") << "
\n" "
\n" "
" << HTMLescapeString(getRecipientInfoFromMap(recipient_info, rec_id).display_name) << "
\n" "
\n" "
\n"; } if (listempty) { outputfile << "
\n" " (none)\n" "
\n"; } // write end of html outputfile << "\n" " \n" "\n"; if (themeswitching) { outputfile << "
\n" "
\n" " \n" "
\n" "
\n" "\n"; } outputfile << "
\n" "
\n"; if (!exportdetails.empty()) outputfile << '\n' << exportdetails << '\n'; if (themeswitching) { outputfile << R"()"; } outputfile << "\n" " \n" "\n"; return true; } signalbackup-tools-20250313-1/signalbackup/htmlwritecalllinkdiv.cc000066400000000000000000000142141476450434500251360ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::HTMLwriteCallLinkDiv(std::ofstream &htmloutput, int indent, std::string const &url, std::string const &title, std::string const &description) const { // get avatar color /* the first byte of the root key is used to get a random color, the root key as found in the url is the binary key in base16 encoding. See: https://github.com/signalapp/ringrtc/blob/9eec8cf6795899e0e0f6a17d4e902c160cc96f00/src/rust/src/lite/call_links/root_key.rs#L147-L155 And: https://github.com/signalapp/ringrtc/blob/9eec8cf6795899e0e0f6a17d4e902c160cc96f00/src/rust/src/lite/call_links/base16.rs#L87-L145 It looks complicated, but basically each pair of letters from the alphabet, represents one 8-bit number const ALPHABET: &[u8; 16] = b"bcdfghkmnpqrstxz"; 1st letter > b c d f g h k m n p q r s t x z 2nd letter v b 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 c 16 17 18 .... d [...] t x ... 235 236 237 238 239 z 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 */ std::string color = "E3E3FE"; // random default if (url.size() > STRLEN("https://signal.link/call/#key=") + 2) { auto consonant_base16_decode = [](char first, char second) -> int32_t { int high = -1; int low = -1; switch (first) { case 'b': high = 0; break; case 'c': high = 16; break; case 'd': high = 32; break; case 'f': high = 48; break; case 'g': high = 64; break; case 'h': high = 80; break; case 'k': high = 96; break; case 'm': high = 112; break; case 'n': high = 128; break; case 'p': high = 144; break; case 'q': high = 160; break; case 'r': high = 176; break; case 's': high = 192; break; case 't': high = 208; break; case 'x': high = 224; break; case 'z': high = 240; break; } switch (second) { case 'b': low = 0; break; case 'c': low = 1; break; case 'd': low = 2; break; case 'f': low = 3; break; case 'g': low = 4; break; case 'h': low = 5; break; case 'k': low = 6; break; case 'm': low = 7; break; case 'n': low = 8; break; case 'p': low = 9; break; case 'q': low = 10; break; case 'r': low = 11; break; case 's': low = 12; break; case 't': low = 13; break; case 'x': low = 14; break; case 'z': low = 15; break; } if (high < 0 || low < 0) [[unlikely]] return -1; return high + low; }; /* // old version, the switch lookup is faster... auto consonant_base16_decode = [](char first, char second) -> int32_t { char constexpr alphabet[16] = {'b', 'c', 'd', 'f', 'g', 'h', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'x', 'z'}; int high = -1; int low = -1; for (unsigned int i = 0; i < 16; ++i) { if (high < 0 && alphabet[i] == first) { high = i; if (low >= 0) break; } if (low < 0 && alphabet[i] == second) { low = i; if (high >= 0) break; } } if (high < 0 || low < 0) [[unlikely]] return -1; return (high << 4) + low; }; */ int32_t index = consonant_base16_decode(url[STRLEN("https://signal.link/call/#key=")], url[STRLEN("https://signal.link/call/#key=") + 1]); //std::cout << (index % 12) << std::endl; if (index > 0) [[likely]] color = s_html_random_colors[index % s_html_random_colors.size()].second; } htmloutput << std::string(indent, ' ') << "
\n"; htmloutput << std::string(indent, ' ') << " \n"; htmloutput << std::string(indent, ' ') << " \n"; htmloutput << std::string(indent, ' ') << "
\n"; } signalbackup-tools-20250313-1/signalbackup/htmlwritecalllog.cc000066400000000000000000001001101476450434500242460ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::HTMLwriteCallLog(std::vector const &threads, std::string const &directory, std::string const &datewhereclause, std::map *recipientinfo, long long int notetoself_tid [[maybe_unused]], bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails, bool compact) const { Logger::message("Writing calllog.html..."); if (bepaald::fileOrDirExists(directory + "/calllog.html")) { if (!overwrite && !append) { Logger::error("'", directory, "/calllog.html' exists. Use --overwrite to overwrite."); return; } } std::ofstream outputfile(WIN_LONGPATH(directory + "/calllog.html"), std::ios_base::binary); if (!outputfile.is_open()) { Logger::error("Failed to open '", directory, "/calllog.html' for writing."); return; } // build string of requested threads std::string threadlist; for (unsigned int i = 0; i < threads.size(); ++i) { threadlist += bepaald::toString(threads[i]); if (i < threads.size() - 1) threadlist += ","; } SqliteDB::QueryResults results; if (d_database.containsTable("call")) { if (!d_database.exec("SELECT " //"_id, " "message_id, peer, type, direction, event, " + (d_database.tableContainsColumn("call", "timestamp") ? "timestamp" : "(SELECT " + d_mms_date_sent + " FROM " + d_mms_table + " WHERE " + d_mms_table + "._id = call.message_id)") + " AS timestamp " //", ringer, deletion_timestamp, " //"datetime((timestamp / 1000), 'unixepoch', 'localtime') " "FROM call WHERE " "message_id IN (SELECT DISTINCT _id FROM " + d_mms_table + " WHERE thread_id IN (" + threadlist + ")) " + datewhereclause + " " "ORDER BY timestamp DESC", &results)) { Logger::error("Failed to query database for call data."); return; } } else Logger::warning("Call table not found in database"); bool listempty = false; if (results.rows() == 0) listempty = true; //results.prettyPrint(d_truncate); /* CALL LOG: enum class Direction(private val code: Int) { INCOMING(0), OUTGOING(1); enum class Type(private val code: Int) { AUDIO_CALL(0), VIDEO_CALL(1), GROUP_CALL(3), AD_HOC_CALL(4); enum class Event(private val code: Int) { ONGOING(0), // 1:1 Calls only. ACCEPTED(1), // 1:1 and Group Calls NOT_ACCEPTED(2), // 1:1 Calls only. MISSED(3), // 1:1 and Group/Ad-Hoc Calls. Group calls: The remote ring has expired or was cancelled by the ringer. DELETE(4), // 1:1 and Group/Ad-Hoc Calls. GENERIC_GROUP_CALL(5), // Group/Ad-Hoc Calls only. Initial state. JOINED(6), // Group Calls: User has joined the group call. RINGING(7), // Group Calls: If a ring was requested by another user. DECLINED(8), // Group Calls: If you declined a ring. OUTGOING_RING(9); // Group Calls: If you are ringing a group. */ std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); //outputfile << "\n" "\n" "\n" " \n" " \n" " Signal call log\n"; // STYLE outputfile << " \n" " \n"; // BODY outputfile << " \n"; if (themeswitching) { outputfile << R"( )"; } outputfile << "\n" << " \n" << "\n" << "
\n" << "\n" << "
\n" << " Signal call log\n" << "
\n" << "\n" << "
\n" << "\n"; for (unsigned int i = 0; i < results.rows(); ++i) { long long int peer = results.valueAsInt(i, "peer"); if (peer == -1) continue; bool isgroup = d_database.getSingleResultAs("SELECT COUNT(*) FROM groups WHERE group_id = (SELECT IFNULL(group_id, 0) FROM recipient WHERE _id = ?)", peer, -1) == 1; bool hasavatar = getRecipientInfoFromMap(recipientinfo, peer).hasavatar; bool emoji_initial = getRecipientInfoFromMap(recipientinfo, peer).initial_is_emoji; long long int datetime = results.getValueAs(i, "timestamp"); std::string date_date = bepaald::toDateString(datetime / 1000, "%H:%M %b %d, %Y"); long long int type = results.valueAsInt(i, "type"); long long int event = results.valueAsInt(i, "event"); long long int direction = results.valueAsInt(i, "direction"); outputfile << "
\n" << "
\n" << ((!hasavatar && !isgroup) ? " " + getRecipientInfoFromMap(recipientinfo, peer).initial + "\n" : "") << "
\n" << "
\n" << "
" << HTMLescapeString(getRecipientInfoFromMap(recipientinfo, peer).display_name) << "
\n" << "
\n"; /* to display the correct status icon and text, 'messagetype' is used. But while it's value is set to an existing message type, it is not taken from the actual message table (message.type), but set as follows: */ long long int messagetype; if (type == 3) // 'GROUP_CALL' messagetype = Types::GROUP_CALL_TYPE; else if (direction == 0 /* incoming */ && event == 3 /* missed */) messagetype = (type == 1 /* VIDEO_CALL */ ? Types::MISSED_VIDEO_CALL_TYPE : Types::MISSED_AUDIO_CALL_TYPE); else if (direction == 0) // incoming messagetype = (type == 1 /* VIDEO_CALL */ ? Types::INCOMING_VIDEO_CALL_TYPE : Types::INCOMING_AUDIO_CALL_TYPE); else // outgoing messagetype = (type == 1 /* VIDEO_CALL */ ? Types::OUTGOING_VIDEO_CALL_TYPE : Types::OUTGOING_AUDIO_CALL_TYPE); // output status icon if (messagetype == Types::MISSED_VIDEO_CALL_TYPE || messagetype == Types::MISSED_AUDIO_CALL_TYPE) outputfile << "
\n"; else if (messagetype == Types::INCOMING_AUDIO_CALL_TYPE || messagetype == Types::INCOMING_VIDEO_CALL_TYPE) outputfile << "
\n"; else if (messagetype == Types::OUTGOING_AUDIO_CALL_TYPE || messagetype == Types::OUTGOING_VIDEO_CALL_TYPE) outputfile << "
\n"; else if (messagetype == Types::GROUP_CALL_TYPE) { if (event == 3) // missed outputfile << "
\n"; else if (event == 5 || event == 6) // 'generic group call', 'joined' outputfile << "
\n"; else if (direction == 0) // incoming outputfile << "
\n"; else if (direction == 1) // outgoing outputfile << "
\n"; } outputfile << "
"; // output status text if (messagetype == Types::MISSED_VIDEO_CALL_TYPE || messagetype == Types::MISSED_AUDIO_CALL_TYPE) outputfile << "Missed"; else if (messagetype == Types::INCOMING_AUDIO_CALL_TYPE || messagetype == Types::INCOMING_VIDEO_CALL_TYPE) outputfile << "Incoming"; else if (messagetype == Types::OUTGOING_AUDIO_CALL_TYPE || messagetype == Types::OUTGOING_VIDEO_CALL_TYPE) outputfile << "Outgoing"; else if (messagetype == Types::GROUP_CALL_TYPE) { if (event == 3) // missed outputfile << "Missed"; else if (event == 5 || event == 6) // 'generic group call', 'joined' outputfile << "Group call"; else if (direction == 0) // incoming outputfile << "Incoming"; else if (direction == 1) // outgoing outputfile << "Outgoing"; } outputfile << "·" << date_date << "
\n" "
\n" "
\n"; if (type == 0) // 'audio call' outputfile << "
\n"; else if (type == 1 || type == 3 || type == 4) // 'video'/'group'/'ad hoc' outputfile << "
\n"; outputfile << "
\n" "\n"; } if (listempty) { outputfile << "
\n" " (none)\n" "
\n"; } outputfile << " \n" "\n"; if (themeswitching) { outputfile << "
\n" "
\n" " \n" "
\n" "
\n" "\n"; } outputfile << "
\n" "
\n"; if (!exportdetails.empty()) outputfile << '\n' << exportdetails << '\n'; if (themeswitching) { outputfile << R"( )"; } // END outputfile << " \n" "\n"; } signalbackup-tools-20250313-1/signalbackup/htmlwritefullcontacts.cc000066400000000000000000000604051476450434500253460ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::HTMLwriteFullContacts(std::string const &dir, std::map *recipient_info, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails, bool compact) const { Logger::message("Writing fullcontactslist.html..."); if (bepaald::fileOrDirExists(dir + "/fullcontactslist.html")) { if (!overwrite && !append) { Logger::error("'", dir, "/fullcontactslist.html' exists. Use --overwrite to overwrite."); return false; } } std::ofstream outputfile(WIN_LONGPATH(dir + "/fullcontactslist.html"), std::ios_base::binary); if (!outputfile.is_open()) { Logger::error("Failed to open '", dir, "/fullcontactslist.html' for writing."); return false; } SqliteDB::QueryResults results; if (!d_database.exec("SELECT _id, " + (d_database.tableContainsColumn("recipient", d_recipient_type) ? d_recipient_type : "-1") + " AS 'group_type', " + d_recipient_e164 + " AS 'phone', " + (d_database.tableContainsColumn("recipient", "username") ? "username, " : "") + "registered, blocked, hidden " "FROM recipient", &results)) { Logger::error("Failed to query database for contacts."); return false; } std::vector> order; for (unsigned int i = 0; i < results.rows(); ++i) order.push_back({getRecipientInfoFromMap(recipient_info, results.valueAsInt(i, "_id")).display_name, i}); std::sort(order.begin(), order.end()); // write start of html std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); outputfile << "\n" "\n" "\n" " \n" " \n" " Signal known contacts list\n" " \n" " \n"; // BODY outputfile << " \n"; if (themeswitching) { outputfile << R"( )"; } outputfile << "\n" " \n" "\n" "
\n" "\n" "
\n" " Signal known contacts\n" "
\n" "\n" "
\n" "\n"; // write blocked list for (unsigned int ii = 0; ii < order.size(); ++ii) { long long int rec_id = results.valueAsInt(order[ii].second, "_id"); long long int registered = results.valueAsInt(order[ii].second, "registered"); long long int blocked = results.valueAsInt(order[ii].second, "blocked"); long long int hidden = results.valueAsInt(order[ii].second, "hidden"); std::string phone = results(order[ii].second, "phone"); std::string username = results(order[ii].second, "username"); bool hasavatar = getRecipientInfoFromMap(recipient_info, rec_id).hasavatar; bool isgroup = d_database.getSingleResultAs("SELECT COUNT(*) FROM groups WHERE group_id = (SELECT IFNULL(group_id, 0) FROM recipient WHERE _id = ?)", rec_id, -1) == 1; if (d_database.containsTable("distribution_list")) isgroup |= d_database.getSingleResultAs("SELECT COUNT(*) FROM distribution_list WHERE recipient_id = ?", rec_id, -1) == 1; bool emoji_initial = getRecipientInfoFromMap(recipient_info, rec_id).initial_is_emoji; //Logger::message(rec_id, " : ", sanitizeFilename(getRecipientInfoFromMap(recipient_info, rec_id).display_name)); outputfile << "
\n" "
\n" << ((!hasavatar && !isgroup) ? " " + getRecipientInfoFromMap(recipient_info, rec_id).initial + "\n" : "") << "
\n" "
\n" "
" << HTMLescapeString(getRecipientInfoFromMap(recipient_info, rec_id).display_name) << "
\n"; if (blocked != 0) outputfile << "
Blocked
\n"; if (hidden != 0) outputfile << "
Hidden
\n"; if (!phone.empty()) outputfile << "
Phone: " << phone << "
\n"; if (!username.empty()) outputfile << "
Username: " << username << "
\n"; if (!isgroup) // groups do not have registration status (always 'unknown') { switch (registered) { case 0: // Registration status UNKNOWN { outputfile << "
Registered: unknown
\n"; break; } case 1: // Registration status REGISTERED { outputfile << "
Registered: yes
\n"; break; } case 2: // Registration status NOT REGISTERED { outputfile << "
Registered: no
\n"; break; } default: break; } } if (isgroup) { long long int grouptype = results.valueAsInt(order[ii].second, "group_type"); outputfile << "
" << "Group type: "; switch (grouptype) { case 0: // GROUP TYPE NONE (should not occur, because of if (isgroup) {...}) { outputfile << " (none)"; break; } case 1: // GROUP TYPE MMS { outputfile << " MMS"; break; } case 2: // GROUP TYPE GROUP v1 { outputfile << " version 1"; break; } case 3: // GROUP TYPE GROUP v2 { outputfile << " version 2"; break; } case 4: // GROUP TYPE DISTRIBUTION LIST (story recipients) { outputfile << " distribution list"; break; } case 5: // GROUP TYPE CALL LINK (dont know what this is) { outputfile << " call link"; break; } default: // could happen if recipient.(group_)type does not exist (dbv < 79?) { outputfile << " (unknown)"; break; } } outputfile << "
\n"; } outputfile << "
\n" "
\n"; } // write end of html outputfile << "\n" " \n" "\n"; if (themeswitching) { outputfile << "
\n" "
\n" " \n" "
\n" "
\n" "\n"; } outputfile << "
\n" "
\n"; if (!exportdetails.empty()) outputfile << '\n' << exportdetails << '\n'; if (themeswitching) { outputfile << R"()"; } outputfile << "\n" " \n" "\n"; return true; } signalbackup-tools-20250313-1/signalbackup/htmlwriteindex.cc000066400000000000000000001707161476450434500237630ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::HTMLwriteIndexImpl(std::vector const &threads, long long int maxtimestamp, std::string const &directory, std::string const &basename, std::map *recipient_info, long long int note_to_self_tid, bool calllog, bool searchpage, bool stickerpacks, bool blocked, bool fullcontacts, bool settings, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails, long long int chatfolder_idx, std::vector> const &chatfolders, bool compact) const { std::string filename(sanitizeFilename(basename) + ".html"); Logger::message("Writing ", filename, "..."); if (bepaald::fileOrDirExists(directory + "/" + filename)) { if (!overwrite && !append) { Logger::error("'", directory, "/", filename, "' exists. Use --overwrite to overwrite."); return false; } } std::ofstream outputfile(WIN_LONGPATH(directory + "/" + filename), std::ios_base::binary); if (!outputfile.is_open()) { Logger::error("Failed to open '", directory, "/", filename, "' for writing."); return false; } // build string of requested threads std::string threadlist; for (unsigned int i = 0; i < threads.size(); ++i) { threadlist += bepaald::toString(threads[i]); if (i < threads.size() - 1) threadlist += ","; } // get thread_id of release channel int releasechannel = -1; for (auto const &skv : d_keyvalueframes) if (skv->key() == "releasechannel.recipient_id") releasechannel = bepaald::toNumber(skv->value()); long long int releasechannel_tid = d_database.getSingleResultAs("SELECT _id FROM thread WHERE " + d_thread_recipient_id + " = ?", releasechannel, -1); int menuitems = 0; for (bool o : {calllog, stickerpacks, blocked, fullcontacts, settings}) if (o) ++menuitems; SqliteDB::QueryResults results; if (!d_database.exec("SELECT " "thread._id, " "thread." + d_thread_recipient_id + ", " "thread.snippet, " "thread.snippet_type, " "expires_in, " "IFNULL(thread.date, 0) AS date, " "json_extract(thread.snippet_extras, '$.individualRecipientId') AS 'group_sender_id', " "json_extract(thread.snippet_extras, '$.bodyRanges') AS 'snippet_ranges', " "json_extract(thread.snippet_extras, '$.isRemoteDelete') AS 'deleted', " + (d_database.tableContainsColumn("thread", d_thread_pinned) ? "IFNULL(" + d_thread_pinned + ", 0) AS pinned," : "0 AS pinned,") + + (d_database.tableContainsColumn("thread", "archived") ? "archived," : "") + //"IFNULL(recipient.mute_until, 0) AS mute_until, " // dont think this is ever NULL //"recipient.blocked, " //"(SELECT COUNT(" + d_mms_table + "._id) FROM " + d_mms_table + " WHERE " + d_mms_table + ".thread_id = thread._id) AS message_count, " "recipient.group_id " "FROM thread " "LEFT JOIN recipient ON recipient._id IS thread." + d_thread_recipient_id + " " "WHERE thread._id IN (" + threadlist + ") AND " + d_thread_message_count + " > 0 ORDER BY " "(pinned != 0) DESC, pinned ASC, " // before 266 pinned == 0 meant 'not pinned', after pinned = NULL is not pinned. But in the code, pinned = 0 is still not possible (when pinning something, even the first thread, the value is set to 1 to start) + (d_database.tableContainsColumn("thread", "archived") ? "archived ASC, " : "") + "date DESC", &results)) { Logger::error("Failed to query database for thread snippets."); return false; } //results.prettyPrint(true); //maxtimestamp = 9999999999999; if (maxtimestamp != -1) [[unlikely]] { if (!d_database.exec("WITH partitioned_messages AS (" "SELECT " "ROW_NUMBER() OVER (PARTITION BY thread_id ORDER BY " + d_mms_table + ".date_received DESC) AS partition_idx, " + d_mms_table + ".thread_id AS _id, " "thread." + d_thread_recipient_id + ", " "NULLIF(" + d_mms_table + ".body, '') AS snippet, " + d_part_table + "." + d_part_ct + " AS partct, " /* // this messes up body ranges "CASE " " WHEN NULLIF(" + d_mms_table + ".body, '') NOT NULL THEN " // body NOT NULL " CASE " " WHEN " + d_part_table + "." + d_part_ct + " IS NULL THEN NULLIF(" + d_mms_table + ".body, '') " // no attachment " WHEN " + d_part_table + "." + d_part_ct + " IS 'image/gif' THEN CONCAT('\xF0\x9F\x8E\xA1 ', NULLIF(" + d_mms_table + ".body, '')) " " WHEN " + d_part_table + "." + d_part_ct + " LIKE 'image/%' THEN CONCAT('\xF0\x9F\x93\xB7 ', NULLIF(" + d_mms_table + ".body, '')) " " WHEN " + d_part_table + "." + d_part_ct + " LIKE 'audio/%' THEN CONCAT('\xF0\x9F\x8E\xA4 ', NULLIF(" + d_mms_table + ".body, '')) " " WHEN " + d_part_table + "." + d_part_ct + " LIKE 'video/%' THEN CONCAT('\xF0\x9F\x8E\xA5 ', NULLIF(" + d_mms_table + ".body, '')) " " ELSE NULLIF(" + d_mms_table + ".body, '') " " END " "ELSE " // body IS NULL " CASE " " WHEN " + d_part_table + "." + d_part_ct + " IS NULL THEN NULL " // no attachment " WHEN " + d_part_table + "." + d_part_ct + " IS 'image/gif' THEN '\xF0\x9F\x8E\xA1 GIF' " " WHEN " + d_part_table + "." + d_part_ct + " LIKE 'image/%' THEN '\xF0\x9F\x93\xB7 Photo' " " WHEN " + d_part_table + "." + d_part_ct + " LIKE 'audio/%' THEN '\xF0\x9F\x8E\xA4 Audio' " " WHEN " + d_part_table + "." + d_part_ct + " LIKE 'video/%' THEN '\xF0\x9F\x8E\xA5 Video' " " ELSE NULL " " END " "END AS snippet, " */ + d_mms_table + "." + d_mms_type + " AS snippet_type, " "thread.expires_in, " "IFNULL(" + d_mms_table + "." + d_mms_date_sent + ", 0) AS date, " "CAST(" + d_mms_table + "." + d_mms_recipient_id + " AS text) AS 'group_sender_id', " + d_mms_ranges + " AS 'snippet_ranges', " + (d_database.tableContainsColumn(d_mms_table, "remote_deleted") ? "remote_deleted AS 'deleted', " : "0 AS 'deleted', ") + (d_database.tableContainsColumn("thread", d_thread_pinned) ? "IFNULL(" + d_thread_pinned + ", 0) AS pinned," : "0 AS pinned,") + + (d_database.tableContainsColumn("thread", "archived") ? "thread.archived, " : "") + //"IFNULL(recipient.mute_until, 0) AS mute_until, " //"recipient.blocked, " //"-1 AS message_count, " "recipient.group_id " "FROM " + d_mms_table + " " "LEFT JOIN thread ON thread._id IS " + d_mms_table + ".thread_id " "LEFT JOIN recipient ON recipient._id IS thread." + d_thread_recipient_id + " " "LEFT JOIN " + d_part_table + " ON " + d_part_table + "." + d_part_mid + " IS " + d_mms_table + "._id " "WHERE thread._id IN (" + threadlist + ") AND " + d_thread_message_count + " > 0 " "AND " + d_mms_table + ".date_received <= ? " "AND (" + d_mms_table + "." + d_mms_type + " & ?) IS NOT ? " "AND (" + d_mms_table + "." + d_mms_type + " & ?) IS NOT ? " "AND (" + d_mms_table + "." + d_mms_type + " & ?) IS NOT ? " "AND (" + d_mms_table + "." + d_mms_type + " & ?) IS NOT ? " "AND (" + d_mms_table + "." + d_mms_type + " & ?) IS NOT ? " "AND (" + d_mms_table + "." + d_mms_type + " & ?) IS NOT ?) SELECT * FROM partitioned_messages WHERE partition_idx = 1 " "ORDER BY " "(pinned != 0) DESC, pinned ASC, " // before 266 pinned == 0 meant 'not pinned', after pinned = NULL is not pinned. But in the code, pinned = 0 is still not possible (when pinning something, even the first thread, the value is set to 1 to start) + (d_database.tableContainsColumn("thread", "archived") ? "archived ASC, " : "") + "date DESC", {maxtimestamp, Types::BASE_TYPE_MASK, Types::PROFILE_CHANGE_TYPE, Types::BASE_TYPE_MASK, Types::GV1_MIGRATION_TYPE, Types::BASE_TYPE_MASK, Types::CHANGE_NUMBER_TYPE, Types::BASE_TYPE_MASK, Types::BOOST_REQUEST_TYPE, Types::GROUP_V2_LEAVE_BITS, Types::GROUP_V2_LEAVE_BITS, Types::BASE_TYPE_MASK, Types::THREAD_MERGE_TYPE}, &results)) { Logger::error("Failed to query database for thread snippets."); return false; } //results.prettyPrint(true); } std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); //outputfile << "\n" "\n" "\n" " \n" " \n" " Signal conversation list\n" " \n" " \n" " \n"; if (themeswitching) { outputfile << R"( )"; } outputfile << "\n" " \n" "
\n" "\n" "
\n" " Signal conversation list\n"; if (chatfolder_idx >= 0) { SqliteDB::QueryResults cf_details; SqliteDB::QueryResults cf_details_members_included; SqliteDB::QueryResults cf_details_members_excluded; if (d_database.exec("SELECT name, show_unread, show_muted, show_individual, show_groups, is_muted, folder_type FROM chat_folder WHERE _id = ?", chatfolder_idx, &cf_details) && d_database.exec("SELECT thread.recipient_id FROM chat_folder_membership " "LEFT JOIN thread ON thread._id = chat_folder_membership.thread_id " "WHERE chat_folder_id = ? AND membership_type = 0", chatfolder_idx, &cf_details_members_included) && d_database.exec("SELECT thread.recipient_id FROM chat_folder_membership " "LEFT JOIN thread ON thread._id = chat_folder_membership.thread_id " "WHERE chat_folder_id = ? AND membership_type = 1", chatfolder_idx, &cf_details_members_excluded)) { outputfile << "
\n" " " << HTMLescapeString(cf_details("name")) << "\n" " \n" " \n" "
\n"; } } outputfile << "
\n" "\n" "
\n" "\n"; // for item in threads bool pinnedheader = false; bool archivedheader = false; bool chatsheader = false; for (unsigned int i = 0; i < results.rows(); ++i) { bool archived = false; if (d_database.tableContainsColumn("thread", "archived")) archived = (results.getValueAs(i, "archived") != 0); if (archived && !archivedheader) { outputfile << "
Archived conversations
\n"; archivedheader = true; } bool pinned = (results.valueAsInt(i, "pinned", 0) != 0); // before 266 pinned == 0 meant 'not pinned', after pinned = NULL is not pinned. But in the code, pinned = 0 is still not possible (when pinning something, even the first thread, the value is set to 1 to start) if (pinned && !pinnedheader) { outputfile << "
Pinned
\n"; pinnedheader = true; } if (pinnedheader && !pinned && !chatsheader && !archived) // this message is not pinned, but pinnedheader was previously shown { outputfile << "
Chats
\n"; chatsheader = true; } long long int rec_id = results.valueAsInt(i, d_thread_recipient_id); if (rec_id == -1) [[unlikely]] { Logger::warning("Failed to get thread recipient id. Skipping."); continue; } if (!results.valueHasType(i, "_id")) continue; long long int t_id = results.getValueAs(i, "_id"); if (!results.valueHasType(i, "snippet_type")) continue; long long int snippet_type = results.getValueAs(i, "snippet_type"); bool isblocked = getRecipientInfoFromMap(recipient_info, rec_id).blocked; bool ismuted = getRecipientInfoFromMap(recipient_info, rec_id).mute_until == 0x7FFFFFFFFFFFFFFF; bool isgroup = !results.isNull(i, "group_id"); bool isnotetoself = (t_id == note_to_self_tid); bool emoji_initial = getRecipientInfoFromMap(recipient_info, rec_id).initial_is_emoji; bool hasavatar = getRecipientInfoFromMap(recipient_info, rec_id).hasavatar; bool isreleasechannel = (t_id == releasechannel_tid); long long int groupsender = -1; if (results.valueHasType(i, "group_sender_id")) groupsender = bepaald::toNumber(results.valueAsString(i, "group_sender_id")); std::string snippet = results.valueAsString(i, "snippet"); //HTMLescapeString(&snippet); std::string snippet_ranges = results.valueAsString(i, "snippet_ranges"); if (!snippet_ranges.empty()) { if (maxtimestamp == -1) // ranges were taken from thread table, here they are base64 encoded { auto [data, length] = Base64::base64StringToBytes(snippet_ranges); std::pair, size_t> brdata(data, length); HTMLprepMsgBody(&snippet, std::vector>(), recipient_info, !Types::isOutgoing(snippet_type), brdata, false /*linkify*/, false); } else // range from message, here range is in binary format { std::pair, size_t> brdata = results.getValueAs, size_t>>(i, "snippet_ranges"); HTMLprepMsgBody(&snippet, std::vector>(), recipient_info, !Types::isOutgoing(snippet_type), brdata, false /*linkify*/, false); } } if (maxtimestamp != -1) // we are creating a new snippet, add attchment icon if needed { // needs to be done _after_ applying ranges. std::string attachmenttype = results.valueAsString(i, "partct"); if (STRING_STARTS_WITH(attachmenttype, "image/gif")) snippet = "\xF0\x9F\x8E\xA1 " + (snippet.empty() ? "GIF" : snippet); // ferris wheel emoji for some reason else if (STRING_STARTS_WITH(attachmenttype, "image")) snippet = "\xF0\x9F\x93\xB7 " + (snippet.empty() ? "Photo" : snippet); // (still) camera emoji else if (STRING_STARTS_WITH(attachmenttype, "audio")) snippet = "\xF0\x9F\x8E\xA4 " + (snippet.empty() ? "Voice message" : snippet); // microphone emoji else if (STRING_STARTS_WITH(attachmenttype, "video")) snippet = "\xF0\x9F\x8E\xA5 " + (snippet.empty() ? "Video" : snippet); // (movie) camera emoji else if (!attachmenttype.empty()) // if binary file snippet = "\xF0\x9F\x93\x8E " + (snippet.empty() ? "File" : snippet); // paperclip } if (results.valueAsInt(i, "deleted", 0) == 1) { if (Types::isOutgoing(snippet_type)) snippet = "You deleted this message."; else snippet = "This message was deleted."; } if (Types::isStatusMessage(snippet_type)) snippet = "(status message)"; // decodeStatusMessage(snippet, results.valueAsInt(i, "expires_in", 0), snippet_type, "", nullptr); long long int datetime = results.getValueAs(i, "date"); std::string date_date = bepaald::toDateString(datetime / 1000, "%b %d, %Y"); //std::string date_time = bepaald::toDateString(datetime / 1000, "%R"); // does not work with mingw std::string date_time = bepaald::toDateString(datetime / 1000, "%H:%M"); std::string raw_convo_url_path(isnotetoself ? "Note to Self" : getRecipientInfoFromMap(recipient_info, rec_id).display_name); WIN_LIMIT_FILENAME_LENGTH(raw_convo_url_path); std::string convo_url_path(sanitizeFilename(raw_convo_url_path) + " (_id" + bepaald::toString(t_id) + ")"); if (compact) [[unlikely]] { raw_convo_url_path.clear(); convo_url_path = "id" + bepaald::toString(t_id); } HTMLescapeUrl(&convo_url_path); std::string convo_url_location(sanitizeFilename(raw_convo_url_path) + ".html"); if (compact) [[unlikely]] convo_url_location = "0.html"; HTMLescapeUrl(&convo_url_location); if (convo_url_location == ".html") [[unlikely]] { Logger::error("Sanitized+url encoded name was empty. This should never happen. Original display_name: '", getRecipientInfoFromMap(recipient_info, rec_id).display_name, "'"); return false; } // if (t_id == 11) // { // std::cout << "Snippet: " << snippet << "\n"; // if (isgroup && groupsender > 0) // std::cout << "GROUPSEND: " + getRecipientInfoFromMap(recipient_info, groupsender).display_name << "\n"; // else // std::cout << "isgroup: " << isgroup << "\n" << "groupsender: " << groupsender << "\n"; // } outputfile << "
\n" "
\n" " \n" << ((!hasavatar && !isgroup && !isnotetoself) ? " " + getRecipientInfoFromMap(recipient_info, rec_id).initial + "\n" : "") << "
\n" "
\n" "
\n"; if (isblocked) outputfile << "
\n"; outputfile << " \n" "
" << (isnotetoself ? "Note to Self" : HTMLescapeString(getRecipientInfoFromMap(recipient_info, rec_id).display_name)) << "
\n"; if (isreleasechannel || isnotetoself) outputfile << "
\n"; if (ismuted) outputfile << "
\n"; outputfile << "
\n" " " << ((isgroup && groupsender > 0) ? "" + HTMLescapeString(getRecipientInfoFromMap(recipient_info, groupsender).display_name) + ": " : "") << snippet << "\n" "
\n" "
\n" " \n" " " << date_date << "\n" " " << date_time << "\n" "
\n" "
\n" "\n"; } if (menuitems > 0 || chatfolders.size() > 0 || chatfolder_idx >= 0) outputfile << "
\n"; if (menuitems > 1 || chatfolders.size() > 0 || (chatfolder_idx >= 0 && menuitems > 0)) // collapsible menu outputfile << "
\n" "
\n" "
\n" " menu\n" "
\n" "
\n" "\n"; if (chatfolder_idx >= 0) // back to index if current page is a chatfolder { outputfile << " \n" "
\n" "
\n" "
\n" " index\n" "
\n" "
\n" "
\n" "\n"; } if (calllog) { outputfile << " \n" "
\n" "
\n" "
\n" " call log\n" "
\n" "
\n" "
\n" "\n"; } if (stickerpacks) { outputfile << " \n" "
\n" "
\n" "
\n" " stickerpacks\n" "
\n" "
\n" "
\n" "\n"; } if (blocked) { outputfile << " \n" "
\n" "
\n" "
\n" " blocked contacts\n" "
\n" "
\n" "
\n" "\n"; } if (fullcontacts) { outputfile << " \n" "
\n" "
\n" "
\n" " all known contacts\n" "
\n" "
\n" "
\n" "\n"; } if (settings) { outputfile << " \n" "
\n" "
\n" "
\n" " settings\n" "
\n" "
\n" "
\n" "\n"; } if (menuitems && chatfolders.size()) outputfile << "
\n" "
\n" "
\n" "\n"; if (chatfolders.size()) for (auto const &cf : chatfolders) outputfile << " (cf)) << ".html\">\n" "
(cf) == chatfolder_idx ? " current-menu-item" : "") << "\">\n" "
\n" "
\n" " " << HTMLescapeString(std::get<1>(cf)) << "\n" "
\n" "
\n" "
\n" "\n"; if (menuitems > 1 || chatfolders.size() || (chatfolder_idx >= 0 && menuitems > 0)) // collapsible menu closing tags outputfile << "
\n" "
\n" "
\n"; if (menuitems > 0 || chatfolders.size() || chatfolder_idx >= 0) outputfile << "
\n"; if (themeswitching || searchpage) { outputfile << "
\n"; if (searchpage) { outputfile << "
\n" " \n" " \n" " \n" " \n" "
\n"; } if (themeswitching) { outputfile << "
\n" " \n" "
\n"; } outputfile << "
\n"; } outputfile << "
\n" "
\n"; if (!exportdetails.empty()) outputfile << '\n' << exportdetails << '\n'; if (themeswitching) { outputfile << R"()"; } outputfile << " \n" "\n"; //for (auto const &[k, v] : chatfolders) // std::cout << "'" << k << "' : " << (sanitizeFilename(v) + ".html") << std::endl; return true; } signalbackup-tools-20250313-1/signalbackup/htmlwritemsgreceiptinfo.cc000066400000000000000000000110721476450434500256570ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::HTMLwriteMsgReceiptInfo(std::ofstream &htmloutput, std::map *recipient_info, long long int message_id, bool isgroup, long long int read_count, long long int delivered_count, long long int timestamp, int indent) const { if (!isgroup) { if (read_count > 0) htmloutput << std::string(indent, ' ') << "
\n" << std::string(indent, ' ') << " Read" << (timestamp != -1 ? " - " + bepaald::toDateString(timestamp / 1000, "%b %d, %Y %H:%M:%S") : "") << '\n' << std::string(indent, ' ') << "
\n"; else if (delivered_count > 0) htmloutput << std::string(indent, ' ') << "
\n" << std::string(indent, ' ') << " Delivered" << (timestamp != -1 ? " - " + bepaald::toDateString(timestamp / 1000, "%b %d, %Y %H:%M:%S") : "") << '\n' << std::string(indent, ' ') << "
\n"; } else // isgroup { SqliteDB::QueryResults group_receipts; if (d_database.exec("SELECT address, status, timestamp FROM group_receipts WHERE mms_id = ? ORDER BY status DESC", message_id, &group_receipts) && group_receipts.rows() > 0) { htmloutput << std::string(indent, ' ') << "
\n" << std::string(indent, ' ') << " \n"; long long int prevstatus = -10; for (unsigned int i = 0; i < group_receipts.rows(); ++i) { long long int status = group_receipts.valueAsInt(i, "status", -10); if (status != prevstatus) { switch (status) { case 4: // enum SKIPPED htmloutput << std::string(indent, ' ') << " " "Skipped\n"; break; case 3: // enum VIEWED htmloutput << std::string(indent, ' ') << " " "Viewed by\n"; break; case 2: // enum READ htmloutput << std::string(indent, ' ') << " " "Read by\n"; break; case 1: // enum DELIVERED htmloutput << std::string(indent, ' ') << " " "Delivered to\n"; break; case 0: // enum UNDELIVERED htmloutput << std::string(indent, ' ') << " " "Sent to\n"; // I think... break; default: // -1 // enum UNKNOWN break; } prevstatus = status; } if (status >= 0) htmloutput << std::string(indent, ' ') << " " << HTMLescapeString(getRecipientInfoFromMap(recipient_info, group_receipts.valueAsInt(i, "address", -1)).display_name) << "" << "" << bepaald::toDateString(group_receipts.valueAsInt(i, "timestamp", -1) / 1000, "%b %d, %Y %H:%M:%S") << "\n"; } htmloutput << std::string(indent, ' ') << " \n" << std::string(indent, ' ') << "
\n"; } } } signalbackup-tools-20250313-1/signalbackup/htmlwriterevision.cc000066400000000000000000000240271476450434500245030ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::HTMLwriteRevision(long long int msg_id, std::ofstream &filt, HTMLMessageInfo const &parent_info, std::map *recipient_info, bool linkify, std::vector const &ignoremediatypes) const { SqliteDB::QueryResults revision; if (!d_database.exec("SELECT " + d_mms_recipient_id + ", " + + (d_database.tableContainsColumn(d_mms_table, "to_recipient_id") ? "to_recipient_id" : "-1") + " AS to_recipient_id, " "MIN(date_received, " + d_mms_date_sent + ") AS bubble_date, " + d_mms_date_sent + ", " + d_mms_type + ", " "body, quote_missing, quote_author, quote_body, " + d_mms_delivery_receipts + ", " + d_mms_read_receipts + ", " "json_extract(link_previews, '$[0].url') AS link_preview_url, " "json_extract(link_previews, '$[0].title') AS link_preview_title, " "json_extract(link_previews, '$[0].description') AS link_preview_description, " + (d_database.tableContainsColumn(d_mms_table, "receipt_timestamp") ? "receipt_timestamp, " : "-1 AS receipt_timestamp, ") + (d_database.tableContainsColumn(d_mms_table, "message_extras") ? "message_extras, " : "") + "shared_contacts, quote_id, expires_in, " + d_mms_ranges + ", quote_mentions" " FROM message WHERE _id = ?", msg_id, &revision) || revision.rows() != 1) return; long long int msg_recipient_id = revision.valueAsInt(0, d_mms_recipient_id); std::string readable_date = bepaald::toDateString(revision.getValueAs(0, "bubble_date"/*d_mms_date_sent*/) / 1000, "%b %d, %Y %H:%M:%S"); bool incoming = !Types::isOutgoing(revision.getValueAs(0, d_mms_type)); std::string body = revision.valueAsString(0, "body"); std::string shared_contacts = revision.valueAsString(0, "shared_contacts"); std::string quote_body = revision.valueAsString(0, "quote_body"); long long int type = revision.getValueAs(0, d_mms_type); long long int expires_in = revision.getValueAs(0, "expires_in"); bool hasquote = !revision.isNull(0, "quote_id") && revision.getValueAs(0, "quote_id"); bool quote_missing = revision.valueAsInt(0, "quote_missing", 0) != 0; SqliteDB::QueryResults attachment_results; d_database.exec("SELECT " + d_part_table + "._id, " + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id"s : "-1 AS unique_id") + ", " + d_part_ct + ", " "file_name, " + d_part_pending + ", " + (d_database.tableContainsColumn(d_part_table, "caption") ? "caption, "s : std::string()) + "sticker_pack_id, " + d_mms_table + ".date_received AS date_received " "FROM " + d_part_table + " " "LEFT JOIN " + d_mms_table + " ON " + d_mms_table + "._id = " + d_part_table + "." + d_part_mid + " " "WHERE " + d_part_mid + " IS ? " "AND quote IS ? " " ORDER BY display_order ASC, " + d_part_table + "._id ASC", {msg_id, 0}, &attachment_results); // check attachments for long message body -> replace cropped body & remove from attachment results setLongMessageBody(&body, &attachment_results); SqliteDB::QueryResults quote_attachment_results; d_database.exec("SELECT " + d_part_table + "._id, " + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id"s : "-1 AS unique_id") + ", " + d_part_ct + ", " "file_name, " + d_part_pending + ", " + (d_database.tableContainsColumn(d_part_table, "caption") ? "caption, "s : std::string()) + "sticker_pack_id, " + d_mms_table + ".date_received AS date_received " "FROM " + d_part_table + " " "LEFT JOIN " + d_mms_table + " ON " + d_mms_table + "._id = " + d_part_table + "." + d_part_mid + " " "WHERE " + d_part_mid + " IS ? " "AND quote IS ? " " ORDER BY display_order ASC, " + d_part_table + "._id ASC", {msg_id, 1}, "e_attachment_results); SqliteDB::QueryResults mention_results; d_database.exec("SELECT recipient_id, range_start, range_length FROM mention WHERE message_id IS ?", msg_id, &mention_results); SqliteDB::QueryResults reaction_results; d_database.exec("SELECT emoji, author_id, DATETIME(date_sent / 1000, 'unixepoch', 'localtime') AS 'date_sent', DATETIME(date_received / 1000, 'unixepoch', 'localtime') AS 'date_received'" " FROM reaction WHERE message_id IS ?", msg_id, &reaction_results); SqliteDB::QueryResults edit_revisions; // leave empty bool issticker = (attachment_results.rows() == 1 && !attachment_results.isNull(0, "sticker_pack_id")); IconType icon = IconType::NONE; if (Types::isStatusMessage(type)) { // see note in exporthtml long long int target_rid = msg_recipient_id; if ((Types::isIdentityVerified(type) || Types::isIdentityDefault(type)) && revision.valueAsInt(0, "to_recipient_id") != -1) [[unlikely]] target_rid = revision.valueAsInt(0, "to_recipient_id"); if (!body.empty()) body = decodeStatusMessage(body, revision.getValueAs(0, "expires_in"), type, getRecipientInfoFromMap(recipient_info, target_rid).display_name, &icon); else if (d_database.tableContainsColumn(d_mms_table, "message_extras") && revision.valueHasType, size_t>>(0, "message_extras")) body = decodeStatusMessage(revision.getValueAs, size_t>>(0, "message_extras"), revision.getValueAs(0, "expires_in"), type, getRecipientInfoFromMap(recipient_info, target_rid).display_name, &icon); } // prep body (scan emoji? -> in ) and handle mentions... // if (prepbody) std::vector> mentions; for (unsigned int mi = 0; mi < mention_results.rows(); ++mi) mentions.emplace_back(std::make_tuple(mention_results.getValueAs(mi, "recipient_id"), mention_results.getValueAs(mi, "range_start"), mention_results.getValueAs(mi, "range_length"))); std::pair, size_t> brdata(nullptr, 0); if (!revision.isNull(0, d_mms_ranges)) brdata = revision.getValueAs, size_t>>(0, d_mms_ranges); bool only_emoji = HTMLprepMsgBody(&body, mentions, recipient_info, incoming, brdata, linkify, false /*isquote*/); bool nobackground = false; if ((only_emoji && !hasquote && !attachment_results.rows()) || // if no quote etc issticker) // or sticker nobackground = true; // same for quote_body! mentions.clear(); std::pair, size_t> quote_mentions{nullptr, 0}; if (!revision.isNull(0, "quote_mentions")) quote_mentions = revision.getValueAs, size_t>>(0, "quote_mentions"); HTMLprepMsgBody("e_body, mentions, recipient_info, incoming, quote_mentions, linkify, true /*isquote*/); HTMLMessageInfo msg_info({only_emoji, false, //is_deleted, false, //is_viewonce, parent_info.isgroup, incoming, nobackground, hasquote, quote_missing, parent_info.orig_filename, parent_info.overwrite, // ? parent_info.append, // ? parent_info.story_reply, type, expires_in, msg_id, msg_recipient_id, -1, //original_message_id, 0, // messagecount, // idx of current message in &messages &revision, "e_attachment_results, &attachment_results, &reaction_results, &edit_revisions, body, quote_body, readable_date, parent_info.directory, parent_info.threaddir, parent_info.filename, revision(0, "link_preview_url"), revision(0, "link_preview_title"), revision(0, "link_preview_description"), shared_contacts, icon}); HTMLwriteMessage(filt, msg_info, recipient_info, false /*searchpage*/, false /*writereceipts*/, ignoremediatypes); } signalbackup-tools-20250313-1/signalbackup/htmlwritesearchpage.cc000066400000000000000000000726611476450434500247560ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::HTMLwriteSearchpage(std::string const &dir, bool light, bool themeswitching, bool compact) const { Logger::message("Writing searchpage.html..."); std::ofstream outputfile(WIN_LONGPATH(dir + "/" + "searchpage.html"), std::ios_base::binary); if (!outputfile.is_open()) { Logger::error("Failed to open '", dir, "/searchpage.html' for writing"); return; } outputfile << R"( Signal conversation search )*" << '\n'; if (themeswitching) { outputfile << R"( )"; } outputfile << R"code(
)code"; if (themeswitching) { outputfile << R"(
)"; } outputfile << R"( )code"; } signalbackup-tools-20250313-1/signalbackup/htmlwritesettings.cc000066400000000000000000000416321476450434500245060ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::HTMLwriteSettings(std::string const &dir, bool overwrite, bool append, bool light [[maybe_unused]], bool themeswitching [[maybe_unused]], std::string const &exportdetails [[maybe_unused]]) const { Logger::message("Writing settings.html..."); if (bepaald::fileOrDirExists(dir + "/settings.html")) { if (!overwrite && !append) { Logger::error("'", dir, "/settings.html' exists. Use --overwrite to overwrite."); return false; } } std::ofstream outputfile(WIN_LONGPATH(dir + "/settings.html"), std::ios_base::binary); if (!outputfile.is_open()) { Logger::error("Failed to open '", dir, "/settings.html' for writing."); return false; } std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); outputfile << "\n"; outputfile << "\n" "\n" " \n" " \n" " Signal settings\n" " \n" " \n"; // BODY outputfile << " \n"; if (themeswitching) { outputfile << R"( )"; } outputfile << "\n" " \n" "\n" "
\n" "\n" "
\n" " Signal settings\n" "
\n" "\n" "
\n" "\n"; bool hasoutput = false; // output keyvalueframes if "settings.*" (others may include private keys and such) if (std::any_of(d_keyvalueframes.begin(), d_keyvalueframes.end(), [](auto const &kv) { return STRING_STARTS_WITH(kv->key(), "settings."); } )) { hasoutput = true; outputfile << "
From KeyValue frames
\n" "
\n" "\n"; for (auto const &kv : d_keyvalueframes) { if (STRING_STARTS_WITH(kv->key(), "settings.")) { std::string key = kv->key(); std::string value = kv->value(); std::string valuetype = kv->valueType(); if (valuetype == "STRING") { value = "\"" + value + "\""; HTMLescapeString(&value); } else if (valuetype == "BLOB") value = "(base64: )" + value; else if (valuetype == "FLOAT") value = "(base64 float: )" + value; outputfile << "
\n" "
\n" "
" << key << "
\n" " " << value << "\n" "
\n" "
\n" "\n"; } } outputfile << "
\n"; } // output sharedpreferenceframes (if not private keys (these were in these frames only in (very) old databases)) if (std::any_of(d_sharedpreferenceframes.begin(), d_sharedpreferenceframes.end(), [](auto const &sp) { return !STRING_STARTS_WITH(sp->key(), "pref_identity_") && sp->key().find("private") == std::string::npos; } )) { hasoutput = true; outputfile << "
From SharedPreference frames
\n" "
\n" "\n"; for (auto const &sp : d_sharedpreferenceframes) { std::string key = sp->key(); if (STRING_STARTS_WITH(key, "pref_identity_") || (key.find("private") != std::string::npos)) continue; std::vector values = sp->value(); std::string valuetype = sp->valueType(); if (valuetype == "STRING" || valuetype == "STRINGSET") { for (unsigned int i = 0; i < values.size(); ++i) { values[i] = "\"" + values[i] + "\""; HTMLescapeString(&values[i]); } } if (values.empty()) { if (valuetype.empty()) values.emplace_back("???"); else values.emplace_back("(empty)"); } outputfile << "
\n" "
\n" "
" << key << "
\n"; for (unsigned int i = 0; i < values.size(); ++i) outputfile << " " << values[i] << "\n"; outputfile << "
\n" "
\n" "\n"; } outputfile << "
\n"; } if (!hasoutput) outputfile << "
\n" " (none)\n" "
\n"; outputfile << "
\n" "\n"; // write end of html outputfile << "\n" " \n" "\n"; if (themeswitching) { outputfile << "
\n" "
\n" " \n" "
\n" "
\n" "\n"; } outputfile << "
\n"; if (!exportdetails.empty()) outputfile << "\n" << exportdetails << "\n"; if (themeswitching) { outputfile << R"()"; } outputfile << "\n" " \n" "\n"; return true; } signalbackup-tools-20250313-1/signalbackup/htmlwritestickerpacks.cc000066400000000000000000000646261476450434500253440ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::HTMLwriteStickerpacks(std::string const &directory, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails) const { Logger::message("Writing stickerpacks.html..."); if (!d_database.containsTable("sticker")) { Logger::error("No stickers in database"); return false; } SqliteDB::QueryResults res_installed; if (!d_database.exec("SELECT _id, sticker_id, pack_id, pack_title, pack_author, emoji " "FROM sticker WHERE installed IS 1 AND cover IS 0 ORDER BY pack_title, pack_id, sticker_id", &res_installed)) return false; SqliteDB::QueryResults res_known; if (!d_database.exec("SELECT _id, sticker_id, pack_id, pack_title, pack_author, emoji " "FROM sticker WHERE installed IS 0 AND cover IS 1 ORDER BY pack_title, pack_id, sticker_id", &res_known)) return false; if (res_installed.rows() == 0 && res_known.rows() == 0) { Logger::warning("No stickerpacks found in database"); return false; } if (d_verbose) [[unlikely]] Logger::message("Exporting ", res_installed.rows(), " installed and ", res_known.rows(), " known stickerpacks"); if (bepaald::fileOrDirExists(directory + "/stickerpacks.html")) { if (!overwrite && !append) { Logger::error("'", directory, "/stickerpacks.html' exists. Use --overwrite to overwrite."); return false; } } // open file std::ofstream stickerhtml(WIN_LONGPATH(directory + "/stickerpacks.html"), std::ios_base::binary); if (!stickerhtml.is_open()) { Logger::error("Failed to open '", directory, "/stickerpacks.html' for writing"); return false; } // start html output std::time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); stickerhtml << "\n" "\n" "\n" " \n" " \n" " Signal stickerpacks\n"; // STYLE stickerhtml << " \n" " \n"; /* BODY */ stickerhtml << " \n"; if (themeswitching) { stickerhtml << R"( )"; } stickerhtml << " \n" "\n" "
\n" "\n" "
\n" " Stickerpacks\n" "
\n" "\n" "
\n" "\n"; // iterate stickers std::string prevpackid; for (auto const *const res : {&res_installed, &res_known}) { if (res == &res_installed && res->rows()) stickerhtml << "
Installed
\n"; if (res == &res_known && res->rows()) stickerhtml << "
Available
\n"; for (unsigned int i = 0; i < res->rows(); ++i) { std::string packid = res->valueAsString(i, "pack_id"); if (packid != prevpackid) // output header! { std::string packtitle = res->valueAsString(i, "pack_title"); HTMLescapeString(&packtitle); std::string packauthor = res->valueAsString(i, "pack_author"); HTMLescapeString(&packauthor); if (d_verbose) [[unlikely]] Logger::message("Exporting '", packtitle, "' by ", packauthor); // get cover: long long int cover_id = d_database.getSingleResultAs("SELECT _id FROM sticker WHERE pack_id = ? AND cover = 1", packid, -1); stickerhtml << "
\n" << "
\n"; std::string ext("bin"); if (cover_id == -1 || !writeStickerToDisk(cover_id, packid, directory, overwrite, append, &ext)) [[unlikely]] Logger::message("No cover found for stickerpack ", packid); else stickerhtml << "
\n" " \"cover\"\n" "
\n"; stickerhtml << "
\n" "
" << packtitle << "
\n" "
" << packauthor << "
\n" "
\n" "
\n" "
\n" "\n"; prevpackid = packid; if (res == &res_known) // for packs known, but not installed, stop here (only output header) continue; } long long int id = res->valueAsInt(i, "_id"); if (id < 0) { Logger::warning("Unexpected id value"); continue; } long long int stickerid = res->valueAsInt(i, "sticker_id"); std::string emoji = res->valueAsString(i, "emoji"); // write actual file to disk std::string ext("bin"); if (!writeStickerToDisk(id, packid, directory, overwrite, append, &ext)) [[unlikely]] { Logger::warning("There was a problem writing the sitcker data to file"); continue; } //Logger::message("Sticker ", stickerid, ": ", emoji); stickerhtml << "
\n" "
\n" "
\n" " \n" " \n" "
\n" "
\n" "
" << stickerid << ". " << emoji << "
\n" "
\n" "\n"; } } // write end of html stickerhtml << " \n" " \n" "\n"; if (themeswitching) { stickerhtml << "
\n" "
\n" " \n" "
\n" "
\n" "\n"; } stickerhtml << "
\n" "
\n"; if (!exportdetails.empty()) stickerhtml << '\n' << exportdetails << '\n'; stickerhtml << " \n"; if (themeswitching) { stickerhtml << R"( )"; } stickerhtml << " \n" "\n"; return true; } #include "../scopeguard/scopeguard.h" bool SignalBackup::writeStickerToDisk(long long int id, std::string const &packid, std::string const &directory, bool overwrite, bool append, std::string *extension) const { // write actual file to disk // find the sticker with id auto it = d_stickers.find(id); if (it == d_stickers.end()) [[unlikely]] { Logger::warning("Failed to find sticker (id: ", id, ")"); return false; } // make sure a 'stickers/' subdirectory exists, and 'stickers/stickerpack_id/' exists for (auto subdir : {"/stickers"s, "/stickers/"s + packid}) { if (!bepaald::fileOrDirExists(directory + subdir)) { if (!bepaald::createDir(directory + subdir)) { Logger::error("Failed to create directory `", directory, "/", subdir, "'"); return false; } } else if (!bepaald::isDir(directory + subdir)) { Logger::error("Failed to create directory `", directory, "/", subdir, "'"); return false; } } StickerFrame *s = it->second.get(); // get the data, so the mimetype is determined unsigned char const *stickerdata = s->attachmentData(); ScopeGuard clear_sticker_data([&](){s->clearData();}); std::optional mimetype = s->mimetype(); if (mimetype) *extension = MimeTypes::getExtension(*mimetype, "bin"); std::string stickerdatapath = directory + "/stickers/" + packid + "/Sticker_" + bepaald::toString(id) + "." + *extension; // check actual sticker file if (bepaald::fileOrDirExists(stickerdatapath) && !overwrite) { if (!append) { Logger::error("Avatar file exists. Not overwriting"); return false; } // file exists, we are appending, we assume we're done return true; } std::ofstream stickerstream(WIN_LONGPATH(stickerdatapath), std::ios_base::binary); if (!stickerstream.is_open()) [[unlikely]] { Logger::error("Failed to open '", stickerdatapath, "' for writing"); return false; } if (!stickerstream.write(reinterpret_cast(stickerdata), s->attachmentSize())) [[unlikely]] { Logger::error("Failed to write sticker data to file"); return false; } return true; } signalbackup-tools-20250313-1/signalbackup/importcsv.cc000066400000000000000000000056231476450434500227340ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" /* * Things to deal with: * get proper thread from id (phone number) * deal with type, in what way do we assume incoming/outgoing is specified in the csv * should imported messages be marked as received / read * should they be imported as secured or unsecured messages? */ bool SignalBackup::importCSV(std::string const &file, std::map const &fieldmap) { CSVReader csvfile(file); if (!csvfile.ok()) return false; std::string statementstub("INSERT INTO sms SET ("); int64_t idx_of_address = -1; //int64_t idx_of_type = -1; std::vector date_indeces; // get columns to set for (unsigned int i = 0; i < csvfile.fields(); ++i) { std::string fieldname = csvfile.getFieldName(i); if (fieldmap.find(fieldname) != fieldmap.end())// (fieldmap.contains(fieldname)) fieldname = fieldmap.at(fieldname); if (fieldname == d_sms_recipient_id) idx_of_address = i; else if (fieldname == "type") ;//idx_of_type = i; else if (fieldname.find("date") != std::string::npos) /// not sure what this does, and if it works as intended date_indeces.push_back(i); // with d_sms_date_received statementstub += fieldname + ','; } statementstub += "thread_id) VALUES ("; // build statement from each row for (unsigned int msg = 0; msg < csvfile.rows(); ++msg) { std::string statement = statementstub; for (unsigned int f = 0; f < csvfile.fields(); ++f) { //if (f == idx_of_type) // translate type? //if (date_indeces.contains(f)) //{ // std::string date = csvfile.get(f, msg); // if (date.find_first_not_of("0123456789") != std::string::npos) // translate(date); //} statement += csvfile.get(f, msg) + ','; } // determine thread_id long long int tid = getThreadIdFromRecipient(csvfile.get(idx_of_address, msg)); if (tid == -1) { Logger::error("Unable to determine thread_id for message."); return false; } statement += bepaald::toString(tid); statement += ')'; if (!d_database.exec(statement)) return false; } return true; } signalbackup-tools-20250313-1/signalbackup/importfromdesktop.cc000066400000000000000000003136401476450434500244770ustar00rootroot00000000000000/* Copyright (C) 2022-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../desktopdatabase/desktopdatabase.h" #include "../msgtypes/msgtypes.h" /* TODO DONE? - limit timeframe DONE? - AUTO timeframe DONE? - fix address for call messages DONE? - implement call messages (group video done?) - implement group-v2- stuff Known missing things: DONE ? - messages for conversation that is not in thread table (-> create new thread for recipient) - when recipient is not present in backup? - message types other than 'incoming' and 'outgoing' - 'group-v2-change' (group member add/remove/change group name/picture - other status messages, like disappearing msgs timer change, profile key change etc - inserting into group-v1-type groups DONE? - all received/read receipts DONE? - voice_note flag in part table? - any group-v1 stuff - stories? - payments - badges - more... */ /* /////////////////////////////// SMS COLUMNS: //_id // this is an AUTO value "thread_id," "address/recipient_id," //"address_device_id/recipient_device_id," //"person,"// = "date,"// = 1663067790169 "date_sent,"// = 1663067792779 "date_server,"// = 1663067790149 //"protocol,"// always 31337? maybe not necessary? //"read," //"status," "type," // = incoming/outgoing + secure //"reply_path_preseny,"// = 1 //"delivery_receipt_count,"// = 0 //"subject,"// = "body,"// //"mismatched_identities,"// = //"service_center,"// = GCM //"subscription_id,"// = -1 //"expires_in,"// = 0 //"expire_started,"// = 0 //"notified,"// = 0 //"read_receipt_count,"// = 0 //"unidentified,"// = 1 //"reactions,"// DOES NOT EXIST IN NEWER DATABASES //"reactions_unread,"// = 0 //"reactions_last_seen,"// = 1663078811832 "remote_deleted,"// = 0 //"notified_timestamp,"// = 1663072365960 "server_guid"// = 0bb19070-e1a2-4c52-b637-00e905583bc1 //"receipt_timestamp"// = -1 // is -1 default? /////////////////////////////// MMS COLUMNS: //"_id,"// AUTO VALUE "thread_id," "date,"// = = 1474184079794 "date_received,"// = = 1474184079855 "date_server,"// = = -1 "msg_box,"// = = 10485783 //"read,"// = = 1 "body,"// //"part_count,"// don't know what this is... not number of attachments // REMOVED IN DBV166 //"ct_l,"// = = "address,/recipient_id"// = = 53 //"address_device_id/recipient_device_id,"// = = //"exp,"// = = "m_type,"// = = 128 //"m_size,"// = = //"st,"// = = //"tr_id,"// = = //"delivery_receipt_count,"// = = 2 //"mismatched_identities,"// = = //"network_failures,"// = = //"subscription_id,"// = = -1 //"expires_in,"// = = 0 //"expire_started = 0 //"notified,"// = = 0 //"read_receipt_count,"// = = 0 "quote_id,"// corresponds to 'messages.date' of quoted message "quote_author,"// = = "quote_body,"// = = "quote_attachment,"// = = -1 "quote_missing,"// = = 0 "quote_mentions,"// = = //"shared_contacts,"// = = //"unidentified,"// = = 0 //"previews,"// = = //"reveal_duration,"// = = 0 //"reactions,"// = = //"reactions_unread,"// = = 0 //"reactions_last_seen,"// = = -1 "remote_deleted,"// = = 0 //"mentions_self,"// = = 0 //"notified_timestamp,"// = = 0 //"viewed_receipt_count,"// = = 0 //"server_guid,"// = //"receipt_timestamp,"// = = -1 //"ranges,"// = = //"is_story,"// = = 0 //"parent_story_id,"// = = 0 "quote_type"// = = 0 /////////////////////////////// PART COLUMNS: //"_id," // = AUTO VALUE "mid," // = 5500 //"seq," // = 0 "ct," // = image/jpeg //"name," // = //"chset," // = //"cd," // = A1sAd5JPAdm5SxZ9q2Bn/2X7BQw/vJfmaWk1zet2cFgb9D+2xpSRyMjuOcUZP7Lic3AEp38BIxKg/LCLMr2v5w== //"fn," // = //"cid," // = //"cl," // = DlImHS8vRhF5VM5ueDVh //"ctt_s," // = //"ctt_t," // = //"encrypted," // = "pending_push," // MUST BE ZERO (i think) //"_data," // = FILLED IN ON RESTORE? /data/user/0/org.thoughtcrime.securesms/app_parts/part7685241378172293912.mms "data_size," // = 421 "file_name," // = //"thumbnail," // = //"aspect_ratio," // = "unique_id," // = 1630950584787 //"digest," // = (binary) //"fast_preflight_id," // = "voice_note," // = 0 //"data_random," // = FILLED IN ON RESTORE? (binary) //"thumbnail_random," // = "width," // = 16 "height," // = 16 "quote," // = 0 //"caption," // = //"sticker_pack_id," // = //"sticker_pack_key," // = //"sticker_id," // = -1 "data_hash," // = Msx++MxFQPNuuCPnsO5Q9H2twoNFPMOKpH521FDVn+U= |-> "data_hash_start," \-> "data_hash_end," //"blur_hash," // = LN7nwD_M_M_M_M_M_M_M_M_M_M_M //"transform_properties," // = {"skipTransform":true,"videoTrim":false,"videoTrimStartTimeUs":0,"videoTrimEndTimeUs":0,"sentMediaQuality":0,"videoEdited":false} //"transfer_file," // = //"display_order," // = 0 //"upload_timestamp," // = 1630950581728 "cdn_number" // = 2 //"borderless," // = 0 //"sticker_emoji," // = //"video_gif" // = 0 /////////////////////////////// REACTION COLUMNS // _id = 80 // message_id = 66869 // is_mms = 0 // author_id = 7 // emoji = 👍 // date_sent = 1662929051259 // date_received = 1662929052309 /////////////////////////////// MENTION COLUMN // mention entry in android db // _id = 10 // thread_id = 43 // message_id = 5910 // recipient_id = 71 // range_start = 40 // range_length = 1 */ bool SignalBackup::importFromDesktop(std::unique_ptr const &dtdb, bool skipmessagereorder, std::vector const &daterangelist, bool createmissingcontacts, bool createmissingcontacts_valid, bool autodates, bool importstickers, std::string const &selfphone, bool targetisdummy) { if (d_verbose) [[unlikely]] Logger::message("Starting importFromDesktop()"); if (d_selfid == -1) { d_selfid = selfphone.empty() ? scanSelf() : d_database.getSingleResultAs("SELECT _id FROM recipient WHERE " + d_recipient_e164 + " = ?", selfphone, -1); if (d_selfid == -1) { Logger::error_start("Failed to determine id of 'self'."); if (selfphone.empty()) Logger::message_start(" Please pass `--setselfid \"[phone]\"' to set it manually"); Logger::message_end(); return false; } if (d_selfuuid.empty()) { d_selfuuid = bepaald::toLower(d_database.getSingleResultAs("SELECT " + d_recipient_aci + " FROM recipient WHERE _id = ?", d_selfid, std::string())); if (d_selfuuid.empty()) Logger::warning("Failed to set self-uuid"); } } // DesktopDatabase dtdb(configdir_hint, databasedir_hint, hexkey, d_verbose, ignorewal, sqlcipherversion, d_truncate); if (!dtdb->ok()) { Logger::error("Failed to open Signal Desktop sqlite database"); return false; } dtSetColumnNames(&dtdb->d_database); //std::string configdir = dtdb->getConfigDir(); std::string const &databasedir = dtdb->getDatabaseDir(); std::vector> dateranges; if (daterangelist.size() % 2 == 0) for (unsigned int i = 0; i < daterangelist.size(); i += 2) dateranges.push_back({daterangelist[i], daterangelist[i + 1]}); // set daterange automatically if (dateranges.empty() && autodates) { SqliteDB::QueryResults res; if ((d_database.containsTable("sms") && !d_database.exec("SELECT MIN(mindate) FROM (SELECT MIN(sms." + d_sms_date_received + ", " + d_mms_table + ".date_received) AS mindate FROM sms " "LEFT JOIN " + d_mms_table + " WHERE sms." + d_sms_date_received + " IS NOT NULL AND " + d_mms_table + ".date_received IS NOT NULL)", &res)) || (!d_database.containsTable("sms") && !d_database.exec("SELECT MIN(" + d_mms_table + ".date_received) AS mindate, MAX(" + d_mms_table + ".date_received) AS maxdate FROM " + d_mms_table + " WHERE " + d_mms_table + ".date_received IS NOT NULL", &res))) { Logger::error("Failed to automatically determine data-range"); return false; } dateranges.push_back({"0", res.valueAsString(0, "mindate")}); dateranges.push_back({res.valueAsString(0, "maxdate"), bepaald::toString(std::numeric_limits::max())}); } std::string datewhereclause; for (unsigned int i = 0; i < dateranges.size(); ++i) { bool needrounding = false; long long int startrange = dateToMSecsSinceEpoch(dateranges[i].first); long long int endrange = dateToMSecsSinceEpoch(dateranges[i].second, &needrounding); if (startrange == -1 || endrange == -1 || endrange < startrange) { Logger::error("Skipping range: '", dateranges[i].first, " - ", dateranges[i].second, "'. Failed to parse or invalid range."); continue; } Logger::message(" Using range: ", dateranges[i].first, " - ", dateranges[i].second, " (", startrange, " - ", endrange, ")"); if (needrounding)// if called with "YYYY-MM-DD HH:MM:SS" endrange += 999; // to get everything in the second specified... datewhereclause += (datewhereclause.empty() ? " AND (" : " OR ") + "JSONLONG(sent_at) BETWEEN "s + bepaald::toString(startrange) + " AND " + bepaald::toString(endrange); if (i == dateranges.size() - 1) datewhereclause += ')'; } bool warned_createcontacts = createmissingcontacts_valid; // no warning if explicitly requesting this... // find out which database is newer long long int maxdate_desktop_db = dtdb->d_database.getSingleResultAs("SELECT MAX(MAX(COALESCE(received_at_ms, json_extract(json, '$.received_at_ms'))),MAX(received_at)) FROM messages", 0); long long int maxdate_android_db = d_database.getSingleResultAs("SELECT MAX(date_received) FROM " + d_mms_table, 0); if (d_database.containsTable("sms")) maxdate_android_db = d_database.getSingleResultAs("SELECT MAX((SELECT MAX(date_received) FROM " + d_mms_table + "),(SELECT MAX(" + d_sms_date_received + ") FROM sms))", 0); bool desktop_is_newer = maxdate_desktop_db > maxdate_android_db; // get all conversations (conversationpartners) from ddb SqliteDB::QueryResults results_all_conversations; if (!dtdb->d_database.exec("SELECT " "rowid," "id," "e164," "type," "LOWER(" + d_dt_c_uuid + ") AS 'uuid'," "groupId," "IFNULL(json_extract(json,'$.isArchived'), false) AS 'is_archived'," "IFNULL(json_extract(json,'$.isPinned'), false) AS 'is_pinned'," "IFNULL(json_extract(json,'$.groupId'),'') AS 'json_groupId'," "IFNULL(json_extract(json,'$.derivedGroupV2Id'),'') AS 'derivedGroupV2Id'," "IFNULL(json_extract(json,'$.groupVersion'), 1) AS groupVersion" " FROM conversations WHERE json_extract(json, '$.messageCount') > 0", &results_all_conversations)) return false; //std::cout << "Conversations in desktop:" << std::endl; //results_all_conversations.prettyPrint(true); // this map will map desktop-recipient-uuid's to android recipient._id's std::map recipientmap; // for each conversation for (unsigned int i = 0; i < results_all_conversations.rows(); ++i) { // skip convo's with no messages... SqliteDB::QueryResults messagecount; if (dtdb->d_database.exec("SELECT COUNT(*) AS count FROM messages WHERE conversationId = ?" + datewhereclause, results_all_conversations(i, "id"), &messagecount)) if (messagecount.rows() == 1 && messagecount.getValueAs(0, "count") == 0) { Logger::message("Skipping conversation, conversation has no messages ", (datewhereclause.empty() ? "" : "in requested time period "), "(", i + 1, "/", results_all_conversations.rows(), ")"); continue; } Logger::message("Trying to match conversation " "(", Logger::Control::BOLD, i + 1, "/", results_all_conversations.rows(), Logger::Control::NORMAL, ")" " (type: ", results_all_conversations.valueAsString(i, "type"), ")"); //long long int conversation_rowid = results_all_conversations.getValueAs(i, "rowid"); // get the actual id bool isgroupconversation = false; std::string person_or_group_id; if (results_all_conversations.valueAsString(i, "type") == "group") { if (results_all_conversations.getValueAs(i, "groupVersion") > 1) { std::pair groupid_data = Base64::base64StringToBytes(results_all_conversations.valueAsString(i, "json_groupId")); if (!groupid_data.first || groupid_data.second == 0) // data was not valid base64 string, lets try the other one groupid_data = Base64::base64StringToBytes(results_all_conversations.valueAsString(i, "groupId")); if (groupid_data.first && groupid_data.second != 0) { //std::cout << bepaald::bytesToHexString(groupid_data, groupid_data_length, true) << std::endl; person_or_group_id = "__signal_group__v2__!" + bepaald::bytesToHexString(groupid_data, true); isgroupconversation = true; bepaald::destroyPtr(&groupid_data.first, &groupid_data.second); } else { Logger::error("Conversation is 'group'-type, but groupId unexpectedly was not base64 data. Maybe this is a groupV1 group? Here is the data: "); dtdb->d_database.printLineMode("SELECT * FROM conversations WHERE id = ?", results_all_conversations.value(i, "id")); continue; } } else // group v1 maybe? { // see if it has a 'derivedgroupv2id', and if that can be matched... bool found_new_group = false; std::pair groupid_data = Base64::base64StringToBytes(results_all_conversations.valueAsString(i, "derivedGroupV2Id")); if (groupid_data.first || groupid_data.second > 0) { if (d_verbose) [[unlikely]] Logger::message("Trying to match group-v1 by 'derivedGroupV2Id'"); person_or_group_id = "__signal_group__v2__!" + bepaald::bytesToHexString(groupid_data, true); if (getRecipientIdFromUuidMapped(person_or_group_id, &recipientmap) != -1) found_new_group = true; bepaald::destroyPtr(&groupid_data.first, &groupid_data.second); } if (found_new_group) isgroupconversation = true; else { /* std::cout << bepaald::bold_on << "Error" << bepaald::bold_off << ": Group V1 type not yet supported" << std::endl; SqliteDB::QueryResults groupid_res; dtdb->d_database.exec("SELECT HEX(groupId) FROM conversations WHERE id = ?", results_all_conversations.value(i, "id"), &groupid_res); if (groupid_res.rows()) std::cout << " Possible group id: " << groupid_res.valueAsString(0, 0) << std::endl; */ // lets just for fun try to find an old-style group with this id: if (results_all_conversations.valueHasType(i, "groupId")) { std::string groupv1id_str = results_all_conversations.valueAsString(i, "groupId"); person_or_group_id = "__textsecure_group__!" + utf8BytesToHexString(groupv1id_str); isgroupconversation = true; //std::cout << "Possible GroupV1 id from STRING: " << gid << std::endl; //d_database.prettyPrint("SELECT _id,group_id FROM groups WHERE LOWER(group_id) == LOWER(?)", gid); } else continue; // person_or_group_id = "__textsecure_group__!" + bepaald::bytesToHexString(reinterpret_cast(giddata.data()), giddata.size()); // isgroupconversation = true; } } } else // type != 'group' ( == 'private'?) person_or_group_id = results_all_conversations.valueAsString(i, "uuid"); // single person id, if group, this is empty // get/create matching thread id from android database long long int recipientid_for_thread = -1; std::string phone; if (!person_or_group_id.empty()) recipientid_for_thread = getRecipientIdFromUuidMapped(person_or_group_id, &recipientmap, createmissingcontacts); else { Logger::warning("Failed to determine uuid. Trying with phone number..."); phone = results_all_conversations.valueAsString(i, "e164"); if (!phone.empty()) recipientid_for_thread = getRecipientIdFromPhoneMapped(phone, &recipientmap, createmissingcontacts); } if (recipientid_for_thread == -1 && (createmissingcontacts || createmissingcontacts_valid)) { recipientid_for_thread = dtCreateRecipient(dtdb->d_database, person_or_group_id, results_all_conversations.valueAsString(i, "e164"), results_all_conversations.valueAsString(i, "groupId"), databasedir, &recipientmap, createmissingcontacts_valid, &warned_createcontacts); if (recipientid_for_thread == -1) { Logger::warning("Failed to create missing recipient. Skipping."); continue; } } if (recipientid_for_thread == -1) { Logger::warning("Chat partner was not found in recipient-table. Skipping. (id: ", (person_or_group_id.empty() ? results_all_conversations.valueAsString(i, "e164") : person_or_group_id), ")"); continue; } SqliteDB::QueryResults results2; long long int ttid = -1; if (!d_database.exec("SELECT _id FROM thread WHERE " + d_thread_recipient_id + " = ?", recipientid_for_thread, &results2)) continue; if (results2.rows() == 1) // we have found our matching thread ttid = results2.getValueAs(0, "_id"); // ttid : target thread id else if (results2.rows() == 0) // the query was succesful, but yielded no results -> create thread { Logger::message_start("Failed to find matching thread for conversation, creating. (", (person_or_group_id.empty() ? "from e164" : ("id: " + makePrintable(person_or_group_id)))); std::any new_thread_id; if (!insertRow("thread", {{d_thread_recipient_id, recipientid_for_thread}, {"active", 1}, {"archived", results_all_conversations.getValueAs(i, "is_archived")}}, "_id", &new_thread_id)) { Logger::message_end(); Logger::error("Failed to create thread for desktop conversation. (", (person_or_group_id.empty() ? "from e164" : ("id: " + person_or_group_id)), "), skipping."); continue; } //std::cout << "Raw any_cast 1" << std::endl; ttid = std::any_cast(new_thread_id); Logger::message_end(", thread_id: ", ttid, ")"); } if (ttid < 0) { Logger::error("No thread for this conversation was found or created. Skipping."); continue; } // in newer databases, the date_sent may need to be adjusted on insertion because of a UNIQUE constraint. // however this date is used as an id for the source of a quoted message. This map saves any adjusted // timestamps std::map adjusted_timestamps; //std::cout << "Match for " << person_or_group_id << std::endl; //std::cout << " - ID of thread in Android database that matches the conversation in desktopdb: " << ttid << std::endl; // we have the Android thread id (ttid) and the desktop data (results_all_conversations.value(i, "xxx")), update // Androids pinned and archived status if desktop is newer: if (desktop_is_newer) { bool res = d_database.exec("UPDATE thread SET archived = ? WHERE _id = ?", {results_all_conversations.getValueAs(i, "is_archived"), ttid}); if (res) { if (results_all_conversations.valueAsInt(i, "is_pinned", 0) > 0) // pin in Android database res &= d_database.exec("UPDATE thread SET " + d_thread_pinned + " = IFNULL((SELECT MAX(" + d_thread_pinned + ") FROM thread), 0) + 1 " "WHERE _id = ? AND (" + d_thread_pinned + " IS NULL OR " + d_thread_pinned + " = 0)", ttid); else // unpin in Android database { // to unpin something, set it to the default value. This was '0' before dbv266, 'NULL' after... std::string pinned_default = d_database.getSingleResultAs("SELECT dflt_value FROM pragma_table_info('thread') WHERE name = '" + d_thread_pinned + "'", std::string()); res &= d_database.exec("UPDATE thread SET " + d_thread_pinned + " = " + pinned_default + " WHERE _id = ?", ttid); } } if (!res) Logger::warning("Failed to update thread properties (id: ", ttid, ")"); } // now lets get all messages for this conversation SqliteDB::QueryResults results_all_messages_from_conversation; if (!dtdb->d_database.exec("SELECT " "rowid," "json_extract(json, '$.quote') AS quote," "IFNULL(json_array_length(json, '$.attachments'), 0) AS numattachments," "IFNULL(json_array_length(json, '$.reactions'), 0) AS numreactions," "IFNULL(json_array_length(json, '$.bodyRanges'), 0) AS nummentions," "IFNULL(json_array_length(json, '$.editHistory'), 0) AS editrevisions," "json_extract(json, '$.callHistoryDetails.creatorUuid') AS group_call_init," "IFNULL(json_extract(json, '$.flags'), 0) AS flags," // see 'if (type.empty())' below for FLAGS enum "body," "type," "JSONLONG(COALESCE(sent_at, json_extract(json, '$.sent_at'), json_extract(json, '$.received_at_ms'), received_at, json_extract(json, '$.received_at'))) AS sent_at," "hasAttachments," // any attachment "hasFileAttachments," // non-media files? (any attachment that does not get a preview?) "hasVisualMediaAttachments," // ??? "IFNULL(isErased, 0) AS isErased," "IFNULL(isViewOnce, 0) AS isViewOnce," "serverGuid," "LOWER(" + d_dt_m_sourceuuid + ") AS 'sourceUuid'," "json_extract(json, '$.source') AS sourcephone," "JSONLONG(expireTimer) AS expireTimer," "seenStatus," "IFNULL(json_array_length(json, '$.preview'), 0) AS haspreview," "IFNULL(json_array_length(json, '$.bodyRanges'), 0) AS hasranges," "IFNULL(json_array_length(json, '$.contact'), 0) AS hassharedcontact," "IFNULL(json_extract(json, '$.callId'), '') AS callId," "json_extract(json, '$.sticker') IS NOT NULL AS issticker," "isStory" " FROM messages WHERE conversationId = ?" + datewhereclause, results_all_conversations.value(i, "id"), &results_all_messages_from_conversation)) { Logger::error("Failed to retrieve message from this conversation."); continue; } //results_all_messages_from_conversation.printLineMode(); Logger::message(" - Importing ", results_all_messages_from_conversation.rows(), " messages into thread._id ", ttid); for (unsigned int j = 0; j < results_all_messages_from_conversation.rows(); ++j) { std::string type = results_all_messages_from_conversation.valueAsString(j, "type"); if (d_verbose) [[unlikely]] Logger::message("Message ", j + 1, "/", results_all_messages_from_conversation.rows(), ":", (!type.empty() ? " '" + type + "'" : ""), " (rowid: ", results_all_messages_from_conversation.getValueAs(j, "rowid"), ")"); long long int rowid = results_all_messages_from_conversation.getValueAs(j, "rowid"); //bool hasattachments = (results_all_messages_from_conversation.getValueAs(j, "hasAttachments") == 1); bool outgoing = (type == "outgoing" || type == "message-request-response-event"); bool incoming = (type == "incoming" || type == "profile-change" || type == "keychange" || type == "verified-change" || type == "change-number-notification"); long long int numattachments = results_all_messages_from_conversation.getValueAs(j, "numattachments"); long long int numreactions = results_all_messages_from_conversation.getValueAs(j, "numreactions"); long long int nummentions = results_all_messages_from_conversation.getValueAs(j, "nummentions"); bool hasquote = !results_all_messages_from_conversation.isNull(j, "quote"); long long int flags = results_all_messages_from_conversation.getValueAs(j, "flags"); long long int haspreview = results_all_messages_from_conversation.getValueAs(j, "haspreview"); long long int hasranges = results_all_messages_from_conversation.getValueAs(j, "hasranges"); long long int hassharedcontact = results_all_messages_from_conversation.getValueAs(j, "hassharedcontact"); bool issticker = results_all_messages_from_conversation.getValueAs(j, "issticker"); // get address (needed in both mms and sms databases) // for 1-on-1 messages, address is conversation partner (with uuid 'person_or_group_id') // for group messages, incoming: address is person originating the message (sourceUuid) // outgoing: address is id of group (with group_id 'person_or_group_id') // for group calls long long int address = -1; if (!results_all_messages_from_conversation.isNull(j, "group_call_init")) { // group calls always have address set to the one initiating the call address = getRecipientIdFromUuidMapped(results_all_messages_from_conversation.valueAsString(j, "group_call_init"), &recipientmap, createmissingcontacts); } else if (isgroupconversation && incoming && type != "group-v1-migration") //if (isgroupconversation && (incoming || (type == "call-history" & something))) { // incoming group messages have 'address' set to the group member who sent the message/ // profile change has source and sourceUuid NULL. There is 'changedId' which is the // conversationId of the source. SqliteDB::QueryResults statusmsguuid; if (type == "profile-change") { if (!dtdb->d_database.exec("SELECT " + d_dt_c_uuid + " AS uuid, e164 FROM conversations WHERE " "id IS (SELECT json_extract(json, '$.changedId') FROM messages WHERE rowid IS ?1) OR " "e164 IS (SELECT json_extract(json, '$.changedId') FROM messages WHERE rowid IS ?1)", // maybe id can be a phone number? rowid, &statusmsguuid)) { Logger::warning("Failed to get uuid for incoming group profile-change."); // print some extra info //dtdb->d_database.printLineMode("SELECT * FROM messaages WHERE rowid IS ?)", rowid); } } else if (type == "keychange") { if (!dtdb->d_database.exec("SELECT " + d_dt_c_uuid + " AS uuid, e164 FROM conversations WHERE " + d_dt_c_uuid + " IS (SELECT json_extract(json, '$.key_changed') FROM messages WHERE rowid IS ?1) OR " "e164 IS (SELECT json_extract(json, '$.key_changed') FROM messages WHERE rowid IS ?1)", // 'key_changed' can be a phone number (confirmed) rowid, &statusmsguuid)) { Logger::warning("Failed to get uuid for incoming group keychange."); // print some extra info //dtdb->d_database.printLineMode("SELECT * FROM messaages WHERE rowid IS ?)", rowid); } } else if (type == "verified-change") { if (!dtdb->d_database.exec("SELECT " + d_dt_c_uuid + " AS uuid, e164 FROM conversations WHERE " "id IS (SELECT json_extract(json, '$.verifiedChanged') FROM messages WHERE rowid IS ?1) OR " "e164 IS (SELECT json_extract(json, '$.verifiedChanged') FROM messages WHERE rowid IS ?1)",// maybe id can be a phone number? rowid, &statusmsguuid)) { Logger::warning("Failed to get uuid for incoming group verified-change."); // print some extra info //dtdb->d_database.printLineMode("SELECT * FROM messaages WHERE rowid IS ?)", rowid); } } // NOTE this might fail on messages sent from a desktop app, those may // have sourceuuid == NULL (only verified on outgoing though) std::string source_uuid = (type == "profile-change" || type == "keychange" || type == "verified-change") ? statusmsguuid.valueAsString(0, "uuid") : results_all_messages_from_conversation.valueAsString(j, "sourceUuid"); std::string source_phone = (type == "profile-change" || type == "keychange" || type == "verified-change") ? statusmsguuid.valueAsString(0, "e164") : results_all_messages_from_conversation.valueAsString(j, "sourcephone"); if (source_uuid.empty() || (address = getRecipientIdFromUuidMapped(source_uuid, &recipientmap, createmissingcontacts)) == -1) // try with phone number address = getRecipientIdFromPhoneMapped(source_phone, &recipientmap, createmissingcontacts); if (address == -1) { if (createmissingcontacts) { if ((address = dtCreateRecipient(dtdb->d_database, source_uuid, source_phone, std::string(), databasedir, &recipientmap, createmissingcontacts_valid, &warned_createcontacts)) == -1) { Logger::error("Failed to create contact for incoming group message. Skipping"); continue; } } else { Logger::error("Failed to set address of incoming group message. Skipping"); //std::cout << "Some more info: " << std::endl; //dtdb->d_database.printLineMode("SELECT * from messages WHERE rowid = ?", results_all_messages_from_conversation.value(j, "rowid")); continue; } } } else address = recipientid_for_thread; // message is 1-on-1 or outgoing_group if (address == -1) { Logger::warning("Failed to get recipient id for message partner. Skipping message."); continue; } // PROCESS THE MESSAGE if (type == "call-history") { if (d_verbose) [[unlikely]] Logger::message_start("Dealing with ", type, " message... "); handleDTCallTypeMessage(dtdb->d_database, results_all_messages_from_conversation(j, "callId"), rowid, ttid, address, createmissingcontacts); if (d_verbose) [[unlikely]] Logger::message_end("done"); continue; } else if (type == "group-v2-change") { //if (d_verbose) [[unlikely]] std::cout << "Dealing with " << type << " message... " << std::flush; // this function does nothing (yet?) when istimermessage = false; if (createmissingcontacts && !createmissingcontacts_valid) handleDTGroupChangeMessage(dtdb->d_database, rowid, ttid, address, results_all_messages_from_conversation.valueAsInt(j, "sent_at"), &adjusted_timestamps, &recipientmap, databasedir, false, createmissingcontacts, createmissingcontacts_valid, &warned_createcontacts); else Logger::warnOnce("Unsupported message type 'group-v2-change'. Skipping... (this warning will be shown only once)"); //if (d_verbose) [[unlikely]] std::cout << "done" << std::endl; continue; } else if (type == "group-v1-migration") { // std::cout << bepaald::bold_on << "Warning" << bepaald::bold_off << ": Unsupported message type '" // << results_all_messages_from_conversation.valueAsString(j, "type") << "'. "; // // dtdb->d_database.printLineMode("SELECT json_extract(json, '$.groupMigration.areWeInvited') AS areWeInvited," // // "json_extract(json, '$.groupMigration.invitedMembers') AS invitedMembers," // // "json_extract(json, '$.groupMigration.droppedMemberIds') AS droppedmemberIds" // // " FROM messages WHERE rowid = ?", rowid); // std::cout << "Skipping message." << std::endl; // continue; if (!handleDTGroupV1Migration(dtdb->d_database, rowid, ttid, results_all_messages_from_conversation.getValueAs(j, "sent_at"), recipientid_for_thread, &recipientmap, createmissingcontacts, databasedir, createmissingcontacts_valid, &warned_createcontacts)) return false; } else if (type == "timer-notification" || flags == 2) // type can be also be 'incoming' or empty { //if (d_verbose) [[unlikely]] std::cout << "Dealing with " << type << " message... " << std::flush; //if (d_verbose) [[unlikely]] std::cout << "done" << std::endl; if (isgroupconversation) // in groups these are groupv2updates (not handled (yet)) { // the created groupchange is interpreted by exporthtml correctly, but is not a normal, valid groupchange message // so it shouldnt be used when outputting to a ready-to-restore backup file if (createmissingcontacts && !createmissingcontacts_valid) handleDTGroupChangeMessage(dtdb->d_database, rowid, ttid, address, results_all_messages_from_conversation.valueAsInt(j, "sent_at"), &adjusted_timestamps, &recipientmap, databasedir, true, createmissingcontacts, createmissingcontacts_valid, &warned_createcontacts); else Logger::warnOnce("Unsupported message type 'timer-notification (in group)'. Skipping... (this warning will be shown only once)"); continue; } if (!handleDTExpirationChangeMessage(dtdb->d_database, rowid, ttid, results_all_messages_from_conversation.getValueAs(j, "sent_at"), address)) return false; continue; } else if (flags == 1) // END_SESSION { long long int endsessiontype = Types::SECURE_MESSAGE_BIT | Types::END_SESSION_BIT | Types::PUSH_MESSAGE_BIT | (outgoing ? Types::BASE_SENDING_TYPE : Types::BASE_INBOX_TYPE); if (d_database.containsTable("sms")) { if (!insertRow("sms", {{"thread_id", ttid}, {"date_sent", results_all_messages_from_conversation.value(j, "sent_at")}, {d_sms_date_received, results_all_messages_from_conversation.value(j, "sent_at")}, {"type", endsessiontype}, {d_sms_recipient_id, address}, {"read", 1}})) Logger::error("Inserting session reset into sms"); } else { if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) { if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, results_all_messages_from_conversation.value(j, "sent_at")}, {"date_received", results_all_messages_from_conversation.value(j, "sent_at")}, {d_mms_type, endsessiontype}, {d_mms_recipient_id, address}, {"read", 1}})) Logger::error("Inserting session reset into mms"); } else { // newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so // we try to get the first free date_sent long long int originaldate = results_all_messages_from_conversation.getValueAs(j, "sent_at"); long long int freedate = originaldate; if (!targetisdummy) { freedate = getFreeDateForMessage(originaldate, ttid, Types::isOutgoing(endsessiontype) ? d_selfid : address); if (freedate == -1) { Logger::error("Getting free date for inserting session reset into mms"); continue; } if (originaldate != freedate) adjusted_timestamps[originaldate] = freedate; } if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, freedate}, {"date_received", freedate}, {d_mms_type, endsessiontype}, {d_mms_recipient_id, Types::isOutgoing(endsessiontype) ? d_selfid : address}, {"to_recipient_id", Types::isOutgoing(endsessiontype) ? address : address}, {"read", 1}})) Logger::error("Inserting session reset into mms"); } } continue; } else if (type == "change-number-notification") { if (d_verbose) [[unlikely]] Logger::message_start("Dealing with ", type, " message... "); if (d_database.containsTable("sms")) { if (!insertRow("sms", {{"thread_id", ttid}, {"date_sent", results_all_messages_from_conversation.value(j, "sent_at")}, {d_sms_date_received, results_all_messages_from_conversation.value(j, "sent_at")}, {"type", Types::CHANGE_NUMBER_TYPE}, {"read", 1}, // hardcoded to 1 in Signal Android (for profile-change) {d_sms_recipient_id, address}})) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting number-change into sms"); return false; } } else { if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) { if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, results_all_messages_from_conversation.value(j, "sent_at")}, {"date_received", results_all_messages_from_conversation.value(j, "sent_at")}, {d_mms_type, Types::CHANGE_NUMBER_TYPE}, {d_mms_recipient_id, address}, {d_mms_recipient_device_id, 1}, // not sure what this is but at least for profile-change {"read", 1}})) // it is hardcoded to 1 in Signal Android (as is 'read') { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting number-change into mms"); dtdb->d_database.printLineMode("SELECT * FROM messages WHERE rowid = ?", rowid); return false; } } else { // newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so // we try to get the first free date_sent long long int originaldate = results_all_messages_from_conversation.getValueAs(j, "sent_at"); long long int freedate = originaldate; if (!targetisdummy) { freedate = getFreeDateForMessage(originaldate, ttid, address); if (freedate == -1) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Getting free date for inserting number-change into mms"); continue; } if (originaldate != freedate) adjusted_timestamps[originaldate] = freedate; } if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, freedate}, {"date_received", freedate}, {d_mms_type, Types::CHANGE_NUMBER_TYPE}, {d_mms_recipient_id, address}, {"to_recipient_id", d_selfid}, {d_mms_recipient_device_id, 1}, // not sure what this is but at least for profile-change {"read", 1}})) // it is hardcoded to 1 in Signal Android (as is 'read') { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting number-change into mms"); dtdb->d_database.printLineMode("SELECT * FROM messages WHERE rowid = ?", rowid); return false; } } } if (d_verbose) [[unlikely]] Logger::message_end("done"); continue; } else if (type == "keychange") { if (d_verbose) [[unlikely]] Logger::message_start("Dealing with ", type, " message... "); if (d_database.containsTable("sms")) { if (!insertRow("sms", {{"thread_id", ttid}, {"date_sent", results_all_messages_from_conversation.value(j, "sent_at")}, {d_sms_date_received, results_all_messages_from_conversation.value(j, "sent_at")}, {"type", Types::BASE_INBOX_TYPE | Types::KEY_EXCHANGE_IDENTITY_UPDATE_BIT | Types::PUSH_MESSAGE_BIT}, {"read", 1}, // hardcoded to 1 in Signal Android (for profile-change) {d_sms_recipient_id, address}})) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting keychange into sms"); return false; } } else { if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) { if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, results_all_messages_from_conversation.value(j, "sent_at")}, {"date_received", results_all_messages_from_conversation.value(j, "sent_at")}, {d_mms_type, Types::BASE_INBOX_TYPE | Types::KEY_EXCHANGE_IDENTITY_UPDATE_BIT | Types::PUSH_MESSAGE_BIT}, {d_mms_recipient_id, address}, {d_mms_recipient_device_id, 1}, // not sure what this is but at least for profile-change {"read", 1}})) // it is hardcoded to 1 in Signal Android (as is 'read') { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting keychange into mms"); dtdb->d_database.printLineMode("SELECT * FROM messages WHERE rowid = ?", rowid); return false; } } else { // newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so // we try to get the first free date_sent long long int originaldate = results_all_messages_from_conversation.getValueAs(j, "sent_at"); long long int freedate = originaldate; if (!targetisdummy) { freedate = getFreeDateForMessage(originaldate, ttid, address); if (freedate == -1) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Getting free date for inserting keychange into mms"); continue; } if (originaldate != freedate) adjusted_timestamps[originaldate] = freedate; } if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, freedate}, {"date_received", freedate}, {d_mms_type, Types::BASE_INBOX_TYPE | Types::KEY_EXCHANGE_IDENTITY_UPDATE_BIT | Types::PUSH_MESSAGE_BIT}, {d_mms_recipient_id, address}, {"to_recipient_id", d_selfid}, {d_mms_recipient_device_id, 1}, // not sure what this is but at least for profile-change {"read", 1}})) // it is hardcoded to 1 in Signal Android (as is 'read') { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting keychange into mms"); dtdb->d_database.printLineMode("SELECT * FROM messages WHERE rowid = ?", rowid); return false; } } } if (d_verbose) [[unlikely]] Logger::message_end("done"); continue; } else if (type == "verified-change") { if (d_verbose) [[unlikely]] Logger::message_start("Dealing with ", type, " message... "); SqliteDB::QueryResults identityverification_results; if (!dtdb->d_database.exec("SELECT " "IFNULL(json_extract(json, '$.local'), 0) AS 'local', " "json_extract(json, '$.verified') AS 'verified' " "FROM messages WHERE rowid = ?", rowid, &identityverification_results)) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Failed to query verified-change data. Skipping message"); continue; } // if local == false, it would be an incoming message on Android and // marked as 'You marked your safety number with CONTACT verified from another device' // instead of just 'You marked your safety number with CONTACT verified' //[[maybe_unused]] bool local = identityverification_results.getValueAs(0, "local") == 0 ? false : true; bool verified = identityverification_results.getValueAs(0, "verified") == 0 ? false : true; // not sure if I should do anythng with local... the desktop may have been 'another device', but // who's to say what this android backup we're importing into is... long long int verifytype = Types::PUSH_MESSAGE_BIT | Types::SECURE_MESSAGE_BIT | Types::BASE_SENDING_TYPE | // if (local == false) BASE_INBOX_TYPE (verified ? Types::KEY_EXCHANGE_IDENTITY_VERIFIED_BIT : Types::KEY_EXCHANGE_IDENTITY_DEFAULT_BIT); if (d_database.containsTable("sms")) { if (!insertRow("sms", {{"thread_id", ttid}, {"date_sent", results_all_messages_from_conversation.value(j, "sent_at")}, {d_sms_date_received, results_all_messages_from_conversation.value(j, "sent_at")}, {"type", verifytype}, {"read", 1}, // hardcoded to 1 in Signal Android (for profile-change) {d_sms_recipient_id, address}})) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting verified-change into sms"); return false; } } else { if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) { if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, results_all_messages_from_conversation.value(j, "sent_at")}, {"date_received", results_all_messages_from_conversation.value(j, "sent_at")}, {d_mms_type, verifytype}, {d_mms_recipient_id, address}, {"m_type", 128}, // probably also if (local == false) 132 {"read", 1}})) // hardcoded to 1 in Signal Android { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting verified-change into mms"); return false; } } else { // newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so // we try to get the first free date_sent long long int originaldate = results_all_messages_from_conversation.getValueAs(j, "sent_at"); long long int freedate = originaldate; if (!targetisdummy) { freedate = getFreeDateForMessage(originaldate, ttid, Types::isOutgoing(verifytype) ? d_selfid : address); if (freedate == -1) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Getting free date for inserting verified-change message into mms"); continue; } if (originaldate != freedate) adjusted_timestamps[originaldate] = freedate; } if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, freedate},//results_all_messages_from_conversation.value(j, "sent_at")}, {"date_received", freedate},//results_all_messages_from_conversation.value(j, "sent_at")}, {d_mms_type, verifytype}, {d_mms_recipient_id, Types::isOutgoing(verifytype) ? d_selfid : address}, {"to_recipient_id", Types::isOutgoing(verifytype) ? address : d_selfid}, {"m_type", 128}, // probably also if (local == false) 132 {"read", 1}})) // hardcoded to 1 in Signal Android { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting verified-change into mms"); return false; } } } if (d_verbose) [[unlikely]] Logger::message_end("done"); continue; } else if (type == "profile-change") { if (d_verbose) [[unlikely]] Logger::message_start("Dealing with ", type, " message... "); SqliteDB::QueryResults profilechange_data; if (!dtdb->d_database.exec("SELECT " "json_extract(json, '$.profileChange.type') AS type, " "IFNULL(json_extract(json, '$.profileChange.oldName'), '') AS old_name, " "IFNULL(json_extract(json, '$.profileChange.newName'), '') AS new_name " "FROM messages WHERE rowid = ?", rowid, &profilechange_data)) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Failed to query profile change data. Skipping message"); continue; } //profilechange_data.prettyPrint(); if (profilechange_data.valueAsString(0, "type") != "name") { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::warning("Unsupported message type 'profile-change' (change type: ", profilechange_data.valueAsString(0, "type"), ". Skipping message"); continue; } /* // from app/src/main/proto/Database.proto message ProfileChangeDetails { message StringChange { string previous = 1; string new = 2; } StringChange profileNameChange = 1; } */ std::string previousname = profilechange_data.valueAsString(0, "old_name"); std::string newname = profilechange_data.valueAsString(0, "new_name"); // subobject namechange: ProtoBufParser profilenamechange; profilenamechange.addField<1>(previousname); profilenamechange.addField<2>(newname); // full profilechange object: ProtoBufParser> profchangefull; profchangefull.addField<1>(profilenamechange); if (d_database.containsTable("sms")) { if (!insertRow("sms", {{"thread_id", ttid}, {"date_sent", results_all_messages_from_conversation.value(j, "sent_at")}, {d_sms_date_received, results_all_messages_from_conversation.value(j, "sent_at")}, {"type", Types::PROFILE_CHANGE_TYPE}, {"body", profchangefull.getDataString()}, {"read", 1}, // hardcoded to 1 in Signal Android (for profile-change) {d_sms_recipient_id, address}})) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting profile-change into sms"); return false; } } else { if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) { if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, results_all_messages_from_conversation.value(j, "sent_at")}, {"date_received", results_all_messages_from_conversation.value(j, "sent_at")}, {d_mms_type, Types::PROFILE_CHANGE_TYPE}, {"body", profchangefull.getDataString()}, {d_mms_recipient_id, address}, {d_mms_recipient_device_id, 1}, // not sure what this is but at least for profile-change {"read", 1}})) // it is hardcoded to 1 in Signal Android (as is 'read') { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting profile-change into mms"); return false; } } else { // newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so // we try to get the first free date_sent long long int originaldate = results_all_messages_from_conversation.getValueAs(j, "sent_at"); long long int freedate = originaldate; if (!targetisdummy) { freedate = getFreeDateForMessage(freedate, ttid, Types::isOutgoing(Types::PROFILE_CHANGE_TYPE) ? d_selfid : address); if (freedate == -1) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Getting free date for inserting profile-change into mms"); continue; } if (originaldate != freedate) adjusted_timestamps[originaldate] = freedate; } if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, freedate},//results_all_messages_from_conversation.value(j, "sent_at")}, {"date_received", freedate},//results_all_messages_from_conversation.value(j, "sent_at")}, {d_mms_type, Types::PROFILE_CHANGE_TYPE}, {"body", profchangefull.getDataString()}, {d_mms_recipient_id, Types::isOutgoing(Types::PROFILE_CHANGE_TYPE) ? d_selfid : address}, {"to_recipient_id", Types::isOutgoing(Types::PROFILE_CHANGE_TYPE) ? address : d_selfid}, {d_mms_recipient_device_id, 1}, // not sure what this is but at least for profile-change {"read", 1}})) // it is hardcoded to 1 in Signal Android (as is 'read') { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting profile-change into mms"); return false; } } } if (d_verbose) [[unlikely]] Logger::message_end("done"); continue; } else if (type == "message-request-response-event") { if (d_verbose) [[unlikely]] Logger::message_start("Dealing with ", type, " message... "); /* message MessageRequestResponse { enum Type { UNKNOWN = 0; ACCEPT = 1; DELETE = 2; BLOCK = 3; BLOCK_AND_DELETE = 4; SPAM = 5; BLOCK_AND_SPAM = 6; }*/ std::string request_response(dtdb->d_database.getSingleResultAs("SELECT json_extract(json, '$.messageRequestResponseEvent') FROM messages WHERE rowid = ?", rowid, std::string())); uint64_t response_type = 0; if (request_response == "ACCEPT") response_type = Types::SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED; else if (request_response == "BLOCK") continue; // This type does not leave any actual message in the Android database. else if (request_response == "UNBLOCK") response_type = Types::SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED; // this only actually shows up in Android when done on Desktop, when unblocked on android, it shows no message. //else if (request_response == "DELETE") // ; //else if (request_response == "BLOCK_AND_DELETE") // ; //else if (request_response == "SPAM") // response_type = Types::SPECIAL_TYPE_REPORTED_SPAM; //else if (request_response == "BLOCK_AND_SPAM") // response_type = Types::SPECIAL_TYPE_REPORTED_SPAM; //else if (request_response == "") // ; else { // unupported (yet?) // im not sure any of the other response options are actually saved in the android message table. // possibly a 'SPAM' response gets inserted with the SPECIAL_TYPE_REPORTED_SPAM type, but this is not // confirmed. if (d_verbose) [[unlikely]] Logger::message_end(); Logger::warning("Unsupported message type 'message-request-response-event' WITH " "messageRequestResponseEvent='", request_response, ". Skipping message"); continue; } long long message_request_response_type = response_type | Types::SECURE_MESSAGE_BIT | Types::PUSH_MESSAGE_BIT | (outgoing ? Types::BASE_SENDING_TYPE : Types::BASE_INBOX_TYPE); if (d_database.containsTable("sms")) { if (!insertRow("sms", {{"thread_id", ttid}, {"date_sent", results_all_messages_from_conversation.value(j, "sent_at")}, {d_sms_date_received, results_all_messages_from_conversation.value(j, "sent_at")}, {"type", message_request_response_type}, {"read", 1}, // hardcoded to 1 in Signal Android (for profile-change) {d_sms_recipient_id, address}})) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting ", type, " into sms"); return false; } } else { if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) { if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, results_all_messages_from_conversation.value(j, "sent_at")}, {"date_received", results_all_messages_from_conversation.value(j, "sent_at")}, {d_mms_type, message_request_response_type}, {d_mms_recipient_id, address}, {d_mms_recipient_device_id, 1}, // not sure what this is but at least for profile-change {"read", 1}})) // it is hardcoded to 1 in Signal Android (as is 'read') { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting ", type, " into sms"); dtdb->d_database.printLineMode("SELECT * FROM messages WHERE rowid = ?", rowid); return false; } } else { // newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so // we try to get the first free date_sent long long int originaldate = results_all_messages_from_conversation.getValueAs(j, "sent_at"); long long int freedate = originaldate; if (!targetisdummy) { freedate = getFreeDateForMessage(originaldate, ttid, Types::isOutgoing(message_request_response_type) ? d_selfid : address); if (freedate == -1) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Getting free date for inserting ", type, " into mms"); continue; } if (originaldate != freedate) adjusted_timestamps[originaldate] = freedate; } if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, freedate}, {"date_received", freedate}, {d_mms_type, message_request_response_type}, {d_mms_recipient_id, Types::isOutgoing(message_request_response_type) ? d_selfid : address}, {"to_recipient_id", Types::isOutgoing(message_request_response_type) ? address : d_selfid}, {d_mms_recipient_device_id, 1}, // not sure what this is but at least for profile-change {"read", 1}})) // it is hardcoded to 1 in Signal Android (as is 'read') { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting ", type, " into mms"); dtdb->d_database.printLineMode("SELECT * FROM messages WHERE rowid = ?", rowid); return false; } } } if (d_verbose) [[unlikely]] Logger::message_end("done"); continue; } else if (type.empty()) { Logger::warning("Unsupported message type (empty type, flags = ", flags, "). Skipping..."); /* Most (the only) empty message types I've seen have json$.flags = 2, but that is handled above enum Flags { END_SESSION = 1; EXPIRATION_TIMER_UPDATE = 2; PROFILE_KEY_UPDATE = 4; } */ continue; } else if (!outgoing && !incoming) { if (!d_verbose) [[likely]] Logger::warnOnce("Unhandled message type '" + type + "'. Skipping message. " "(this warning will be shown only once)"); else [[unlikely]] { // get some extra info and show it (threadname, timestamp?) long long int originaldate = results_all_messages_from_conversation.getValueAs(j, "sent_at"); std::string date = "(unknown)"; std::string threadtitle = getNameFromRecipientId(address); if (threadtitle.empty()) threadtitle = "(unknown)"; if (originaldate != -1) date = bepaald::toDateString(originaldate / 1000, "%Y-%m-%d %H:%M:%S"); Logger::warning("Unhandled message type '" + type + "'. Skipping message. Threadtitle: \"", threadtitle, "\", Date: ", date); Logger::warning_indent("Raw message data:"); results_all_messages_from_conversation.printLineMode(j); } continue; } // skip viewonce messages if (results_all_messages_from_conversation.valueHasType(j, "isViewOnce") != 0 && results_all_messages_from_conversation.getValueAs(j, "isViewOnce") != 0 && !createmissingcontacts) continue; std::string shared_contacts_json; if (hassharedcontact) { shared_contacts_json = dtSetSharedContactsJsonString(dtdb->d_database, rowid); //std::cout << shared_contacts_json << std::endl; //warnOnce("Message is 'contact share'. This is not yet supported, skipping..."); //continue; } // get emoji reactions if (d_verbose) [[unlikely]] Logger::message_start("Handling reactions..."); std::vector> reactions; getDTReactions(dtdb->d_database, rowid, numreactions, &reactions); if (d_verbose) [[unlikely]] Logger::message_end("done"); // LONG_TEXT messages (> 2000 bytes) are sent with an attachment in android (not on desktop) std::string msgbody = results_all_messages_from_conversation(j, "body"); std::string msgbody_full; if (utf8Chars(msgbody) > 2000) { msgbody_full = msgbody; resizeToNUtf8Chars(msgbody, 2000); } // insert the collected data in the correct tables if (!d_database.containsTable("sms") || // starting at dbv168, the sms table is removed altogether (numattachments > 0 || nummentions > 0 || hasquote || (isgroupconversation && outgoing))) // this goes in mms table on older database versions { // get quote stuff // if message has quote attachments, find the original message (the quote json does not contain all info) long long int mmsquote_id = 0; std::string mmsquote_author_uuid; long long int mmsquote_author = -1; std::any mmsquote_body; //long long int mmsquote_attachment = -1; // always -1??? long long int mmsquote_missing = 0; std::pair, size_t> mmsquote_mentions{nullptr, 0}; long long int mmsquote_type = 0; // 0 == NORMAL, 1 == GIFT_BADGE (src/main/java/org/thoughtcrime/securesms/mms/QuoteModel.java) if (hasquote) { if (d_verbose) [[unlikely]] Logger::message_start("Gathering quote data..."); //std::cout << " Message has quote" << std::endl; SqliteDB::QueryResults quote_results; if (!dtdb->d_database.exec("SELECT " "json_extract(messages.json, '$.quote.id') AS quote_id," "json_extract(messages.json, '$.quote.author') AS quote_author_phone," // in old databases, authorUuid does not exist, but this holds the phone number "conversations." + d_dt_c_uuid + " AS quote_author_uuid_from_phone," // this is filled from a left join on the possible phone number above "LOWER(json_extract(messages.json, '$.quote.authorAci')) AS quote_author_aci," // in newer databases, this replaces the 'authorUuid' "LOWER(json_extract(messages.json, '$.quote.authorUuid')) AS quote_author_uuid," "json_extract(messages.json, '$.quote.text') AS quote_text," "IFNULL(json_array_length(messages.json, '$.quote.attachments'), 0) AS num_quote_attachments," "IFNULL(json_array_length(messages.json, '$.quote.bodyRanges'), 0) AS num_quote_bodyranges," "IFNULL(json_extract(messages.json, '$.quote.type'), 0) AS quote_type," "IFNULL(json_extract(messages.json, '$.quote.referencedMessageNotFound'), 0) AS quote_referencedmessagenotfound," "IFNULL(json_extract(messages.json, '$.quote.isGiftBadge'), 0) AS quote_isgiftbadge," // if null because it probably does not exist in older databases "IFNULL(json_extract(messages.json, '$.quote.isViewOnce'), 0) AS quote_isviewonce" " FROM messages " "LEFT JOIN conversations ON json_extract(messages.json, '$.quote.author') = conversations.e164 " "WHERE messages.rowid = ?", rowid, "e_results)) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Quote error msg"); } // try to set quote author from uuid or phone mmsquote_author_uuid = quote_results.valueAsString(0, "quote_author_aci"); if (mmsquote_author_uuid.empty()) // possibly older database, try authorUuid mmsquote_author_uuid = quote_results.valueAsString(0, "quote_author_uuid"); if (mmsquote_author_uuid.empty()) // possibly old database, try conversations.uuid mmsquote_author_uuid = quote_results.valueAsString(0, "quote_author_uuid_from_phone"); if (mmsquote_author_uuid.empty()) // failed to get uuid from desktopdatabase, try matching on phone number mmsquote_author = getRecipientIdFromPhoneMapped(quote_results.valueAsString(0, "quote_author_phone"), &recipientmap, createmissingcontacts); else { mmsquote_author = getRecipientIdFromUuidMapped(mmsquote_author_uuid, &recipientmap, createmissingcontacts); if (mmsquote_author == -1 && (createmissingcontacts || createmissingcontacts_valid)) mmsquote_author = dtCreateRecipient(dtdb->d_database, mmsquote_author_uuid, quote_results.valueAsString(0, "quote_author_phone"), std::string(), databasedir, &recipientmap, createmissingcontacts_valid, &warned_createcontacts); } if (mmsquote_author == -1) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::warning("Failed to find quote author. skipping"); // DEBUG Logger::message("Additional info:"); dtdb->d_database.print("SELECT json_extract(json, '$.quote') FROM messages WHERE rowid = ?", rowid); hasquote = false; } mmsquote_body = quote_results.valueAsString(0, "quote_text"); // check if this can be null (if quote exists, dont think so) mmsquote_missing = (quote_results.getValueAs(0, "quote_referencedmessagenotfound") == false ? 0 : 1); mmsquote_type = (quote_results.getValueAs(0, "quote_isgiftbadge") == false ? 0 : 1); if (quote_results.valueHasType(0, "quote_id")) mmsquote_id = quote_results.getValueAs(0, "quote_id"); // this is the messages.json.$timestamp or messages.sent_at. In the android // db, it should be mms.date, but this should be set by this import anyway // *EDIT* since there is a unique constraint on mms.date, the quoted message's // date may have been adjusted!!! This needs work else // type is string mmsquote_id = bepaald::toNumber(quote_results.valueAsString(0, "quote_id")); if (quote_results.getValueAs(0, "num_quote_bodyranges") > 0) { // HEX(quote_mentions) = 0A2A080A10011A2439333732323237332D373865332D343133362D383634302D633832363139363937313463 // PROTOBUF // Field #1: 0A String Length = 42, Hex = 2A, UTF8 = " $93722273-7 ..." (total 42 chars) // As sub-object : // Field #1: 08 Varint Value = 10, Hex = 0A // Field #2: 10 Varint Value = 1, Hex = 01 // Field #3: 1A String Length = 36, Hex = 24, UTF8 = "93722273-78e3-41 ..." (total 36 chars) // // (actual mention) // _id = 3 // thread_id = 39 // message_id = 4584 // recipient_id = 71 (= 93722273-78e3-41 ...) // range_start = 10 // range_length = 1 // // protospec (app/src/main/proto/Database.proto): // message BodyRangeList { // message BodyRange { // enum Style { // BOLD = 0; // ITALIC = 1; // } // // message Button { // string label = 1; // string action = 2; // } // // int32 start = 1; // int32 length = 2; // // oneof associatedValue { // string mentionUuid = 3; // Style style = 4; // string link = 5; // Button button = 6; // } // } // repeated BodyRange ranges = 1; // } BodyRanges bodyrangelist; for (unsigned int qbr = 0; qbr < quote_results.getValueAs(0, "num_quote_bodyranges"); ++qbr) { std::string qbr_uuid; SqliteDB::QueryResults qbrres; if (!dtdb->d_database.exec("SELECT " "json_extract(json, '$.quote.bodyRanges[" + bepaald::toString(qbr) + "].start') AS qbr_start," "json_extract(json, '$.quote.bodyRanges[" + bepaald::toString(qbr) + "].length') AS qbr_length," "json_extract(json, '$.quote.bodyRanges[" + bepaald::toString(qbr) + "].style') AS qbr_style," "LOWER(COALESCE(json_extract(json, '$.quote.bodyRanges[" + bepaald::toString(qbr) + "].mentionAci'), json_extract(json, '$.quote.bodyRanges[" + bepaald::toString(qbr) + "].mentionUuid'))) AS qbr_uuid " "FROM messages WHERE rowid = ?", rowid, &qbrres)) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Retrieving quote bodyranges"); continue; } //qbrres.prettyPrint(); if (qbrres.isNull(0, "qbr_style")) { if (qbrres.isNull(0, "qbr_uuid")) [[unlikely]] // if style = null, this must be a mention { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::warning("Quote-bodyrange contains no recipient and no style. Skipping."); dtdb->d_database.prettyPrint(d_truncate, "SELECT json_extract(json, '$.quote.bodyRanges[" + bepaald::toString(qbr) + "] FROM messages WHERE rowid = ?", rowid); continue; } long long int rec_id = getRecipientIdFromUuidMapped(qbrres.valueAsString(0, "qbr_uuid"), &recipientmap, createmissingcontacts); if (rec_id == -1) { if (createmissingcontacts) { if ((rec_id = dtCreateRecipient(dtdb->d_database, qbrres.valueAsString(0, "qbr_uuid"), std::string(), std::string(), databasedir, &recipientmap, createmissingcontacts_valid, &warned_createcontacts)) == -1) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::warning("Failed to create recipient for quote-mention. Skipping."); continue; } } else { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::warning("Failed to find recipient for quote-mention. Skipping."); continue; } } qbr_uuid = d_database.getSingleResultAs("SELECT " + d_recipient_aci + " FROM recipient WHERE _id = ?", rec_id, std::string()); } BodyRange bodyrange; bodyrange.addField<1>(qbrres.getValueAs(0, "qbr_start")); bodyrange.addField<2>(qbrres.getValueAs(0, "qbr_length")); if (qbrres.isNull(0, "qbr_style")) bodyrange.addField<3>(qbr_uuid); else bodyrange.addField<4>(qbrres.getValueAs(0, "qbr_style") - 1); // NOTE desktop style enum starts at 1 (android at 0) bodyrangelist.addField<1>(bodyrange); } #if __cpp_lib_smart_ptr_for_overwrite >= 202002L mmsquote_mentions.first = std::make_shared_for_overwrite(bodyrangelist.size()); #elif __cpp_lib_shared_ptr_arrays >= 201707L mmsquote_mentions.first = std::make_shared(bodyrangelist.size()); #else mmsquote_mentions.first = std::shared_ptr(new unsigned char[bodyrangelist.size()], [](unsigned char *p) { delete[] p; } ); #endif mmsquote_mentions.second = bodyrangelist.size(); std::memcpy(mmsquote_mentions.first.get(), bodyrangelist.data(), bodyrangelist.size()); } //"mms.quote_attachment,"// = -1 Always -1?? //quote_results.prettyPrint(); if (d_verbose) [[unlikely]] Logger::message_end("done"); } if (d_verbose) [[unlikely]] Logger::message_start("Inserting message..."); std::any retval; if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) { if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, results_all_messages_from_conversation.value(j, "sent_at")}, {"date_received", results_all_messages_from_conversation.value(j, "sent_at")}, {"date_server", results_all_messages_from_conversation.value(j, "sent_at")}, {d_mms_type, Types::SECURE_MESSAGE_BIT | Types::PUSH_MESSAGE_BIT | (incoming ? Types::BASE_INBOX_TYPE : Types::BASE_SENT_TYPE)}, {"body", msgbody}, {"read", 1}, // defaults to 0, but causes tons of unread message notifications //{"delivery_receipt_count", (incoming ? 0 : 0)}, // set later in setMessagedeliveryreceipts() //{"read_receipt_count", (incoming ? 0 : 0)}, // "" "" {d_mms_recipient_id, address}, {"m_type", incoming ? 132 : 128}, // dont know what this is, but these are the values... {"quote_id", hasquote ? (bepaald::contains(adjusted_timestamps, mmsquote_id) ? adjusted_timestamps[mmsquote_id] : mmsquote_id) : 0}, {"quote_author", hasquote ? std::any(mmsquote_author) : std::any(nullptr)}, {"quote_body", hasquote ? mmsquote_body : nullptr}, //{"quote_attachment", hasquote ? mmsquote_attachment : -1}, // removed since dbv166 so probably not important, was always -1 before {"quote_missing", hasquote ? mmsquote_missing : 0}, {"quote_mentions", hasquote ? std::any(mmsquote_mentions) : std::any(nullptr)}, {"shared_contacts", shared_contacts_json.empty() ? std::any(nullptr) : std::any(shared_contacts_json)}, {"remote_deleted", results_all_messages_from_conversation.value(j, "isErased")}, {((!results_all_messages_from_conversation.isNull(j, "expireTimer") && results_all_messages_from_conversation.valueAsInt(j, "expireTimer", 0) != 0) ? "expires_in" : ""), results_all_messages_from_conversation.valueAsInt(j, "expireTimer", 0) * 1000}, {"view_once", results_all_messages_from_conversation.value(j, "isViewOnce")}, // if !createrecipient -> this message was already skipped {"quote_type", hasquote ? mmsquote_type : 0}}, "_id", &retval)) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting into mms"); return false; } } else { // newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so // we try to get the first free date_sent long long int originaldate = results_all_messages_from_conversation.getValueAs(j, "sent_at"); long long int freedate = originaldate; if (!targetisdummy) { freedate = getFreeDateForMessage(originaldate, ttid, incoming ? address : d_selfid); if (freedate == -1) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Getting free date for inserting message into mms"); continue; } if (originaldate != freedate) adjusted_timestamps[originaldate] = freedate; } if (!insertRow(d_mms_table, {{"thread_id", ttid}, {d_mms_date_sent, freedate},//results_all_messages_from_conversation.value(j, "sent_at")}, {"date_received", freedate},//results_all_messages_from_conversation.value(j, "sent_at")}, {"date_server", results_all_messages_from_conversation.value(j, "sent_at")}, {d_mms_type, Types::SECURE_MESSAGE_BIT | Types::PUSH_MESSAGE_BIT | (incoming ? Types::BASE_INBOX_TYPE : Types::BASE_SENT_TYPE)}, {"body", msgbody}, {"read", 1}, // defaults to 0, but causes tons of unread message notifications //{"delivery_receipt_count", (incoming ? 0 : 0)}, // set later in setMessagedeliveryreceipts() //{"read_receipt_count", (incoming ? 0 : 0)}, // "" "" {d_mms_recipient_id, incoming ? address : d_selfid}, {"to_recipient_id", incoming ? d_selfid : address}, {"m_type", incoming ? 132 : 128}, // dont know what this is, but these are the values... {"quote_id", hasquote ? (bepaald::contains(adjusted_timestamps, mmsquote_id) ? adjusted_timestamps[mmsquote_id] : mmsquote_id) : 0}, {"quote_author", hasquote ? std::any(mmsquote_author) : std::any(nullptr)}, {"quote_body", hasquote ? mmsquote_body : nullptr}, //{"quote_attachment", hasquote ? mmsquote_attachment : -1}, // removed since dbv166 so probably not important, was always -1 before {"quote_missing", hasquote ? mmsquote_missing : 0}, {"quote_mentions", hasquote ? std::any(mmsquote_mentions) : std::any(nullptr)}, {"shared_contacts", shared_contacts_json.empty() ? std::any(nullptr) : std::any(shared_contacts_json)}, {"remote_deleted", results_all_messages_from_conversation.value(j, "isErased")}, {((!results_all_messages_from_conversation.isNull(j, "expireTimer") && results_all_messages_from_conversation.valueAsInt(j, "expireTimer", 0) != 0) ? "expires_in" : ""), results_all_messages_from_conversation.valueAsInt(j, "expireTimer", 0) * 1000}, {"view_once", results_all_messages_from_conversation.value(j, "isViewOnce")}, // if !createrecipient -> this message was already skipped {"quote_type", hasquote ? mmsquote_type : 0}}, "_id", &retval)) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting into mms"); return false; } } //std::cout << "Raw any_cast 2" << std::endl; long long int new_mms_id = std::any_cast(retval); if (d_verbose) [[unlikely]] Logger::message_end("done"); // add ranges if present // note 'bodyRanges' on desktop is also used for mentions, // in that case a range is {start, end, mentionUuid}, instead of {start, end, style}. // (so style == NULL) these must be skipped here. if (hasranges) { //dtdb->d_database.prettyPrint("SELECT json_extract(json, '$.bodyRanges') FROM messages WHERE rowid IS ?", rowid); BodyRanges bodyrangelist; SqliteDB::QueryResults ranges_results; for (unsigned int r = 0; r < hasranges; ++r) { if (dtdb->d_database.exec("SELECT " "json_extract(json, '$.bodyRanges[" + bepaald::toString(r) + "].start') AS range_start," "json_extract(json, '$.bodyRanges[" + bepaald::toString(r) + "].length') AS range_length," "json_extract(json, '$.bodyRanges[" + bepaald::toString(r) + "].style') AS range_style" " FROM messages WHERE rowid IS ?", rowid, &ranges_results)) { if (ranges_results.isNull(0, "range_style")) continue; //ranges_results.prettyPrint(); BodyRange bodyrange; if (ranges_results.getValueAs(0, "range_start") != 0) bodyrange.addField<1>(ranges_results.getValueAs(0, "range_start")); bodyrange.addField<2>(ranges_results.getValueAs(0, "range_length")); bodyrange.addField<4>(ranges_results.getValueAs(0, "range_style") - 1); // NOTE desktop style enum starts at 1 (android 0) bodyrangelist.addField<1>(bodyrange); } } if (bodyrangelist.size() && d_database.tableContainsColumn(d_mms_table, d_mms_ranges)) { std::pair bodyrangesdata(bodyrangelist.data(), bodyrangelist.size()); d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_ranges + " = ? WHERE rowid = ?", {bodyrangesdata, new_mms_id}); } } // insert message attachments if (d_verbose) [[unlikely]] Logger::message_start("Inserting attachments..."); dtInsertAttachments(new_mms_id, results_all_messages_from_conversation.getValueAs(j, "sent_at"), numattachments, haspreview, rowid, dtdb->d_database, "WHERE rowid = " + bepaald::toString(rowid), databasedir, false, issticker, targetisdummy); if (hasquote && !mmsquote_missing) { // insert quotes attachments dtInsertAttachments(new_mms_id, results_all_messages_from_conversation.getValueAs(j, "sent_at"), -1, 0, rowid, dtdb->d_database, //"WHERE (sent_at = " + bepaald::toString(mmsquote_id) + " AND sourceUuid = '" + mmsquote_author_uuid + "')", databasedir, true); // sourceUuid IS NULL if sent from desktop "WHERE JSONLONG(sent_at) = " + bepaald::toString(mmsquote_id), databasedir, true, false /*issticker, not in quotes right now, need to test that*/, targetisdummy); } if (d_verbose) [[unlikely]] Logger::message_end("done"); // insert LONG_TEXT attachment if (!msgbody_full.empty()) dtImportLongText(msgbody_full, new_mms_id, results_all_messages_from_conversation.getValueAs(j, "sent_at")); if (outgoing) dtSetMessageDeliveryReceipts(dtdb->d_database, rowid, &recipientmap, databasedir, createmissingcontacts, new_mms_id, true/*mms*/, isgroupconversation, createmissingcontacts_valid, &warned_createcontacts); // insert into reactions if (d_verbose) [[unlikely]] Logger::message_start("Inserting reactions..."); dtInsertReactions(dtdb->d_database, new_mms_id, reactions, true, &recipientmap, databasedir, createmissingcontacts, createmissingcontacts_valid); if (d_verbose) [[unlikely]] Logger::message_end("done"); // insert into mentions if (d_verbose) [[unlikely]] Logger::message_start("Inserting mentions..."); for (unsigned int k = 0; k < nummentions; ++k) { SqliteDB::QueryResults results_mentions; if (!dtdb->d_database.exec("SELECT " "json_extract(json, '$.bodyRanges[" + bepaald::toString(k) + "].start') AS start," "json_extract(json, '$.bodyRanges[" + bepaald::toString(k) + "].length') AS length," "LOWER(COALESCE(json_extract(json, '$.bodyRanges[" + bepaald::toString(k) + "].mentionAci'), json_extract(json, '$.bodyRanges[" + bepaald::toString(k) + "].mentionUuid'))) AS mention_uuid" " FROM messages WHERE rowid = ?", rowid, &results_mentions)) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::warning("Failed to retrieve mentions. Skipping."); continue; } //std::cout << " Mention " << k + 1 << "/" << nummentions << std::endl; // NOTE Desktop uses the same bodyRanges field for styling {start,length,style} and mentions {start,length,mentionUuid}. // if this is a style, mentionUuid will not exist, and we should skip it. if (results_mentions.isNull(0, "mention_uuid")) continue; long long int rec_id = getRecipientIdFromUuidMapped(results_mentions.valueAsString(0, "mention_uuid"), &recipientmap, createmissingcontacts); if (rec_id == -1) { if (createmissingcontacts) { if ((rec_id = dtCreateRecipient(dtdb->d_database, results_mentions("mention_uuid"), std::string(), std::string(), databasedir, &recipientmap, createmissingcontacts_valid, &warned_createcontacts)) == -1) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::warning("Failed to create recipient for mention. Skipping."); continue; } } else { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::warning("Failed to find recipient for mention. Skipping."); continue; } } if (!insertRow("mention", {{"thread_id", ttid}, {"message_id", new_mms_id}, {"recipient_id", rec_id}, {"range_start", results_mentions.getValueAs(0, "start")}, {"range_length", results_mentions.getValueAs(0, "length")}})) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Inserting into mention"); } //else // std::cout << " Inserted mention" << std::endl; } if (d_verbose) [[unlikely]] Logger::message_end("done"); } else // database contains sms-table and message has no attachment/quote/mention and is not group { // insert into sms std::any retval; if (!insertRow("sms", {{"thread_id", ttid}, {d_sms_recipient_id, address}, {d_sms_date_received, results_all_messages_from_conversation.getValueAs(j, "sent_at")}, {"date_sent", results_all_messages_from_conversation.getValueAs(j, "sent_at")}, {"date_server", results_all_messages_from_conversation.getValueAs(j, "sent_at")}, {"type", Types::SECURE_MESSAGE_BIT | Types::PUSH_MESSAGE_BIT | (incoming ? Types::BASE_INBOX_TYPE : Types::BASE_SENT_TYPE)}, {"body", results_all_messages_from_conversation.value(j, "body")}, {"read", 1}, //{"delivery_receipt_count", (incoming ? 0 : 0)}, // set later in setMessagedeliveryreceipts() //{"read_receipt_count", (incoming ? 0 : 0)}, // "" "" {"remote_deleted", results_all_messages_from_conversation.value(j, "isErased")}, {((!results_all_messages_from_conversation.isNull(j, "expireTimer") && results_all_messages_from_conversation.valueAsInt(j, "expireTimer", 0) != 0) ? "expires_in" : ""), results_all_messages_from_conversation.valueAsInt(j, "expireTimer", 0) * 1000}, {"server_guid", results_all_messages_from_conversation.value(j, "serverGuid")}}, "_id", &retval)) { Logger::error("Inserting into sms"); continue; } //std::cout << "Raw any_cast 3" << std::endl; long long int new_sms_id = std::any_cast(retval); //std::cout << " Inserted sms message, new id: " << new_sms_id << std::endl; // set delivery/read counts if (outgoing) dtSetMessageDeliveryReceipts(dtdb->d_database, rowid, &recipientmap, databasedir, createmissingcontacts, new_sms_id, false/*mms*/, isgroupconversation, createmissingcontacts_valid, &warned_createcontacts); // insert into reactions dtInsertReactions(dtdb->d_database, new_sms_id, reactions, false, &recipientmap, databasedir, createmissingcontacts, createmissingcontacts_valid); } } //updateThreadsEntries(ttid); } for (auto const &r : recipientmap) { //std::cout << "Recpients in map: " << r.first << " : " << r.second << std::endl; long long int profile_date_desktop = dtdb->d_database.getSingleResultAs("SELECT profileLastFetchedAt FROM conversations WHERE " + d_dt_c_uuid + " = ?1 OR groupId = ?1 OR e164 = ?1", r.first, 0); long long int profile_date_android = d_database.getSingleResultAs("SELECT last_profile_fetch FROM recipient WHERE _id = ?", r.second, 0); //std::cout << "Profile update? : " << r.first << " " << profile_date_desktop << " " << profile_date_android << std::endl; if (profile_date_desktop > profile_date_android) { //std::cout << "Need to update profile!" << std::endl; // update profile from desktop. if (d_verbose) [[unlikely]] Logger::message("Attempting to update profile"); if (!dtUpdateProfile(dtdb->d_database, r.first, r.second, databasedir)) Logger::warning("Failed to update profile data."); } } if (importstickers) { Logger::message("Importing installed stickerpacks"); if (!dtImportStickerPacks(dtdb->d_database, databasedir)) { Logger::error("Failed to import stickers"); return false; } } if (!skipmessagereorder) [[likely]] reorderMmsSmsIds(); updateThreadsEntries(); // check if identitykeys are all not-NULL if (createmissingcontacts_valid) { long long int null_keys = d_database.getSingleResultAs("SELECT COUNT(*) FROM identities WHERE identity_key IS NULL", -1); if (null_keys == -1) [[unlikely]] Logger::warning("Failed to get count of NULL identity_keys"); else if (null_keys == 0) Logger::message("All identity_keys appear OK"); else Logger::warning("Found ", null_keys, " NULL identity_keys. This will likely cause problems when restoring the backup."); } return checkDbIntegrity(); } /* EXAMPLE DESKTOP DB: rowid = 56 id = 845bff95-[...]-4b53efcba27b json = {"timestamp":1643874290360, "attachments":[{"contentType":"application/pdf","fileName":"qrcode.pdf","path":"21/21561db325667446c84702bc2af2cb779aaaeb32c6b3d190d41f86d12e8bf5f0","size":38749,"pending":false,"url":"/home/svandijk/.config/Signal/drafts.noindex/4b/4bb11cd1be7c718ae8ed57dc28f34d57a1032d4ab0595128527466e876ddde9d"}], "type":"outgoing", "body":"qrcode", "conversationId":"d6b93b26-[...]-b949d4de0aba", "preview":[], "sent_at":1643874290360, "received_at":1623335267006, "received_at_ms":1643874290360, "recipients":["93722273-[...]-c8261969714c"], "bodyRanges":[], "sendHQImages":false, "sendStateByConversationId":{"d6b93b26-[...]-b949d4de0aba":{"status":"Delivered","updatedAt":1643874294845}, "87e8067b-[...]-011b5c5ee23a":{"status":"Sent","updatedAt":1643874291830}}, "schemaVersion":10, "hasAttachments":1, "hasFileAttachments":1, "contact":[], "destination":"93722273-[...]-c8261969714c", "id":"845bff95-[...]-4b53efcba27b", "readStatus":0, "expirationStartTimestamp":1643874291546, "unidentifiedDeliveries":["93722273-[...]-c8261969714c"], "errors":[], "synced":true} readStatus = 0 expires_at = sent_at = 1643874290360 schemaVersion = 10 conversationId = d6b93b26-[...]-b949d4de0aba received_at = 1623335267006 source = deprecatedSourceDevice = hasAttachments = 1 hasFileAttachments = 1 hasVisualMediaAttachments = 0 expireTimer = expirationStartTimestamp = 1643874291546 type = outgoing body = qrcode messageTimer = messageTimerStart = messageTimerExpiresAt = isErased = 0 isViewOnce = 0 sourceUuid = serverGuid = expiresAt = sourceDevice = storyId = isStory = 0 isChangeCreatedByUs = 0 shouldAffectActivity = 1 shouldAffectPreview = 1 isUserInitiatedMessage = 1 isTimerChangeFromSync = 0 isGroupLeaveEvent = 0 isGroupLeaveEventFromOther = 0 ANDROID DB: _id = 631 thread_id = 1 date = 1643874290360 date_received = 1643874294496 date_server = -1 msg_box = 10485783 read = 1 body = qrcode part_count = 1 ct_l = address = 2 address_device_id = exp = m_type = 128 m_size = st = tr_id = delivery_receipt_count = 1 mismatched_identities = network_failures = subscription_id = -1 expires_in = 0 expire_started = 0 notified = 0 read_receipt_count = 0 quote_id = 0 quote_author = quote_body = quote_attachment = -1 quote_missing = 0 quote_mentions = shared_contacts = unidentified = 1 previews = reveal_duration = 0 reactions = reactions_unread = 0 reactions_last_seen = -1 remote_deleted = 0 mentions_self = 0 notified_timestamp = 0 viewed_receipt_count = 0 server_guid = receipt_timestamp = 1643874295302 ranges = is_story = 0 parent_story_id = 0 */ signalbackup-tools-20250313-1/signalbackup/importfromplaintextbackup.cc000066400000000000000000000633531476450434500262270ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../signalplaintextbackupdatabase/signalplaintextbackupdatabase.h" #include "../signalplaintextbackupattachmentreader/signalplaintextbackupattachmentreader.h" #include "../msgtypes/msgtypes.h" bool SignalBackup::importFromPlaintextBackup(std::unique_ptr const &ptdb, bool skipmessagereorder, std::vector> const &initial_contactmap, std::vector const &daterangelist, std::vector const &chats, bool createmissingcontacts, bool markdelivered, bool markread, bool autodates, std::string const &selfphone, bool isdummy) { if (d_selfid == -1) { if (isdummy && !selfphone.empty()) // lets just create a recipient { d_selfid = d_database.getSingleResultAs("SELECT _id FROM recipient WHERE " + d_recipient_e164 + " = ?", selfphone, -1); if (d_selfid == -1) { // it is possible the contactname is set for this contact in ptdb through --mapxmlcontactnames std::string contact_name = ptdb->d_database.getSingleResultAs("SELECT MAX(contact_name) FROM smses WHERE address = ? " "AND contact_name IS NOT NULL AND contact_name IS NOT ''", selfphone, std::string()); std::any new_rid; if (!insertRow("recipient", {{d_recipient_e164, selfphone}, {(contact_name.empty() ? "" : "profile_given_name"), contact_name}, {(contact_name.empty() ? "" : "profile_joined_name"), contact_name}}, "_id", &new_rid)) return false; d_selfid = std::any_cast(new_rid); } } else { d_selfid = selfphone.empty() ? scanSelf() : d_database.getSingleResultAs("SELECT _id FROM recipient WHERE " + d_recipient_e164 + " = ?", selfphone, -1); if (d_selfid == -1) { Logger::error_start("Failed to determine id of 'self'."); if (selfphone.empty()) Logger::message_start(" Please pass `--setselfid \"[phone]\"' to set it manually"); Logger::message_end(); return false; } if (d_selfuuid.empty()) d_selfuuid = bepaald::toLower(d_database.getSingleResultAs("SELECT " + d_recipient_aci + " FROM recipient WHERE _id = ?", d_selfid, std::string())); } } if (!ptdb->ok()) { Logger::error("Failed to open Signal Plaintext Backup database"); return false; } // mms messages seem to usually omit the self-address, it is implied to be the sourceaddress when the message is outgoing, // or (one of the) targetaddresses when the message is incoming. std::string selfe164(selfphone); if (selfe164.empty()) selfe164 = d_database.getSingleResultAs("SELECT " + d_recipient_e164 + " FROM recipient WHERE _id = ?", d_selfid, std::string()); if (selfe164.empty()) Logger::warning("Failed to get self phone"); else { //ptdb->d_database.prettyPrint(false, "SELECT DISTINCT sourceaddress FROM smses WHERE ismms = 1 AND type = 2"); // set source address to self for outgoing ptdb->d_database.exec("UPDATE smses SET sourceaddress = ? WHERE " "(NULLIF(sourceaddress, '') IS NULL OR sourceaddress = 'insert-address-token') AND " "ismms = 1 AND " "type = 2", selfe164); //ptdb->d_database.prettyPrint(false, "SELECT DISTINCT sourceaddress FROM smses WHERE ismms = 1 AND type = 2"); //ptdb->d_database.prettyPrint(false, "SELECT DISTINCT targetaddresses FROM smses WHERE ismms = 1 AND type = 1"); // set target address: ptdb->d_database.exec("UPDATE smses SET targetaddresses = " "CASE " " WHEN targetaddresses IS NULL THEN json_array(?1)" " ELSE json_insert(targetaddresses, '$[#]', ?1) " "END " "WHERE ismms = 1 AND type = 1", selfe164); //ptdb->d_database.prettyPrint(false, "SELECT DISTINCT targetaddresses FROM smses WHERE ismms = 1 AND type = 1"); } std::vector> dateranges; if (daterangelist.size() % 2 == 0) for (unsigned int i = 0; i < daterangelist.size(); i += 2) dateranges.push_back({daterangelist[i], daterangelist[i + 1]}); // set daterange automatically if (dateranges.empty() && autodates) { SqliteDB::QueryResults res; if ((d_database.containsTable("sms") && !d_database.exec("SELECT MIN(mindate) FROM (SELECT MIN(sms." + d_sms_date_received + ", " + d_mms_table + ".date_received) AS mindate FROM sms " "LEFT JOIN " + d_mms_table + " WHERE sms." + d_sms_date_received + " IS NOT NULL AND " + d_mms_table + ".date_received IS NOT NULL)", &res)) || (!d_database.containsTable("sms") && !d_database.exec("SELECT MIN(" + d_mms_table + ".date_received) AS mindate, MAX(" + d_mms_table + ".date_received) AS maxdate FROM " + d_mms_table + " WHERE " + d_mms_table + ".date_received IS NOT NULL", &res))) { Logger::error("Failed to automatically determine data-range"); return false; } dateranges.push_back({"0", res.valueAsString(0, "mindate")}); dateranges.push_back({res.valueAsString(0, "maxdate"), bepaald::toString(std::numeric_limits::max())}); } std::string datewhereclause; for (unsigned int i = 0; i < dateranges.size(); ++i) { bool needrounding = false; long long int startrange = dateToMSecsSinceEpoch(dateranges[i].first); long long int endrange = dateToMSecsSinceEpoch(dateranges[i].second, &needrounding); if (startrange == -1 || endrange == -1 || endrange < startrange) { Logger::error("Skipping range: '", dateranges[i].first, " - ", dateranges[i].second, "'. Failed to parse or invalid range."); continue; } Logger::message(" Using range: ", dateranges[i].first, " - ", dateranges[i].second, " (", startrange, " - ", endrange, ")"); if (needrounding)// if called with "YYYY-MM-DD HH:MM:SS" endrange += 999; // to get everything in the second specified... datewhereclause += (datewhereclause.empty() ? " WHERE (" : " OR ") + "date BETWEEN "s + bepaald::toString(startrange) + " AND " + bepaald::toString(endrange); if (i == dateranges.size() - 1) datewhereclause += ')'; } std::string chatselectionclause; if (!chats.empty()) { chatselectionclause += (datewhereclause.empty() ? " WHERE address IN (" : " AND address IN ("); for (unsigned int i = 0; i < chats.size(); ++i) chatselectionclause += "'" + chats[i] + (i < chats.size() - 1 ? "', " : "')"); } /* contactmap: SELECT address,max(contact_name) FROM smses GROUP BY address ORDER BY address; */ std::map contactmap(initial_contactmap.begin(), initial_contactmap.end()); // fill contactmap with known recipients: { SqliteDB::QueryResults recipient_results; if (d_database.exec("SELECT _id, " + d_recipient_e164 + " FROM recipient", &recipient_results)) for (unsigned int i = 0; i < recipient_results.rows(); ++i) contactmap.emplace(recipient_results(i, "e164"), recipient_results.valueAsInt(i, "_id")); } /* threadmap */ std::map threadmap; // READ always seems to be 1.... //ptdb->d_database.prettyPrint(true, "SELECT DISTINCT type, read FROM smses"); // in signal android backup, all messages are read as well, only old versions of edited are not... //ptdb->d_database.saveToFile("plainttext.sqlite"); SqliteDB::QueryResults pt_messages; if (!ptdb->d_database.exec("SELECT " "rowid, " "date, type, read, body, contact_name, address, numattachments, COALESCE(sourceaddress, address) AS sourceaddress, ismms, skip " "FROM smses" + datewhereclause + chatselectionclause + (datewhereclause.empty() && chatselectionclause.empty() ? " WHERE skip = 0" : " AND skip = 0") + " ORDER BY date", &pt_messages)) return false; //pt_messages.prettyPrint(d_truncate); bool warned_createcontacts = (isdummy ? true : false); // saves {rowid, {new_mms_id, unique_id}} for messages with attachments std::map> attachment_messages; //auto t1 = std::chrono::high_resolution_clock::now(); for (unsigned int i = 0; i < pt_messages.rows(); ++i) { if (i % (pt_messages.rows() > 100 ? 100 : 1) == 0) Logger::message_overwrite("Importing messages into backup... ", i, "/", pt_messages.rows());//, //" (", pt_messages.valueAsInt(i, "ismms"), ",", pt_messages.valueAsInt(i, "type"), ")"); std::string body = pt_messages(i, "body"); if (body.empty() && pt_messages.valueAsInt(i, "numattachments") <= 0) { Logger::warning("Not inserting message with empty body and no attachments. (date: ", pt_messages.valueAsInt(i, "date", -1), ", rowid: ", pt_messages.valueAsInt(i, "rowid"), ")"); continue; } std::string pt_messages_address = pt_messages(i, "address"); std::string pt_messages_sourceaddress = pt_messages(i, "sourceaddress"); std::string pt_messages_contact_name = pt_messages(i, "contact_name"); bool isgroup = false; if (std::string::size_type pos = pt_messages_address.find('~'); pos != std::string::npos && pos != 0 && pos != pt_messages_address.size() - 1) isgroup = true; // match phone number to thread recipient_id long long int trid = -1; if (auto it = contactmap.find(pt_messages_address); it != contactmap.end()) { trid = it->second; if (d_verbose) [[unlikely]] Logger::message("Got trid from contactmap: ", makePrintable(pt_messages_address)); } else { if (isdummy) /// only try by address (=phone). Names may be falsely doubled in the XML file { /// if (isgroup) trid = d_database.getSingleResultAs("SELECT _id FROM recipient WHERE group_id = ?", "__signal_group__fake__" + pt_messages_address, -1); else trid = getRecipientIdFromPhone(pt_messages_address, false); if (trid != -1) { contactmap[pt_messages_address] = trid; if (d_verbose) [[unlikely]] Logger::message("Got trid from addres: ", makePrintable(pt_messages_address)); } } else { trid = getRecipientIdFromName(pt_messages_contact_name, false); if (trid != -1) { contactmap[pt_messages_address] = trid; if (d_verbose) [[unlikely]] Logger::message("Got trid from name: ", makePrintable(pt_messages_contact_name)); } else // try by phone number... { // this can go wrong these days. When an old contact is no longer on signal, it // is possible the database has 2 entries for this contact, one with nothing but // phone number (from the system address book, possibly not a valid signal contact), // and one with names/aci/pni etc (which, while no longer registered, is valid). // // since the xml only has e164 to match, it will match the former (which is possibly // not a valid signal contact and likely causes problems when restoring) trid = getRecipientIdFromPhone(pt_messages_address, false); if (trid != -1) { contactmap[pt_messages_address] = trid; if (d_verbose) [[unlikely]] Logger::message("Got trid from addres: ", makePrintable(pt_messages_address)); } } } } if (trid == -1) { if (createmissingcontacts || isdummy) { if ((trid = ptCreateRecipient(ptdb, &contactmap, &warned_createcontacts, pt_messages_contact_name, pt_messages_address, isgroup)) == -1) { Logger::warning("Failed to create thread-recipient for address ", makePrintable(pt_messages_address), " (rowid: ", pt_messages.valueAsInt(i, "rowid"), ")"); continue; } if (d_verbose) [[unlikely]] Logger::message("Created thread-recipient id for address ", makePrintable(pt_messages_address)); } else Logger::error("Thread recipient not found in database."); } long long int tid = -1; // get matching thread if (auto it = threadmap.find(trid); it != threadmap.end()) tid = it->second; else { tid = getThreadIdFromRecipient(trid); if (tid == -1) { // create thread Logger::message_start("Failed to find matching thread for conversation, creating. (e164: ", makePrintable(pt_messages_address), " -> ", trid); std::any new_thread_id; if (!insertRow("thread", {{d_thread_recipient_id, trid}, {"active", 1}}, "_id", &new_thread_id)) { Logger::message_end(); Logger::error("Failed to create thread for conversation. Skipping message. (rowid: ", pt_messages.valueAsInt(i, "rowid"), ")"); continue; } tid = std::any_cast(new_thread_id); Logger::message_end(" -> thread_id: ", tid, ")"); } threadmap.emplace(trid, tid); } long long int rid = -1; if (auto it = contactmap.find(pt_messages_sourceaddress); it != contactmap.end()) [[likely]] rid = it->second; if (rid == -1) { Logger::error("Failed to find source_recipient_id in contactmap (", makePrintable(pt_messages_sourceaddress), "). Should be present at this point. Skipping message (rowid: ", pt_messages.valueAsInt(i, "rowid"), ")"); Logger::error_indent("Extra info:"); Logger::error_indent("Thread recipient: ", trid); std::string groupid = d_database.getSingleResultAs("SELECT group_id FROM recipient WHERE _id = ?", trid, std::string()); if (!groupid.empty()) { Logger::error_indent("Thread is group"); std::vector members; if (!getGroupMembersModern(&members, groupid)) Logger::error("Failed to get group members"); Logger::error_indent("Known group members (", members.size(), "): "); for (auto m : members) { std::string phone = d_database.getSingleResultAs("SELECT e164 FROM recipient WHERE _id = ?", m, std::string()); Logger::error_indent(" - ", m, " : ", phone.empty() ? "(empty)" : makePrintable(phone)); } } else Logger::error_indent("Thread is 1-on-1"); Logger::error_indent("First message for this address was (probably):"); ptdb->d_database.prettyPrint(false, "SELECT rowid, date, type, read, contact_name, address, numattachments, " "sourceaddress, targetaddresses, ismms, skip FROM smses WHERE address = ? " "ORDER BY date LIMIT 1", pt_messages_address); Logger::error_indent("Current message for this address was:"); ptdb->d_database.prettyPrint(false, "SELECT rowid, date, type, read, contact_name, address, numattachments, " "sourceaddress, targetaddresses, ismms, skip FROM smses WHERE rowid = ? " "ORDER BY date LIMIT 1", pt_messages.value(i, "rowid")); continue; } //std::cout << pt_messages(i, "address") << "/" << pt_messages(i, "contact_name") << " : " << trid << "/" << getNameFromRecipientId(trid) << std::endl; /* XML type : 1 = Received, 2 = Sent, 3 = Draft, 4 = Outbox, 5 = Failed, 6 = Queued */ bool incoming; switch (pt_messages.valueAsInt(i, "type", -1)) { case 1: incoming = true; break; case 2: case 4: // ? incoming = false; break; case 3: case 5: case 6: default: Logger::warning("Unsupported message type (", pt_messages.valueAsInt(i, "type", -1), "). Skipping... (rowid: ", pt_messages.valueAsInt(i, "rowid"), ")"); continue; } if (!unescapeXmlString(&body)) [[unlikely]] Logger::warning("Failed to escape message body: '", body, "'"); long long int freedate = pt_messages.valueAsInt(i, "date", -1); if (!isdummy) { // newer tables have a unique constraint on date_sent/thread_id/from_recipient_id, so // we try to get the first free date_sent long long int originaldate = freedate; if (originaldate == -1) { Logger::error("Failed to get message date. Skipping... (rowid: ", pt_messages.valueAsInt(i, "rowid"), ")"); continue; } //std::cout << "Get free date for message: '" << body << "'" << std::endl; freedate = getFreeDateForMessage(originaldate, tid, incoming ? trid : d_selfid); if (freedate == -1) { if (d_verbose) [[unlikely]] Logger::message_end(); Logger::error("Getting free date for inserting message into mms. Skipping... (rowid: ", pt_messages.valueAsInt(i, "rowid"), ")"); continue; } } std::any newid; if (!insertRow(d_mms_table, {{"thread_id", tid}, {d_mms_date_sent, freedate}, {"date_received", freedate}, {d_mms_type, Types::SECURE_MESSAGE_BIT | Types::PUSH_MESSAGE_BIT | (incoming ? Types::BASE_INBOX_TYPE : Types::BASE_SENT_TYPE)}, {"body", body}, {"read", 1}, // defaults to 0, but causes tons of unread message notifications {d_mms_delivery_receipts, (incoming ? 0 : (markdelivered ? 1 : 0))}, {d_mms_read_receipts, (incoming ? 0 : (markread ? 1 : 0))}, {d_mms_recipient_id, incoming ? (isgroup ? rid : trid) : d_selfid}, // FROM_RECIPIENT_ID {"to_recipient_id", incoming ? d_selfid : trid}, {"m_type", incoming ? 132 : 128}}, // dont know what this is, but these are the values... (pt_messages.valueAsInt(i, "numattachments", -1) > 0 ? "_id" : ""), &newid)) [[unlikely]] { Logger::warning("Failed to insert message (rowid: ", pt_messages.valueAsInt(i, "rowid"), ")"); continue; } if (pt_messages.valueAsInt(i, "numattachments", 0) > 0) attachment_messages.emplace_hint(attachment_messages.end(), pt_messages.valueAsInt(i, "rowid", 0), std::pair{std::any_cast(newid), freedate}); } Logger::message_overwrite("Importing messages into backup... ", pt_messages.rows(), "/", pt_messages.rows(), " done!", Logger::Control::ENDOVERWRITE); SqliteDB::QueryResults attachment_res; if (!ptdb->d_database.exec("SELECT data, filename, pos, size, ct, cl, mid FROM attachments " "WHERE mid IN (SELECT rowid FROM smses" + datewhereclause + chatselectionclause + (datewhereclause.empty() && chatselectionclause.empty() ? " WHERE skip = 0" : " AND skip = 0") + ")", &attachment_res)) return false; for (unsigned int j = 0; j < attachment_res.rows(); ++j) { if (j % (attachment_res.rows() > 100 ? 100 : 1) == 0) Logger::message_overwrite("Importing attachments into backup... ", j, "/", attachment_res.rows()); MEMINFO("START ATTACHMENT LOOP"); std::string data = attachment_res(j, "data"); std::string file = attachment_res(j, "filename"); long long int pos = attachment_res.valueAsInt(j, "pos", -1); long long int size = attachment_res.valueAsInt(j, "size", -1); std::string ct = attachment_res(j, "ct"); std::string cl = attachment_res(j, "cl"); auto amit = attachment_messages.find(attachment_res.valueAsInt(j, "mid", -1)); if (amit == attachment_messages.end()) [[unlikely]] { Logger::warning("Found attachment that belongs to no message (mid: ", attachment_res.valueAsInt(j, "mid", -1), ", size: ", size, ")"); continue; } long long int new_message_id = amit->second.first; long long int unique_id = amit->second.second; uint64_t att_data_size; int width = 0; int height = 0; std::string hash; // get attachment metadata SignalPlainTextBackupAttachmentReader ptar(data, file, pos, size); // if it's not just for HTML export we need data hash, and resolution if (!isdummy) { #if __cpp_lib_out_ptr >= 202106L std::unique_ptr att_data; if (ptar.getAttachmentData(std::out_ptr(att_data), d_verbose) != 0) #else unsigned char *att_data = nullptr; // !! NOTE RAW POINTER if (ptar.getAttachmentData(&att_data, d_verbose) != 0) #endif { Logger::error("Failed to get attachment data"); continue; } #if __cpp_lib_out_ptr >= 202106L AttachmentMetadata amd = AttachmentMetadata::getAttachmentMetaData(std::string(), att_data.get(), ptar.dataSize()); // get metadata from heap #else AttachmentMetadata amd = AttachmentMetadata::getAttachmentMetaData(std::string(), att_data, ptar.dataSize()); // get metadata from heap if (att_data) delete[] att_data; #endif att_data_size = amd.filesize; width = amd.width == -1 ? 0 : amd.width; height = amd.height== -1 ? 0 : amd.height; hash = amd.hash; } else att_data_size = ptar.dataSize(); // not supported on signal android anyway if (att_data_size == 0) [[unlikely]] { Logger::warning("Not inserting 0 byte attachment"); continue; } // add entry to attachment table; std::any new_aid; if (!insertRow(d_part_table, {{d_part_mid, new_message_id}, {d_part_ct, ct}, {!cl.empty() ? "file_name" : "", cl}, {d_part_pending, 0}, {"data_size", att_data_size}, {"voice_note", 0}, {"width", width}, {"height", height}, {"quote", 0}, {((d_database.tableContainsColumn(d_part_table, "data_hash") && !isdummy) ? "data_hash" : ""), hash}, {((d_database.tableContainsColumn(d_part_table, "data_hash_start") && !isdummy) ? "data_hash_start" : ""), hash}, {((d_database.tableContainsColumn(d_part_table, "data_hash_end") && !isdummy) ? "data_hash_end" : ""), hash}}, "_id", &new_aid)) continue; long long int new_part_id = std::any_cast(new_aid); DeepCopyingUniquePtr new_attachment_frame; if (setFrameFromStrings(&new_attachment_frame, std::vector{"ROWID:uint64:" + bepaald::toString(new_part_id), (d_database.tableContainsColumn(d_part_table, "unique_id") ? "ATTACHMENTID:uint64:" + bepaald::toString(unique_id) : ""), "LENGTH:uint32:" + bepaald::toString(att_data_size)})) { new_attachment_frame->setReader(new SignalPlainTextBackupAttachmentReader(data, file, pos, size)); d_attachments.emplace(std::make_pair(new_part_id, d_database.tableContainsColumn(d_part_table, "unique_id") ? unique_id : -1), new_attachment_frame.release()); } else { Logger::error("Failed to create AttachmentFrame for data"); Logger::error_indent(" rowid : ", new_part_id); Logger::error_indent(" attachmentid: ", d_database.tableContainsColumn(d_part_table, "unique_id") ? unique_id : -1); Logger::error_indent(" length : ", att_data_size); // try to remove the inserted part entry: d_database.exec("DELETE FROM " + d_part_table + " WHERE _id = ?", new_part_id); continue; } MEMINFO("END ATTACHMENT LOOP"); } //auto t2 = std::chrono::high_resolution_clock::now(); //auto ms_int = std::chrono::duration_cast(t2 - t1); //std::cout << " *** TIME: " << ms_int.count() << "ms\n"; Logger::message_overwrite("Importing attachments into backup... ", attachment_res.rows(), "/", attachment_res.rows(), " done!", Logger::Control::ENDOVERWRITE); // count entities still present... //ptdb->d_database.exec("SELECT rowid,body,LENGTH(body) - LENGTH(REPLACE(body, '&', '')) AS entities FROM smses ORDER BY entities ASC"); // save to disk //ptdb->d_database.saveToFile("xmldb.sqlite"); if (!skipmessagereorder) [[likely]] if (!isdummy) reorderMmsSmsIds(); updateThreadsEntries(); return checkDbIntegrity(); } signalbackup-tools-20250313-1/signalbackup/importtelegramjson.cc000066400000000000000000000115561476450434500246350ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ /* some known issues: - Overlapping text styles are not exported properly by telegram - Message delivery info might not be available in json - Message types other than 'message' (eg 'service') are currently skipped - underline style is not supported by signal - stickers turn into normal attachments? - 'forwarded from' is not an existing attribute in signal - message reaction are not exported by Telegram - poll-attachments skipped */ #include "signalbackup.ih" #include "../jsondatabase/jsondatabase.h" bool SignalBackup::importTelegramJson(std::string const &file, std::vector const &chatselection, std::vector> contactmap, std::vector const &inhibitmapping, bool prependforwarded, bool skipmessagereorder, bool markdelivered, bool markread, std::string const &selfphone) { Logger::message("Import from Telegram json export"); if (bepaald::isDir(file)) { Logger::error("Did not get regular file as input"); return false; } // check and warn about selfid d_selfid = selfphone.empty() ? scanSelf() : d_database.getSingleResultAs("SELECT _id FROM recipient WHERE " + d_recipient_e164 + " = ?", selfphone, -1); if (d_selfid == -1) { Logger::error("Failed to determine id of 'self'.", (selfphone.empty() ? "Please pass `--setselfid \"[phone]\"' to set it manually" : "")); return false; } // set selfuuid d_selfuuid = bepaald::toLower(d_database.getSingleResultAs("SELECT " + d_recipient_aci + " FROM recipient WHERE _id = ?", d_selfid, std::string())); JsonDatabase jsondb(file, d_verbose, d_truncate); if (!jsondb.ok()) return false; // get base path of file (we need it to set the resolve relative paths // referenced in the JSON data std::filesystem::path p(file); std::string datapath(p.parent_path().string()); if (!datapath.empty()) datapath += static_cast(std::filesystem::path::preferred_separator); // build chatlist: std::string chatlist; for (auto idx : chatselection) chatlist += (chatlist.empty() ? "(" : ", ") + bepaald::toString(idx); chatlist += (chatlist.empty() ? "" : ")"); // make sure all json-contacts are mapped to Signal contacts std::vector, long long int>> finalcontactmap; for (unsigned int i = 0; i < contactmap.size(); ++i) finalcontactmap.push_back({{contactmap[i].first}, contactmap[i].second}); if (!tgMapContacts(jsondb, chatlist, &finalcontactmap, inhibitmapping)) return false; SqliteDB::QueryResults chats; if (!jsondb.d_database.exec("SELECT idx, name, id, type FROM chats" + (chatlist.empty() ? "" : (" WHERE idx IN " + chatlist)), &chats)) return false; // for each chat, get the messages and insert for (unsigned int i = 0; i < chats.rows(); ++i) { Logger::message("Dealing with conversation ", i + 1, "/", chats.rows()); if (chats.valueAsString(i, "type") == "private_group" /*|| chats.valueAsString(i, "type") == "some_other_group"*/) tgImportMessages(jsondb.d_database, finalcontactmap, datapath, chats.valueAsString(i, "id"), chats.valueAsInt(i, "idx"), prependforwarded, markdelivered, markread, true); // deal with group chat else if (chats.valueAsString(i, "type") == "personal_chat") // ???? tgImportMessages(jsondb.d_database, finalcontactmap, datapath, chats.valueAsString(i, "id"), chats.valueAsInt(i, "idx"), prependforwarded, markdelivered, markread, false); // deal with 1-on-1 convo else if (chats.valueAsString(i, "type") == "saved_messages") tgImportMessages(jsondb.d_database, finalcontactmap, datapath, chats.valueAsString(i, "id"), chats.valueAsInt(i, "idx"), prependforwarded, markdelivered, markread, false); // deal note-to-self else { Logger::warning("Unsupported chat type `", chats.valueAsString(i, "type"), "'. Skipping..."); continue; } } if (!skipmessagereorder) [[likely]] reorderMmsSmsIds(); updateThreadsEntries(); return checkDbIntegrity(); } signalbackup-tools-20250313-1/signalbackup/importthread.cc000066400000000000000000001604641476450434500234150ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::importThread(SignalBackup *source, long long int thread) { Logger::message(__FUNCTION__, " (", thread, ")"); // known incompatibilities. There are almost certainly also unknown ones! if ((d_databaseversion >= 215 && source->d_databaseversion < 215) || // part.unique_id dropped from db (d_databaseversion < 215 && source->d_databaseversion >= 215) || (d_databaseversion >= 185 && source->d_databaseversion < 185) || // from/to_recipient_id (d_databaseversion < 185 && source->d_databaseversion >= 185) || // (d_databaseversion >= 172 && source->d_databaseversion < 172) || // group.members dropped (d_databaseversion < 172 && source->d_databaseversion >= 172) || // (d_databaseversion >= 168 && source->d_databaseversion < 168) || // sms table dropped (d_databaseversion < 168 && source->d_databaseversion >= 168) || // sms table dropped (d_databaseversion >= 33 && source->d_databaseversion < 33) || (d_databaseversion < 33 && source->d_databaseversion >= 33) || (d_databaseversion >= 27 && source->d_databaseversion < 27) || (d_databaseversion < 27 && source->d_databaseversion >= 27)) { Logger::error("Source and target database at incompatible versions"); return false; } if (source->d_database.containsTable("remapped_recipients")) { SqliteDB::QueryResults r; source->d_database.exec("SELECT * FROM remapped_recipients", &r); if (r.rows()) { Logger::warnOnce("Source database contains 'remapped_recipients'. This case may not yet be handled correctly by this program!"); if (d_verbose) [[unlikely]] { for (unsigned int i = 0; i < r.rows(); ++i) { long long int id = r.getValueAs(i, "_id"); long long int oldid = r.getValueAs(i, "old_id"); long long int newid = r.getValueAs(i, "new_id"); Logger::message(id, " : ", oldid, " -> ", newid, "\n"); Logger::message("Old id:"); source->d_database.print("SELECT * FROM recipient WHERE _id = ?", oldid); Logger::message_end(); Logger::message("New id:"); source->d_database.print("SELECT * FROM recipient WHERE _id = ?", newid); Logger::message_end(); } } // apply the remapping (probably only some reactions _may_ need to be transferred?) source->remapRecipients(); // now, the remapping was 'applied', old_id should not occur in database anymore, and remapped_recipients can be cleared? source->d_database.exec("DELETE FROM remapped_recipients"); } } /* // if target contains releasechannel recipient, make sure to remove it from source int target_releasechannel = -1; int source_releasechannel = -1; for (auto const &kv : d_keyvalueframes) if (kv->key() == "releasechannel.recipient_id" && !kv->value().empty()) { target_releasechannel = bepaald::toNumber(kv->value()); break; } if (target_releasechannel >= 0) for (auto const &skv : source->d_keyvalueframes) if (skv->key() == "releasechannel.recipient_id") { source_releasechannel = bepaald::toNumber(skv->value()); source->d_database.exec("DELETE FROM recipient WHERE _id = ?", source_releasechannel); std::cout << "Deleted double releasechannel recipient from source database (_id: " << source_releasechannel << ")" << std::endl; break; } */ // do not import release_channel from source. Target will either have its own, or be too old for one int source_releasechannel = -1; for (auto const &skv : source->d_keyvalueframes) if (skv->key() == "releasechannel.recipient_id") { source_releasechannel = bepaald::toNumber(skv->value()); source->d_database.exec("DELETE FROM recipient WHERE _id = ?", source_releasechannel); Logger::message("Deleted releasechannel recipient from source database (_id: ", source_releasechannel, ")"); break; } long long int targetthread = -1; SqliteDB::QueryResults results; if (d_databaseversion < 24) // old database version { // get targetthread from source thread id (source.thread_id->source.recipient_id->target.thread_id source->d_database.exec("SELECT " + source->d_thread_recipient_id + " FROM thread WHERE _id = ?", thread, &results); if (results.rows() != 1 || results.columns() != 1 || !results.valueHasType(0, 0)) { Logger::error("Failed to get recipient id from source database"); return false; } std::string recipient_id = results.getValueAs(0, 0); targetthread = getThreadIdFromRecipient(recipient_id); // -1 if none found } else // new database version { // get targetthread from source thread id (source.thread_id->source.recipient_id->source.recipient.phone/group_id->target.thread_id if (source->d_database.tableContainsColumn("recipient", source->d_recipient_aci, source->d_recipient_e164, "group_id", "distribution_list_id", source->d_recipient_storage_service)) source->d_database.exec("SELECT " "IFNULL(" + source->d_recipient_aci + ", '') AS uuid, " "IFNULL(" + source->d_recipient_e164 + ", '') AS phone, " "IFNULL(group_id, '') AS group_id, " "IFNULL(distribution_list.distribution_id, '') AS distribution_id, " "IFNULL(" + source->d_recipient_storage_service + ", '') AS storage_service " "FROM recipient " "LEFT JOIN distribution_list ON distribution_list._id = recipient.distribution_list_id " "WHERE recipient._id IS (SELECT " + source->d_thread_recipient_id + " FROM thread WHERE thread._id = ?)", thread, &results); else source->d_database.exec("SELECT " "IFNULL(" + source->d_recipient_aci + ", '') AS uuid, " "IFNULL(" + source->d_recipient_e164 + ", '') AS phone, " "IFNULL(group_id, '') AS group_id, " "'' AS distribution_id, " "'' AS storage_service " "FROM recipient " "WHERE _id IS (SELECT " + source->d_thread_recipient_id + " FROM thread WHERE _id = ?)", thread, &results); if (results.rows() != 1) { // skip current thread if it is the releasechannel-thread // maybe I should deal with this in the future SqliteDB::QueryResults res2; source->d_database.exec("SELECT " + source->d_thread_recipient_id + " FROM thread WHERE _id = ?", thread, &res2); if (res2.rows() && ((res2.valueHasType(0, 0) && res2.getValueAs(0, 0) == source_releasechannel) || (res2.valueHasType(0, 0) && bepaald::toNumber(res2.getValueAs(0, 0)) == source_releasechannel))) { Logger::message("Skipping releasechannel..."); return true; // when this channel is actually active, maybe remove this return statement and // manually set targetthread with the help of target_releasechannel (if != -1) } Logger::error("Failed to get uuid/phone/group_id from source database"); return false; } //std::string phone_or_group = results.getValueAs(0, 0); RecipientIdentification rec_id = {results(0, "uuid"), results(0, "phone"), results(0, "group_id"), results(0, "distribution_id"), results(0, "storage_service")}; if (d_verbose) [[unlikely]] Logger::message("Trying to match source recipient: {\"", rec_id.uuid, "\", \"", rec_id.phone, "\", \"", rec_id.group_id, "\"}"); if (d_database.tableContainsColumn("recipient", "distribution_list_id")) { long long int distribution_list_id = d_database.getSingleResultAs("SELECT _id FROM distribution_list WHERE distribution_id = ?", rec_id.distribution_id, -1); d_database.exec("SELECT _id FROM recipient WHERE " // match by aci "(" + d_recipient_aci + " IS NOT NULL AND " + d_recipient_aci + " IS ?) OR " // only match by phone if match by aci fails: "CASE WHEN (SELECT COUNT(_id) FROM recipient WHERE (" + d_recipient_aci + " IS NOT NULL AND " + d_recipient_aci + " IS ?)) = 0 THEN " "(" + d_recipient_e164 + " IS NOT NULL AND " + d_recipient_e164 + " IS ?) END OR " // match by group_id "(group_id IS NOT NULL AND group_id IS ?) OR " "(distribution_list_id IS NOT NULL AND distribution_list_id IS ?)", {rec_id.uuid, rec_id.uuid, rec_id.phone, rec_id.group_id, distribution_list_id}, &results); } else d_database.exec("SELECT _id FROM recipient WHERE " // match by aci "(" + d_recipient_aci + " IS NOT NULL AND " + d_recipient_aci + " IS ?) OR " // only match by phone if match by aci fails: "CASE WHEN (SELECT COUNT(_id) FROM recipient WHERE (" + d_recipient_aci + " IS NOT NULL AND " + d_recipient_aci + " IS ?)) = 0 THEN " "(" + d_recipient_e164 + " IS NOT NULL AND " + d_recipient_e164 + " IS ?) END OR " // match by group_id "(group_id IS NOT NULL AND group_id IS ?)", {rec_id.uuid, rec_id.uuid, rec_id.phone, rec_id.group_id}, &results); if (results.rows() != 1 || results.columns() != 1 || !results.valueHasType(0, 0)) { Logger::message("Failed to find recipient._id matching uuid/phone/group_id in target database"); // d_database.prettyPrint("SELECT _id, " + d_recipient_aci + "," + d_recipient_e164 + ",group_id FROM recipient " // "WHERE " + d_recipient_aci + " = ? OR " + // d_recipient_e164 + " = ? OR group_id = ?", {rec_id.uuid, rec_id.phone, rec_id.group_id}); } else { long long int recipient_id = results.getValueAs(0, 0); targetthread = getThreadIdFromRecipient(bepaald::toString(recipient_id)); if (d_verbose) [[unlikely]] Logger::message("Matched source recipient with target ", recipient_id, ", targetthread: ", targetthread); } } // std::cout << "RECIPIENTS BEFORE CROP:" << std::endl; // source->d_database.prettyPrint("SELECT _id, COALESCE(signal_profile_name, group_id) FROM recipient"); // delete doubles /* work in progress */ /* I dont think the recipentId == recipientId part is right */ if (false /*skipexisting*/ && targetthread != -1) { SqliteDB::QueryResults existing; d_database.exec("SELECT body, thread_id, " + d_mms_date_sent + ", " + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE thread_id = ?", targetthread, &existing); int count = 0; for (unsigned int i = 0; i < existing.rows(); ++i) { source->d_database.exec("DELETE FROM " + d_mms_table + " WHERE body = ? AND thread_id = ? AND " + d_mms_date_sent + " = ? AND " + d_mms_recipient_id + " = ?", {existing.value(i, "body"), thread, existing.value(i, d_mms_date_sent), existing.value(i, d_mms_recipient_id)}); count += source->d_database.changed(); } if (count) Logger::message(" Deleted ", count, " existing messages in source thread"); // check if any messages are left: if (source->d_database.getSingleResultAs("SELECT COUNT(*) FROM " + d_mms_table + " WHERE thread_id = ?", thread, -1) == 0) { Logger::message("After removing existing messages, thread is empty -> skipping..."); return true; } } // crop the source db to the specified thread source->cropToThread(thread); // std::cout << "RECIPIENTS AFTER CROP:" << std::endl; // source->d_database.prettyPrint("SELECT _id, COALESCE(signal_profile_name, group_id) FROM recipient"); // remove any storage_key entries that are already in target... if (d_database.containsTable("storage_key") && source->d_database.containsTable("storage_key")) { SqliteDB::QueryResults res; d_database.exec("SELECT key FROM storage_key", &res); int count = 0; for (unsigned int i = 0; i < res.rows(); ++i) { source->d_database.exec("DELETE FROM storage_key WHERE key = ?", res.value(i, 0)); count += source->d_database.changed(); } if (count) Logger::message(" Deleted ", count, " existing storage_keys"); } // delete double megaphones if (d_database.containsTable("megaphone") && source->d_database.containsTable("megaphone")) { SqliteDB::QueryResults res; d_database.exec("SELECT event FROM megaphone", &res); int count = 0; for (unsigned int i = 0; i < res.rows(); ++i) { source->d_database.exec("DELETE FROM megaphone WHERE event = ?", res.value(i, 0)); count += source->d_database.changed(); } if (count) Logger::message(" Deleted ", count, " existing megaphones"); } // delete double remote_megaphones if (d_database.containsTable("remote_megaphone") && source->d_database.containsTable("remote_megaphone")) { SqliteDB::QueryResults res; d_database.exec("SELECT uuid FROM remote_megaphone", &res); int count = 0; for (unsigned int i = 0; i < res.rows(); ++i) { source->d_database.exec("DELETE FROM remote_megaphone WHERE uuid = ?", res.value(i, 0)); count += source->d_database.changed(); } if (count) Logger::message(" Deleted ", count, " existing remote_megaphone's"); } // delete double cds (contact discovery service entries) if (d_database.containsTable("cds") && source->d_database.containsTable("cds")) { SqliteDB::QueryResults res; d_database.exec("SELECT e164 FROM cds", &res); int count = 0; for (unsigned int i = 0; i < res.rows(); ++i) { source->d_database.exec("DELETE FROM cds WHERE e164 = ?", res.value(i, 0)); count += source->d_database.changed(); } if (count) Logger::message(" Deleted ", count, " existing cds's"); } // UNTESTED // remove double in app payment subscribers // this table has unique string: subscriber_id_ID TEXT NOT NULL UNIQUE // and combo: UNIQUE(currency_code, type) if (d_database.containsTable("in_app_payment_subscriber")) { SqliteDB::QueryResults res; d_database.exec("SELECT subscriber_id, currency_code, type FROM in_app_payment_subscriber", &res); int count = 0; for (unsigned int i = 0; i < res.rows(); ++i) { source->d_database.exec("DELETE FROM in_app_payment_subscriber WHERE subscriber_id = ?", res.value(i, "subscriber_id")); count += source->d_database.changed(); source->d_database.exec("DELETE FROM in_app_payment_subscriber WHERE currency_code = ? AND type = ?", {res.value(i, "currency_code"), res.value(i, "type")}); count += source->d_database.changed(); } if (count) Logger::message(" Deleted ", count, " existing cds's"); } // remove any kyber_keys that are already in target... if (d_database.containsTable("kyber_prekey") && source->d_database.containsTable("kyber_prekey")) { SqliteDB::QueryResults res; d_database.exec("SELECT key_id FROM kyber_prekey", &res); int count = 0; for (unsigned int i = 0; i < res.rows(); ++i) { source->d_database.exec("DELETE FROM kyber_prekey WHERE key_id = ?", res.value(i, 0)); count += source->d_database.changed(); } if (count) Logger::message(" Deleted ", count, " existing kyber_prekeys"); } // NOTE THIS IS SUPERFLUOUS NOW? (SINCE key_id by itself is unique and removed above?) // delete double kyber prekey entries if (d_database.containsTable("kyber_prekey") && source->d_database.containsTable("kyber_prekey")) { SqliteDB::QueryResults res; d_database.exec("SELECT account_id, key_id FROM kyber_prekey", &res); int count = 0; for (unsigned int i = 0; i < res.rows(); ++i) { source->d_database.exec("DELETE FROM kyber_prekey WHERE account_id = ? AND key_id = ?", {res.value(i, "account_id"), res.value(i, "key_id")}); count += source->d_database.changed(); } if (count) Logger::message(" Deleted ", count, " existing kyber_prekey's"); } // delete double key_value entries (this table does not exist anymore currently) if (d_database.containsTable("key_value") && source->d_database.containsTable("key_value")) { SqliteDB::QueryResults res; d_database.exec("SELECT key FROM key_value", &res); int count = 0; for (unsigned int i = 0; i < res.rows(); ++i) { source->d_database.exec("DELETE FROM key_value WHERE key = ?", res.getValueAs(i, 0)); count += source->d_database.changed(); } if (count) Logger::message(" Deleted ", count, " existing key values"); } // delete double backup_media_snapshot (v257) // UNTESTED not sure what this does, but if it is going // to be used for incremental backups (backup v2), and // the media_id is used to determine if an attachment // was already backed up, this needs to be correct to // prevent data loss.. if (d_database.containsTable("backup_media_snapshot") && source->d_database.containsTable("backup_media_snapshot")) { SqliteDB::QueryResults res; d_database.exec("SELECT media_id FROM backup_media_snapshot", &res); int count = 0; for (unsigned int i = 0; i < res.rows(); ++i) { source->d_database.exec("DELETE FROM backup_media_snapshot WHERE media_id = ?", res.value(i, "media_id")); count += source->d_database.changed(); } if (count) Logger::message(" Deleted ", count, " existing backup_media_snapshot entries"); } // the target will have its own job_spec etc... if (source->d_database.containsTable("job_spec")) source->d_database.exec("DELETE FROM job_spec"); if (source->d_database.containsTable("push")) // dropped around dbv205 source->d_database.exec("DELETE FROM push"); if (source->d_database.containsTable("constraint_spec")) source->d_database.exec("DELETE FROM constraint_spec"); // has to do with job_spec, references it... if (source->d_database.containsTable("dependency_spec")) source->d_database.exec("DELETE FROM dependency_spec"); // has to do with job_spec, references it... // we will delete any notification_profile data, these are specific not to any threads, // but to the phone owner. The notification profiles will probably already exist on the // target, or can be easily recreated. They are difficult to import (they contain multiple // UNIQUE fields and should only be imported once, while this function will otherwise // do it for each thread imported.... if (source->d_database.containsTable("notification_profile_allowed_members")) source->d_database.exec("DELETE FROM notification_profile_allowed_members"); if (source->d_database.containsTable("notification_profile_schedule")) source->d_database.exec("DELETE FROM notification_profile_schedule"); if (source->d_database.containsTable("notification_profile")) source->d_database.exec("DELETE FROM notification_profile"); // NOT NECESSARY (these tables are skipped when merging anyway) AND CAUSES BREAKAGE // all emoji_search_* tables are ignored on import, delete from source here to prevent failing unique constraints /* if (source->d_database.containsTable("emoji_search_data")) source->d_database.exec("DELETE FROM emoji_search_data"); if (source->d_database.containsTable("emoji_search_idx")) source->d_database.exec("DELETE FROM emoji_search_idx"); if (source->d_database.containsTable("emoji_search_content")) source->d_database.exec("DELETE FROM emoji_search_content"); if (source->d_database.containsTable("emoji_search_docsize")) source->d_database.exec("DELETE FROM emoji_search_docsize"); if (source->d_database.containsTable("emoji_search_config")) source->d_database.exec("DELETE FROM emoji_search_config"); */ if (d_database.containsTable("group_call_ring") && source->d_database.containsTable("group_call_ring")) { // not sure what this table is for, but it has a UNIQUE ring_id field, // so let's just delete any double ring_id's SqliteDB::QueryResults res; d_database.exec("SELECT ring_id FROM group_call_ring", &res); for (unsigned int i = 0; i < res.rows(); ++i) source->d_database.exec("DELETE FROM group_call_ring WHERE ring_id = ?", res.getValueAs(i, 0)); } // untested /* sqlite> SELECT * FROM sqlite_master WHERE name IS "payments"; type|name|tbl_name|rootpage|sql table|payments|payments|60|CREATE TABLE payments(_id INTEGER PRIMARY KEY, uuid TEXT DEFAULT NULL, recipient INTEGER DEFAULT 0, recipient_address TEXT DEFAULT NULL, timestamp INTEGER, note TEXT DEFAULT NULL, direction INTEGER, state INTEGER, failure_reason INTEGER, amount BLOB NOT NULL, fee BLOB NOT NULL, transaction_record BLOB DEFAULT NULL, receipt BLOB DEFAULT NULL, payment_metadata BLOB DEFAULT NULL, receipt_public_key TEXT DEFAULT NULL, block_index INTEGER DEFAULT 0, block_timestamp INTEGER DEFAULT 0, seen INTEGER, UNIQUE(uuid) ON CONFLICT ABORT) */ // NOTE: 'recipient' here is probably recipient._id which should be updated later in updateRecipientId() // maybe delete completely if recipient is not in this bit of database? /* // delete double payments if (d_database.containsTable("payments") && source->d_database.containsTable("payments")) { SqliteDB::QueryResults res; d_database.exec("SELECT uuid FROM payments", &res); std::cout << " Deleting " << res.rows() << " existing payments" << std::endl; for (unsigned int i = 0; i < res.rows(); ++i) source->d_database.exec("DELETE FROM payments WHERE uuid = ?", res.getValueAs(i, 0)); } */ /* sqlite> SELECT * FROM sqlite_master WHERE name IS "chat_colors"; table|chat_colors|chat_colors|65|CREATE TABLE chat_colors (_id INTEGER PRIMARY KEY AUTOINCREMENT,chat_colors BLOB) */ // untested: I guess chat_colors are in target, or they can be easily recreated if (source->d_database.containsTable("chat_colors")) source->d_database.exec("DELETE FROM chat_colors"); // TODO deal with 'sender_keys' /* sqlite> SELECT * FROM sqlite_master WHERE name IS "sender_keys"; table|sender_keys|sender_keys|71|CREATE TABLE sender_keys (_id INTEGER PRIMARY KEY AUTOINCREMENT, recipient_id INTEGER NOT NULL, device INTEGER NOT NULL, distribution_id TEXT NOT NULL, record BLOB NOT NULL, created_at INTEGER NOT NULL, UNIQUE(recipient_id, device, distribution_id) ON CONFLICT REPLACE) */ // notes: // * there is recipient_id, which probably needs to be adjusted in updateRecipientId(), but it must be unique so it must then be deleted if the adjustment can be made // * there is a created_at, probably a timestamp -> delete the older one?, also use this in croptodate? //source->d_database.exec("VACUUM"); // id's need to be unique makeIdsUnique(source); // delete double remapped_recipients if (d_database.containsTable("remapped_recipients") && source->d_database.containsTable("remapped_recipients")) { SqliteDB::QueryResults res; source->d_database.exec("SELECT * FROM remapped_recipients", &res); // get all remapped recipients in source for (unsigned int i = 0; i < res.rows(); ++i) { long long int id = res.getValueAs(i, "_id"); long long int oldid = res.getValueAs(i, "old_id"); long long int newid = res.getValueAs(i, "new_id"); SqliteDB::QueryResults r2; d_database.exec("SELECT * FROM remapped_recipients WHERE old_id = ? AND new_id = ?", {oldid, newid}, &r2); if (r2.rows()) // this mapping is in target already { Logger::message("Skipping import of remapped_recipient (", oldid, " -> ", newid, "), mapping already in target database"); source->d_database.exec("DELETE FROM remapped_recipients WHERE _id = ?", id); } } } // merge into existing thread, set the id on the sms, mms, and drafts // drop the recipient_preferences, identities and thread tables, they are already in the target db if (targetthread > -1) { Logger::message(" Found existing thread for this recipient in target database, merging into thread ", targetthread); // set thread_ids to found targetthread for (auto const &dbl : s_databaselinks) if (dbl.table == "thread") { for (auto const &c : dbl.connections) { if (source->d_database.containsTable(c.table)) source->d_database.exec("UPDATE " + c.table + " SET " + c.column + " = ?", targetthread); } break; } // see below for comment explaining this function if (d_databaseversion >= 24) { //d_database.exec("SELECT _id, COALESCE(uuid,phone,group_id) AS identifier FROM recipient", &results); if (d_database.tableContainsColumn("recipient", "distribution_list_id", d_recipient_storage_service)) d_database.exec("SELECT recipient._id, " "IFNULL(" + d_recipient_aci + ", '') AS uuid, " "IFNULL(" + d_recipient_e164 + ", '') AS phone, " "IFNULL(group_id, '') AS group_id, " "IFNULL(distribution_list.distribution_id, '') AS distribution_id, " "IFNULL(" + d_recipient_storage_service + ", '') AS storage_service " "FROM recipient " "LEFT JOIN distribution_list ON distribution_list._id = recipient.distribution_list_id ", &results); else d_database.exec("SELECT _id, " "IFNULL(" + d_recipient_aci + ", '') AS uuid, " "IFNULL(" + d_recipient_e164 + ", '') AS phone, " "IFNULL(group_id, '') AS group_id, " "'' AS 'distribution_id', " "'' AS 'storage_service' " "FROM recipient", &results); Logger::message(" updateRecipientIds"); //results.prettyPrint(d_truncate); for (unsigned int i = 0; i < results.rows(); ++i) { RecipientIdentification rec_id = {results(i, "uuid"), results(i, "phone"), results(i, "group_id"), results(i, "distribution_id"), results(i, "storage_service")}; //source->updateRecipientId(results.getValueAs(i, "_id"), results.getValueAs(i, "identifier")); source->updateRecipientId(results.getValueAs(i, "_id"), rec_id); } } source->d_database.exec("DROP TABLE thread"); source->d_database.exec("DROP TABLE identities"); // even though the thread already exists, not all recipients are guaranteed to // exist in target. For example: current thread is group conversation, in the source // a member was added, but this member did not yet exist in the target. // // the same probably goes for ancient (<24) databases, but I'll write that if someone ever // tries to merge those. // // delete existsing recipients (recipient table has unique constraint on phone, uuid and group_id) if (d_databaseversion < 24) source->d_database.exec("DROP TABLE recipient_preferences"); else { // get the unique features of existing recipients SqliteDB::QueryResults existing_rec; if (d_database.tableContainsColumn("recipient", "distribution_list_id") && d_database.tableContainsColumn("recipient", d_recipient_storage_service)) d_database.exec("SELECT recipient._id, " "IFNULL(" + d_recipient_aci + ", '') AS uuid, " "IFNULL(" + d_recipient_e164 + ", '') AS phone, " "IFNULL(group_id, '') AS group_id, " "IFNULL(distribution_list.distribution_id, '') AS distribution_id, " "IFNULL(" + source->d_recipient_storage_service + ", '') AS storage_service " "FROM recipient " "LEFT JOIN distribution_list ON distribution_list._id = recipient.distribution_list_id ", &existing_rec); else d_database.exec("SELECT _id, " "IFNULL(" + d_recipient_aci + ", '') AS uuid, " "IFNULL(" + d_recipient_e164 + ", '') AS phone, " "IFNULL(group_id, '') AS group_id, " "'' AS distribution_id, " "'' AS storage_service " "FROM recipient", &existing_rec); // for each of them, check if they are also in source, and delete int count = 0; for (unsigned int i = 0; i < results.rows(); ++i) { RecipientIdentification rec_id = {existing_rec(i, "uuid"), existing_rec(i, "phone"), existing_rec(i, "group_id"), existing_rec(i, "distribution_id"), existing_rec(i, "storage_service")}; if (source->d_database.tableContainsColumn("recipient", "distribution_list_id")) { long long int distribution_list_id = source->d_database.getSingleResultAs("SELECT _id FROM distribution_list WHERE distribution_id = ?", rec_id.distribution_id, -1); source->d_database.exec("DELETE FROM recipient WHERE " // one-of uuid/phone/group_id is set and equal to existing recipient, // or distribution_list_id points to dist_list with same _id "((" + source->d_recipient_aci + " IS NOT NULL AND " + source->d_recipient_aci + " IS ?) OR " "(" + source->d_recipient_e164 + " IS NOT NULL AND " + source->d_recipient_e164 + " IS ?) OR " "(group_id IS NOT NULL AND group_id IS ?) OR " "(distribution_list_id IS NOT NULL AND distribution_list_id IS ?))", {rec_id.uuid, rec_id.phone, rec_id.group_id, distribution_list_id}); count += source->d_database.changed(); } else { source->d_database.exec("DELETE FROM recipient WHERE " // one-of uuid/phone/group_id is set and equal to existing recipient "((" + source->d_recipient_aci + " IS NOT NULL AND " + source->d_recipient_aci + " IS ?) OR " "(" + source->d_recipient_e164 + " IS NOT NULL AND " + source->d_recipient_e164 + " IS ?) OR " "(group_id IS NOT NULL AND group_id IS ?))", {rec_id.uuid, rec_id.phone, rec_id.group_id}); count += source->d_database.changed(); } } if (count) Logger::message("Dropped ", count, " existing recipients from source database"); } source->d_database.exec("DROP TABLE groups"); source->d_avatars.clear(); } else // no matching thread in target (but recipient may still exist) { Logger::message(" No existing thread found in target database for this recipient, importing."); // unpin thread from source database, to prevent uniqueness issues with target if (source->d_database.tableContainsColumn("thread", source->d_thread_pinned) && d_database.tableContainsColumn("thread", d_thread_pinned)) { // before dbv266, 'unpinned' meant the column was set to '0', after dbv266 it was 'NULL'. Make sure to use target's default here! std::string target_pinned_default = d_database.getSingleResultAs("SELECT dflt_value FROM pragma_table_info('thread') WHERE name = '" + d_thread_pinned + "'", std::string()); if (!source->d_database.exec("UPDATE thread SET " + source->d_thread_pinned + " = " + target_pinned_default)) Logger::warning("Failed to unpin threads in source database."); } // check identities and recipient prefs for presence of values, they may be there (even // though no thread was found (for example via a group chat or deleted thread)) // get identities from target, drop all rows from source that are already present if (d_databaseversion < 24) { d_database.exec("SELECT address FROM identities", &results); // address == phonenumber/__text_secure_group for (unsigned int i = 0; i < results.rows(); ++i) if (results.header(0) == "address" && results.valueHasType(i, 0)) source->d_database.exec("DELETE FROM identities WHERE address = '" + results.getValueAs(i, 0) + "'"); } else { // get all phonenums/groups_ids for all in identities if (d_database.tableContainsColumn("recipient", "distribution_list_id", source->d_recipient_storage_service)) d_database.exec("SELECT " "IFNULL(" + d_recipient_aci + ", '') AS uuid, " "IFNULL(" + d_recipient_e164 + ", '') AS phone, " "IFNULL(group_id, '') AS group_id, " "IFNULL(distribution_list.distribution_id, '') AS distribution_id, " "IFNULL(" + source->d_recipient_storage_service + ", '') AS storage_service " "FROM recipient " "LEFT JOIN distribution_list ON distribution_list._id = recipient.distribution_list_id " "WHERE recipient._id IN (SELECT address FROM identities)", &results); else d_database.exec("SELECT " "IFNULL(" + d_recipient_aci + ", '') AS uuid, " "IFNULL(" + d_recipient_e164 + ", '') AS phone, " "IFNULL(group_id, '') AS group_id, " "'' AS distribution_id, " "'' AS storage_service " "FROM recipient " "WHERE _id IN (SELECT address FROM identities)", &results); for (unsigned int i = 0; i < results.rows(); ++i) { RecipientIdentification rec_id = {results(i, "uuid"), results(i, "phone"), results(i, "group_id"), results(i, "distribution_id"), results(i, "storage_service")}; // source->d_database.exec("DELETE FROM identities WHERE address IN (SELECT _id FROM recipient WHERE COALESCE(uuid,phone,group_id) = '" + results.getValueAs(i, 0) + "')"); if (source->d_database.tableContainsColumn("recipient", "distribution_list_id")) { long long int distribution_list_id = source->d_database.getSingleResultAs("SELECT _id FROM distribution_list WHERE distribution_id = ?", rec_id.distribution_id, -1); source->d_database.exec("DELETE FROM identities WHERE address IN " "(SELECT _id FROM recipient WHERE " "(" + source->d_recipient_aci + " IS NOT NULL AND " + source->d_recipient_aci + " IS ?) OR " "(" + source->d_recipient_e164 + " IS NOT NULL AND " + source->d_recipient_e164 + " IS ?) OR " "(group_id IS NOT NULL AND group_id IS ?) OR " "(distribution_list_id IS NOT NULL AND distribution_list_id IS ?))", {rec_id.uuid, rec_id.phone, rec_id.group_id, distribution_list_id}); } else source->d_database.exec("DELETE FROM identities WHERE address IN " "(SELECT _id FROM recipient WHERE " "(" + source->d_recipient_aci + " IS NOT NULL AND " + source->d_recipient_aci + " IS ?) OR " "(" + source->d_recipient_e164 + " IS NOT NULL AND " + source->d_recipient_e164 + " IS ?) OR " "(group_id IS NOT NULL AND group_id IS ?))", {rec_id.uuid, rec_id.phone, rec_id.group_id}); } } // get recipient(_preferences) from target, drop all rows from source that are already present if (d_databaseversion < 24) { d_database.exec("SELECT recipient_ids FROM recipient_preferences", &results); for (unsigned int i = 0; i < results.rows(); ++i) if (results.header(0) == "recipient_ids" && results.valueHasType(i, 0)) source->d_database.exec("DELETE FROM recipient_preferences WHERE recipient_ids = '" + results.getValueAs(i, 0) + "'"); } else { //d_database.exec("SELECT _id,COALESCE(uuid,phone,group_id) AS ident FROM recipient", &results); if (d_database.tableContainsColumn("recipient", "distribution_list_id", d_recipient_storage_service)) d_database.exec("SELECT recipient._id, " "IFNULL(" + d_recipient_aci + ", '') AS uuid, " "IFNULL(" + d_recipient_e164 + ", '') AS phone, " "IFNULL(group_id, '') AS group_id, " "IFNULL(distribution_list.distribution_id, '') AS distribution_id, " "IFNULL(" + d_recipient_storage_service + ", '') AS storage_service " "FROM recipient " "LEFT JOIN distribution_list ON distribution_list._id = recipient.distribution_list_id", &results); else d_database.exec("SELECT _id, " "IFNULL(" + d_recipient_aci + ", '') AS uuid, " "IFNULL(" + d_recipient_e164 + ", '') AS phone, " "IFNULL(group_id, '') AS group_id, " "'' AS distribution_id, " "'' AS storage_service " "FROM recipient", &results); Logger::message(" updateRecipientIds (2)"); //results.prettyPrint(d_truncate); int count = 0; for (unsigned int i = 0; i < results.rows(); ++i) { // if the recipient is already in target, we are going to delete it from // source, to prevent doubles. However, many tables refer to the recipient._id // which was made unique above. If we just delete the doubles (by phone/group_id, // and in the future probably uuid), the fields in other tables will point // to random or non-existing recipients, so we need to remap them: RecipientIdentification rec_id = {results(i, "uuid"), results(i, "phone"), results(i, "group_id"), results(i, "distribution_id"), results(i, "storage_service")}; source->updateRecipientId(results.getValueAs(i, "_id"), rec_id); //source->updateRecipientId(results.getValueAs(i, "_id"), results.getValueAs(i, "ident")); // std::cout << "Testing if recipient is present:" << std::endl; // std::cout << "\"" << rec_id.uuid << "\" \"" << rec_id.phone << "\" \"" << rec_id.group_id << "\" \"" << rec_id.group_type << "\" \"" << rec_id.storage_service_key << "\"" << std::endl; // now drop the already present recipient from source. // source->d_database.exec("DELETE FROM recipient WHERE COALESCE(uuid,phone,group_id) = '" + results.getValueAs(i, "ident") + "'"); if (d_database.tableContainsColumn("recipient", "distribution_list_id")) { long long int distribution_list_id = source->d_database.getSingleResultAs("SELECT _id FROM distribution_list WHERE distribution_id = ?", rec_id.distribution_id, -1); source->d_database.exec("DELETE FROM recipient WHERE " // one-of uuid/phone/group_id is set and equal to existing recipient "((" + source->d_recipient_aci + " IS NOT NULL AND " + source->d_recipient_aci + " IS ?) OR " "(" + source->d_recipient_e164 + " IS NOT NULL AND " + source->d_recipient_e164 + " IS ?) OR " "(group_id IS NOT NULL AND group_id IS ?) OR " "(distribution_list_id IS NOT NULL AND distribution_list_id IS ?))", {rec_id.uuid, rec_id.phone, rec_id.group_id, distribution_list_id}); count += source->d_database.changed(); } else { source->d_database.exec("DELETE FROM recipient WHERE " // one-of uuid/phone/group_id is set and equal to existing recipient "((" + source->d_recipient_aci + " IS NOT NULL AND " + source->d_recipient_aci + " IS ?) OR " "(" + source->d_recipient_e164 + " IS NOT NULL AND " + source->d_recipient_e164 + " IS ?) OR " "(group_id IS NOT NULL AND group_id IS ?))", {rec_id.uuid, rec_id.phone, rec_id.group_id}); count += source->d_database.changed(); } } if (count) Logger::message("Dropped ", count, " existing recipients from source database"); } // even though the source was cropped to single thread, and this thread was not in target, avatar might still already be in target // because contact (and avatar) might be present in group in source, and only as one-on-one in target bool erased = true; while (erased) { erased = false; for (std::vector>>::iterator sourceav = source->d_avatars.begin(); sourceav != source->d_avatars.end(); ++sourceav) { for (std::vector>>::iterator targetav = d_avatars.begin(); targetav != d_avatars.end(); ++targetav) if (sourceav->first == targetav->first) { source->d_avatars.erase(sourceav); erased = true; break; } if (erased) break; } } // Just because the group has no thread, doesn't mean it doesn't exist already int count = 0; SqliteDB::QueryResults existing_groups; d_database.exec("SELECT group_id, recipient_id FROM groups", &existing_groups); for (unsigned int i = 0; i < existing_groups.rows(); ++i) { SqliteDB::QueryResults removed_group_recipient_id; source->d_database.exec("DELETE FROM groups WHERE group_id = ? RETURNING recipient_id", existing_groups.value(i, "group_id"), &removed_group_recipient_id); int changed = source->d_database.changed(); count += changed; if (changed && existing_groups.valueAsInt(i, "recipient_id", -1) != removed_group_recipient_id.valueAsInt(0, "recipient_id", -2)) Logger::warning("Existing group removed from source table, but recipient_ids did not match " "(", existing_groups.valueAsInt(i, "recipient_id", -1), ", ", removed_group_recipient_id.valueAsInt(0, "recipient_id", -2), ")"); } if (count) Logger::message("Removed ", count, " existing groups from source database"); } // delete group_membership's already present if (d_database.containsTable("group_membership")) { SqliteDB::QueryResults gm_results; d_database.exec("SELECT DISTINCT group_id, recipient_id FROM group_membership", &gm_results); for (unsigned int i = 0; i < gm_results.rows(); ++i) source->d_database.exec("DELETE FROM group_membership WHERE group_id = ? AND recipient_id = ?", {gm_results.value(i, "group_id"), gm_results.value(i, "recipient_id")}); } // delete double call.call_id's (call_id is timestamp, this shouldn't naturally // happen, but does when merging threads that overlap in time) if (d_database.containsTable("call")) { SqliteDB::QueryResults call_results; d_database.exec("SELECT DISTINCT call_id FROM call", &call_results); for (unsigned int i = 0; i < call_results.rows(); ++i) source->d_database.exec("DELETE FROM call WHERE call_id = ?", call_results.value(i, "call_id")); } // delete double stickers if (d_database.containsTable("sticker") && source->d_database.containsTable("sticker")) { SqliteDB::QueryResults installed_stickers; d_database.exec("SELECT pack_id, sticker_id, cover FROM sticker", &installed_stickers); int count = 0; for (unsigned int i = 0; i < installed_stickers.rows(); ++i) { SqliteDB::QueryResults deleted_sticker_ids; source->d_database.exec("DELETE FROM sticker WHERE pack_id = ? AND sticker_id = ? AND cover = ? RETURNING _id", {installed_stickers.value(i, "pack_id"), installed_stickers.value(i, "sticker_id"), installed_stickers.value(i, "cover")}, &deleted_sticker_ids); count += source->d_database.changed(); // delete actual sticker image for (unsigned int j = 0; j < deleted_sticker_ids.rows(); ++j) { long long int erased = deleted_sticker_ids.valueAsInt(j, "_id"); if (erased == -1) continue; auto it = std::find_if(source->d_stickers.begin(), source->d_stickers.end(), [erased](auto const &s) { return s.first == static_cast(erased); }); if (it != source->d_stickers.end()) source->d_stickers.erase(it); } } if (count) Logger::message(" Deleted ", count, " existing stickers"); } // delete double pendingpnisignaturemessages if (d_database.containsTable("pending_pni_signature_message") && source->d_database.containsTable("pending_pni_signature_message")) { SqliteDB::QueryResults res; d_database.exec("SELECT recipient_id, sent_timestamp, device_id FROM pending_pni_signature_message", &res); int count = 0; for (unsigned int i = 0; i < res.rows(); ++i) { source->d_database.exec("DELETE FROM pending_pni_signature_message " "WHERE recipient_id = ? AND sent_timestamp = ? AND device_id = ?", {res.value(i, "recipient_id"), res.value(i, "sent_timestamp"), res.value(i, "device_id")}); count += source->d_database.changed(); } if (count) Logger::message(" Deleted ", count, " existing pending_pni_signature_messages"); } // delete double distribution lists? if (d_database.containsTable("distribution_list") && source->d_database.containsTable("distribution_list")) { SqliteDB::QueryResults res; d_database.exec("SELECT distribution_id FROM distribution_list", &res); int count = 0; for (unsigned int i = 0; i < res.rows(); ++i) { SqliteDB::QueryResults deleted_distribution_list_recipients; source->d_database.exec("DELETE FROM distribution_list WHERE distribution_id = ? RETURNING recipient_id", res.value(i, 0), &deleted_distribution_list_recipients); count += source->d_database.changed(); //deleted_distribution_list_recipients.prettyPrint(d_truncate); // delete the corresponding recipient for (unsigned int j = 0; j < deleted_distribution_list_recipients.rows(); ++j) { source->d_database.exec("DELETE FROM recipient WHERE _id = ?", deleted_distribution_list_recipients.value(j, "recipient_id")); // the number can be zero 0, as the recipient is matched (by distribution_list_id, for // MY_STORY it is always 00000000-0000-0000-0000-000000000000), after which it is deleted // as a doubled, existing recipient above (see: Dropped x existing recipients from source database) if (source->d_database.changed() > 1) [[unlikely]] Logger::warning("Unexpected number of distribution_list recipients deleted: ", source->d_database.changed()); } } if (count) Logger::message(" Deleted ", count, " existing distribution lists"); // clean up the member table source->d_database.exec("DELETE FROM distribution_list_member WHERE list_id NOT IN (SELECT DISTINCT _id FROM distribution_list)"); } // delete exisiting name_collisions (only possible if targetthread is an existing thread) if (source->d_database.containsTable("name_collision") && d_database.containsTable("name_collision")) { SqliteDB::QueryResults res; d_database.exec("SELECT thread_id FROM name_collision", &res); for (unsigned int i = 0; i < res.rows(); ++i) source->d_database.exec("DELETE FROM name_collision WHERE thread_id = ?", res.value(i, 0)); // delete corresponding collision_members source->d_database.exec("DELETE FROM name_collision_membership WHERE collision_id NOT IN (SELECT _id FROM name_collision)"); } // delete existing chat_folder_memberships if (source->d_database.containsTable("chat_folder_membership") && d_database.containsTable("chat_folder_membership")) { SqliteDB::QueryResults res; d_database.exec("SELECT chat_folder_id, thread_id FROM chat_folder_membership", &res); for (unsigned int i = 0; i < res.rows(); ++i) source->d_database.exec("DELETE FROM chat_folder_membership WHERE chat_folder_id = ? AND thread_id = ?", {res.value(i, "chat_folder_id"), res.value(i, "thread_id")}); } // delete the default chat_folder if (source->d_database.containsTable("chat_folder_membership") && d_database.containsTable("chat_folder_membership")) { source->d_database.exec("DELETE FROM chat_folder WHERE NAME IS NULL"); } // // export database // std::cout << "Writing database..." << std::endl; // SqliteDB database("NEWSTYLE.2.sqlite", false); // if (!SqliteDB::copyDb(source->d_database, database)) // std::cout << "Error exporting sqlite database" << std::endl; // return; // now import the source tables into target, // get tables std::string q("SELECT sql, name, type FROM sqlite_master"); source->d_database.exec(q, &results); std::vector tables; for (unsigned int i = 0; i < results.rows(); ++i) { if (!results.isNull(i, 0)) { //std::cout << "Dealing with: " << results.getValueAs(i, 1) << std::endl; if (results.valueHasType(i, 1) && (results.getValueAs(i, 1) != "sms_fts" && STRING_STARTS_WITH(results.getValueAs(i, 1), "sms_fts"))) ;//std::cout << "Skipping " << results[i][1].second << " because it is sms_ftssecrettable" << std::endl; else if (results.valueHasType(i, 1) && (results.getValueAs(i, 1) != d_mms_table + "_fts" && STRING_STARTS_WITH(results.getValueAs(i, 1), d_mms_table + "_fts"))) ;//std::cout << "Skipping " << results[i][1].second << " because it is mms_ftssecrettable" << std::endl; else if (results.valueHasType(i, 1) && (results.getValueAs(i, 1) != "emoji_search" && STRING_STARTS_WITH(results.getValueAs(i, 1), "emoji_search"))) ;//std::cout << "Skipping " << results.getValueAs(i, 1) << " because it is emoji_search_ftssecrettable" << std::endl; else if (results.valueHasType(i, 1) && STRING_STARTS_WITH(results.getValueAs(i, 1), "sqlite_")) ; else if (results.valueHasType(i, 2) && results.getValueAs(i, 2) == "table") { tables.emplace_back(results.getValueAs(i, 1)); //std::cout << "Added: " << results.getValueAs(i, 1) << std::endl; } } } // write contents of tables for (std::string const &table : tables) { if (table == "signed_prekeys" || table == "one_time_prekeys" || table == "sessions" || //table == "job_spec" || // this is in the official export. But it makes testing more difficult. it //table == "constraint_spec" || // should be ok to export these (if present in source), since we are only //table == "dependency_spec" || // dealing with exported backups (not from live installations) -> they should //table == "emoji_search" || // have been excluded + the official import should be able to deal with them STRING_STARTS_WITH(table, "sms_fts") || STRING_STARTS_WITH(table, d_mms_table + "_fts") || STRING_STARTS_WITH(table, "sqlite_")) continue; source->d_database.exec("SELECT * FROM " + table, &results); if (results.rows() == 0) { if (d_verbose) [[unlikely]] Logger::message("Importing statements from source table '", table, "'... (0 entries) ...done"); continue; } if (!d_database.containsTable(table)) { Logger::warning("Skipping table '", table, "', as it is not present in target database. Data may be missing."); continue; } // check if source contains columns not existing in target // even though target is newer, a fresh install would not have // created dropped columns that may still be present in // source database; unsigned int idx = 0; while (idx < results.headers().size()) { if (!d_database.tableContainsColumn(table, results.headers()[idx])) { // attempt translate std::string oldname = results.headers()[idx]; std::string newname = getTranslatedName(table, oldname); if (!newname.empty() && results.renameColumn(idx, newname)) { Logger::message(" NOTE: Translating column name from '", table, ".", oldname, "' (source) to '", table, ".", newname, "' (target)"); } else //: drop { Logger::message(" NOTE: Dropping column '", table, ".", results.headers()[idx], "' from source : Column not present in target database"); if (results.removeColumn(idx)) continue; } } // else ++idx; } // if all columns were dropped, the entire table (probably) does not exist in target database, we'll just skip it // for instance, a (newly/currently?) created database will not have the megaphone table if (results.columns() == 0) { Logger::warning("Skipping table '", table, "', it has no columns left."); continue; } Logger::message_start("Importing statements from source table '", table, "'... (", results.rows(), " entries)"); for (unsigned int i = 0; i < results.rows(); ++i) { // if (table == "identities") // { // std::cout << "Trying to add: "; // for (unsigned int j = 0; j < results.columns(); ++j) // std::cout << results.valueAsString(i, j) << " "; // std::cout << std::endl; // } SqlStatementFrame newframe = buildSqlStatementFrame(table, results.headers(), results.row(i)); d_database.exec(newframe.bindStatement(), newframe.parameters()); //newframe.printInfo(); } Logger::message_end(" ...done"); } // and copy avatars and attachments. for (auto &att : source->d_attachments) d_attachments.emplace(std::move(att)); for (auto &av : source->d_avatars) d_avatars.emplace_back(std::move(av)); // stickers??? for (auto &s : source->d_stickers) d_stickers.emplace(std::move(s)); /* THIS IS NOT TRUE, CURRENTLY THE RELEASECHANNEL THREAD FROM SOURCE IS SKIPPED UNCONDITIONALLY AND THE RELEASECHANNEL RECIPIENT FROM SOURCE IS REMOVED. ADDING THIS KEY WILL CAUSE TROUBLE // if target has no release channel-recipient, but // source does, it is copied over, we need the pref if (target_releasechannel == -1) for (auto &skv : source->d_keyvalueframes) if (skv->key() == "releasechannel.recipient_id") { d_keyvalueframes.emplace_back(std::move(skv)); break; } */ // update thread snippet and date and count updateThreadsEntries(); d_database.exec("VACUUM"); d_database.freeMemory(); return checkDbIntegrity(); } signalbackup-tools-20250313-1/signalbackup/importwachat.cc000066400000000000000000000160631476450434500234100ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ //#include "signalbackup.ih" //DEPRECATED //#include /* Limitations and requirements - no media (yet?) - no mentions YET - filename must exactly and uniquely identify an existing conversation (group or contact name) in the Signal backup - messages have format ': ' where timestamp is passed as 'fmt', author exactly and uniquely identifies an existing contact in the Signal backup and contains no colons (':') - */ // bool SignalBackup::importWAChat(std::string const &file, std::string const &fmt, std::string const &self) // { // std::ifstream chatfile(file); // if (!chatfile.is_open()) // { // std::cout << bepaald::bold_on << "ERROR" << bepaald::bold_off // << " opening file '" << file << "' for reading." << std::endl; // return false; // } // // get global address (recipient_id of chat partner for 1-on-1, group_id for groupchats // std::string chatname = file.substr(0, file.length() - STRLEN(".txt")); // std::string globaladdress; // bool isgroup = false; // SqliteDB::QueryResults results; // std::map name_to_recipientid; // std::cout << "Looking for conversation: '" << file.substr(0, file.length() - STRLEN(".txt")) << "'" << std::endl; // if (!d_database.exec("SELECT recipient._id, recipient.group_id, COALESCE(groups.title, recipient.system_display_name, recipient.profile_joined_name, recipient.phone) AS 'identifier' " // "FROM recipient " // "LEFT JOIN groups ON recipient.group_id == groups.group_id " // "WHERE identifier == ?", chatname, &results)) // return false; // //results.prettyPrint(); // if (results.rows() == 1) // { // globaladdress = results.valueAsString(0, "_id"); // name_to_recipientid[chatname] = globaladdress; // if (results.valueHasType(0, "group_id")) // { // isgroup = true; // if (self.empty()) // { // std::cout << "Error: dealing with group chat, but self-id not supplied. No way of determining which messages are outgoing and which are incoming." << std::endl; // return false; // } // } // } // else // { // std::cout << "Failed to find conversation partner/group for chat: '" << file << "' : query returned " << results.rows() << " results." << std::endl; // return false; // } // // get thread id from recipient // long long int tid = -1; // if (!d_database.exec("SELECT _id FROM thread WHERE " + d_thread_recipient_id + " == ?", globaladdress, &results)) // return false; // if (results.rows() != 1) // { // std::cout << "Failed to find thread for chat. Query returned " << results.rows() << " results." << std::endl; // return false; // } // tid = results.getValueAs(0, "_id"); // std::cout << "Importing messages into thread: " << tid << std::endl; // std::string line; // std::string author; // // scan for chat participants, we need these upfront to reference in mentions and update delevery_receipts for groups // while (std::getline(chatfile, line)) // { // std::tm tmb = {}; // std::istringstream ss(line); // ss >> std::get_time(&tmb, fmt.c_str()); // if (ss.fail()) // continue; // else // { // std::getline(ss, author, ':'); // if (ss.eof()) // { // //std::cout << "No colon => some type of status message?" << std::endl; // author.clear(); // continue; // } // // get the address and save in map. // if (name_to_recipientid.find(author) == name_to_recipientid.end()) // { // if (!d_database.exec("SELECT _id, COALESCE(system_display_name, profile_joined_name, phone) AS 'identifier' " // "FROM recipient WHERE identifier == ?", author, &results)) // return false; // if (results.rows() != 1) // return false; // name_to_recipientid[author] = results.valueAsString(0, "_id"); // } // } // } // chatfile.clear(); // chatfile.seekg(0); // line.clear(); // author.clear(); // long long int time = 0; // long long int previous_time = 0; // long long int previous_time_adj = 0; // std::string message; // uint64_t count = 0; // // scan for messages // while (std::getline(chatfile, line)) // { // std::tm tmb = {}; // std::istringstream ss(line); // ss >> std::get_time(&tmb, fmt.c_str()); // if (ss.fail()) // { // std::cout << "Invalid timefmt (" << fmt << ") => continue previous message" << std::endl; // ss.clear(); // ss.seekg(0); // std::string msg_cnt; // if (!std::getline(ss, msg_cnt)) // { // std::cout << "Some error reading the line" << std::endl; // continue; // } // message += "\n" + msg_cnt; // } // else // { // // found start of new message, deal with previous // if (time != 0 && !author.empty()) // { // if (!handleWAMessage(tid, time, chatname, author, message, self, isgroup, name_to_recipientid)) // return false; // else // ++count; // } // // get a unique sequentially later time // time = std::mktime(&tmb) * 1000; // if (time == previous_time) // time = previous_time_adj + 1; // else // previous_time = time; // previous_time_adj = time; // //std::cout << "Time: " << std::put_time(&tmb, "%c") << " (" << time << ")" << std::endl; // std::getline(ss, author, ':'); // if (ss.eof()) // { // std::cout << "No colon => some type of status message? SKIPPING LINE : '" << author << "'" << std::endl; // author.clear(); // continue; // } // //std::cout << "Author: " << author << std::endl; // std::getline(ss, message); // message = message.substr(1); // remove the single whitespace after colon?? // //std::cout << "Message: " << message << std::endl; // } // } // // deal with last message // if (time != 0 && !author.empty()) // { // if (!handleWAMessage(tid, time, chatname, author, message, self, isgroup, name_to_recipientid)) // return false; // else // ++count; // } // std::cout << "Imported " << count << " messages from file '" << file << "'" << std::endl; // return true; // } signalbackup-tools-20250313-1/signalbackup/initfromdir.cc000066400000000000000000000175261476450434500232410ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../attachmentmetadata/attachmentmetadata.h" void SignalBackup::initFromDir(std::string const &inputdir, bool replaceattachments) { Logger::message("Opening from dir!"); Logger::message("Reading database..."); FileSqliteDB database(inputdir + "/database.sqlite"); if (!SqliteDB::copyDb(database, d_database)) return; Logger::message("Reading HeaderFrame"); if (!setFrameFromFile(&d_headerframe, inputdir + "/Header.sbf")) return; d_backupfileversion = d_headerframe->version(); //d_headerframe->printInfo(); Logger::message("Reading DatabaseVersionFrame"); if (!setFrameFromFile(&d_databaseversionframe, inputdir + "/DatabaseVersion.sbf")) return; d_databaseversion = d_databaseversionframe->version(); //d_databaseversionframe->printInfo(); setColumnNames(); Logger::message("Reading SharedPreferenceFrame(s)"); int idx = 1; while (true) { d_sharedpreferenceframes.resize(d_sharedpreferenceframes.size() + 1); if (!setFrameFromFile(&d_sharedpreferenceframes.back(), inputdir + "/SharedPreference_" + bepaald::toString(idx) + ".sbf", true)) { d_sharedpreferenceframes.pop_back(); break; } //d_sharedpreferenceframes.back()->printInfo(); ++idx; } Logger::message("Reading KeyValueFrame(s)"); idx = 1; while (true) { d_keyvalueframes.resize(d_keyvalueframes.size() + 1); if (!setFrameFromFile(&d_keyvalueframes.back(), inputdir + "/KeyValue_" + bepaald::toString(idx) + ".sbf", true)) { d_keyvalueframes.pop_back(); break; } //d_keyvalueframes.back()->printInfo(); ++idx; } Logger::message("Reading EndFrame"); if (!setFrameFromFile(&d_endframe, inputdir + "/End.sbf")) { Logger::warning("EndFrame was not read: backup is probably incomplete"); addEndFrame(); } //d_endframe->printInfo(); // avatars // NOTE, avatars are read in two passes to force correct order if (!d_showprogress) Logger::message_start("Reading AvatarFrames"); std::error_code ec; std::filesystem::directory_iterator dirit(inputdir, ec); std::vector avatarfiles; if (ec) { Logger::message_end(); Logger::error("Error iterating directory `", inputdir, "' : ", ec.message()); return; } for (auto const &avatar : dirit) // put all Avatar_[...].sbf files in vector: if (avatar.path().extension() == ".sbf" && STRING_STARTS_WITH(avatar.path().filename().string(), "Avatar_")) avatarfiles.push_back(avatar.path().string()); std::sort(avatarfiles.begin(), avatarfiles.end()); #if __cplusplus > 201703L for (unsigned int i = 0; auto const &file : avatarfiles) #else unsigned int i = 0; for (auto const &file : avatarfiles) #endif { if (d_showprogress) { Logger::message_overwrite("Reading AvatarFrames: ", ++i, "/", avatarfiles.size()); if (i == avatarfiles.size()) Logger::message_overwrite("Reading AvatarFrames: ", avatarfiles.size(), "/", avatarfiles.size(), Logger::Control::ENDOVERWRITE); } std::filesystem::path avatarframe(file); std::filesystem::path avatarbin(file); avatarbin.replace_extension(".bin"); DeepCopyingUniquePtr temp; if (!setFrameFromFile(&temp, avatarframe.string())) return; //if (!temp->setAttachmentDataFromFile(avatarbin.string())) // return; temp->setReader(new RawFileAttachmentReader(avatarbin.string())); //temp->printInfo(); std::string name = (d_databaseversion < 33) ? temp->name() : temp->recipient(); d_avatars.emplace_back(name, temp.release()); } if (!d_showprogress) Logger::message_end(); Logger::message("Reading AttachmentFrames"); //attachments dirit = std::filesystem::directory_iterator(inputdir, ec); if (ec) { Logger::error("Error iterating directory `", inputdir, "' : ", ec.message()); return; } int replaced_count = 0; for (auto const &att : dirit) { if (att.path().extension() != ".sbf" || !STRING_STARTS_WITH(att.path().filename().string(), "Attachment_")) continue; std::filesystem::path attframe = att.path(); std::filesystem::path attbin = att.path(); attbin.replace_extension(".bin"); bool replaced_attachement = false; if (replaceattachments) { attbin.replace_extension(".new"); if (bepaald::fileOrDirExists(attbin)) replaced_attachement = true; else attbin.replace_extension(".bin"); } DeepCopyingUniquePtr temp; if (!setFrameFromFile(&temp, attframe.string())) return; //if (!temp->setAttachmentData(attbin.string())) // return; //temp->setLazyDataRAW(temp->length(), attbin.string()); temp->setReader(new RawFileAttachmentReader(/*temp->length(), */attbin.string())); if (replaced_attachement) { AttachmentMetadata amd = AttachmentMetadata::getAttachmentMetaData(attbin.string()); if (!amd) // undo the replacement { Logger::error("Failed to get metadata on new attachment: ", attbin); attbin.replace_extension(".bin"); //if (!temp->setAttachmentData(attbin.string())) // return; //temp->setLazyDataRAW(temp->length(), attbin.string()); temp->setReader(new RawFileAttachmentReader(/*temp->length(), */attbin.string())); } else { // update database if (!updatePartTableForReplace(amd, temp->rowId())) { Logger::error("Failed to insert new attachment into database"); return; } // set correct size on AttachmentFrame temp->setLength(amd.filesize); ++replaced_count; } } uint64_t rowid = temp->rowId(); int64_t attachmentid = temp->attachmentId(); d_attachments.emplace(std::make_pair(rowid, attachmentid ? attachmentid : -1), temp.release()); MEMINFO("ADDED ATTACHMENT"); } if (replaced_count) Logger::message(" - Replaced ", replaced_count, " attachments"); Logger::message("Reading StickerFrames"); //stickers dirit = std::filesystem::directory_iterator(inputdir, ec); if (ec) { Logger::error("Error iterating directory `", inputdir, "' : ", ec.message()); return; } for (auto const &sticker : dirit) { if (sticker.path().extension() != ".sbf" || sticker.path().filename().string().substr(0, STRLEN("Sticker_")) != "Sticker_") continue; std::filesystem::path stickerframe = sticker.path(); std::filesystem::path stickerbin = sticker.path(); stickerbin.replace_extension(".bin"); DeepCopyingUniquePtr temp; if (!setFrameFromFile(&temp, stickerframe.string())) return; //if (!temp->setAttachmentDataFromFile(stickerbin.string())) // return; temp->setReader(new RawFileAttachmentReader(stickerbin.string())); uint64_t rowid = temp->rowId(); d_stickers.emplace(std::make_pair(rowid, temp.release())); } #ifdef BUILT_FOR_TESTING // check for file 'BUILT_FOR_TESTING_FOUND_SQLITE_SEQUENCE' // set d_found_sqlite_sequence_in_backup if (bepaald::fileOrDirExists(inputdir + "/BUILT_FOR_TESTING_FOUND_SQLITE_SEQUENCE")) d_found_sqlite_sequence_in_backup = true; #endif Logger::message("Done!"); d_ok = true; } signalbackup-tools-20250313-1/signalbackup/initfromfile.cc000066400000000000000000000177001476450434500233740ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::initFromFile() { if (!d_fd->ok()) { Logger::message("Failed to create filedecrypter"); return; } // open file std::ifstream backupfile(d_filename, std::ios_base::binary | std::ios_base::in); if (!backupfile.is_open()) [[unlikely]] { Logger::error("Failed to open file '", d_filename, "'"); return; } int64_t totalsize = d_fd->total(); int prev_progress = 2000; // note this number will be max 1000 (10 * 100%), so this is an invalid number (to trigger on 0). if (d_verbose || !d_showprogress) [[unlikely]] Logger::message_overwrite("Reading backup file..."); else Logger::message_overwrite("Reading backup file: 000.0%..."); std::unique_ptr frame; d_database.exec("BEGIN TRANSACTION"); // get frames and handle them until file is fully read or error is encountered while ((frame = d_fd->getFrame(backupfile))) { if (d_fd->badMac()) [[unlikely]] { dumpInfoOnBadFrame(&frame); if (d_stoponerror) return; } if (d_showprogress) [[likely]] { int64_t progress = (static_cast(backupfile.tellg()) / totalsize) * 1000; if (progress != prev_progress || d_verbose) { //std::cout << "Progress: " << progress << " " << std::fixed << (static_cast(progress) / 10) << std::endl; if (d_verbose) [[unlikely]] Logger::message_overwrite("FRAME ", frame->frameNumber(), ": ", std::fixed, std::setprecision(1), std::setw(5), std::setfill('0'), (static_cast(progress) / 10), std::defaultfloat, "%..."); else Logger::message_overwrite("Reading backup file:", " ", std::fixed, std::setprecision(1), std::setw(5), std::setfill('0'), (static_cast(progress) / 10), std::defaultfloat, "%..."); prev_progress = progress; } } //if (frame->frameNumber() > 73085) // frame->printInfo(); //MEMINFO("At frame ", frame->frameNumber(), " (", frame->frameTypeString(), ")"); if (frame->frameType() == BackupFrame::FRAMETYPE::SQLSTATEMENT) [[likely]] { SqlStatementFrame *s = reinterpret_cast(frame.get()); if (!STRING_STARTS_WITH(s->bindStatement(), "CREATE TABLE sqlite_")) [[likely]] // skip creation of sqlite_ internal db's { // NOTE: in the official import, there are other tables that are skipped (virtual tables for search data) // we lazily do not check for them here, since we are dealing with official exported files which do not contain // these tables as they are excluded on the export-side as well. Additionally, the official import should be able // to properly deal with them anyway (that is: ignore them) if (!d_database.exec(s->bindStatement(), s->parameters())) [[unlikely]] Logger::warning("Failed to execute statement: ", s->statement()); } #ifdef BUILT_FOR_TESTING else if (s->statement().find("CREATE TABLE sqlite_sequence") != std::string::npos) { // force early creation of sqlite_sequence table, this is completely unnecessary and only used // to get byte-identical backups during testing Logger::message("BUILT_FOR_TESTING : Forcing early creation of sqlite_sequence"); d_database.exec("CREATE TABLE dummy (_id INTEGER PRIMARY KEY AUTOINCREMENT)"); d_database.exec("DROP TABLE dummy"); d_found_sqlite_sequence_in_backup = true; } #endif } else if (frame->frameType() == BackupFrame::FRAMETYPE::ATTACHMENT) { AttachmentFrame *a = reinterpret_cast(frame.release()); if (d_fulldecode) [[unlikely]] { a->attachmentData(nullptr, d_verbose); a->clearData(); } int64_t attachmentid = a->attachmentId(); d_attachments.emplace(std::make_pair(a->rowId(), attachmentid ? attachmentid : -1), a); } else if (frame->frameType() == BackupFrame::FRAMETYPE::AVATAR) { AvatarFrame *a = reinterpret_cast(frame.release()); if (d_fulldecode) [[unlikely]] { a->attachmentData(nullptr, d_verbose); a->clearData(); } d_avatars.emplace_back(std::string((d_databaseversion < 33) ? a->name() : a->recipient()), a); } else if (frame->frameType() == BackupFrame::FRAMETYPE::STICKER) { StickerFrame *s = reinterpret_cast(frame.release()); if (d_fulldecode) [[unlikely]] { s->attachmentData(nullptr, d_verbose); s->clearData(); } d_stickers.emplace(s->rowId(), s); } else if (frame->frameType() == BackupFrame::FRAMETYPE::SHAREDPREFERENCE) { //frame->printInfo(); d_sharedpreferenceframes.emplace_back(reinterpret_cast(frame.release())); } else if (frame->frameType() == BackupFrame::FRAMETYPE::KEYVALUE) { //frame->printInfo(); d_keyvalueframes.emplace_back(reinterpret_cast(frame.release())); } else if (frame->frameType() == BackupFrame::FRAMETYPE::HEADER) { d_headerframe.reset(reinterpret_cast(frame.release())); d_backupfileversion = d_headerframe->version(); if (d_verbose) [[unlikely]] d_headerframe->printInfo(); } else if (frame->frameType() == BackupFrame::FRAMETYPE::DATABASEVERSION) { d_databaseversionframe.reset(reinterpret_cast(frame.release())); d_databaseversion = d_databaseversionframe->version(); if (d_verbose) [[unlikely]] Logger::message("Database version: ", d_databaseversionframe->version()); } else if (frame->frameType() == BackupFrame::FRAMETYPE::END) { //frame->printInfo(); if (d_verbose) [[unlikely]] Logger::message("Read EndFrame"); d_endframe.reset(reinterpret_cast(frame.release())); } else [[unlikely]] // if (frame->frameType() == BackupFrame::FRAMETYPE::INVALID) { Logger::warning(Logger::Control::BOLD, "SKIPPING INVALID FRAME! (", frame->frameType(), ")", Logger::Control::NORMAL); } } d_database.exec("COMMIT"); if (!d_badattachments.empty()) [[unlikely]] { Logger::message("Attachment data with BAD MAC was encountered:"); dumpInfoOnBadFrames(); } //std::cout << "" << std::endl; if (d_fd->badMac()) [[unlikely]] { if (d_stoponerror) return; } if (!d_endframe) [[unlikely]] { Logger::warning("EndFrame was not read: backup is probably incomplete"); addEndFrame(); } if (backupfile.tellg() == totalsize && d_showprogress) [[likely]] Logger::message_overwrite("Reading backup file:", " 100.0%... done!", Logger::Control::ENDOVERWRITE); /* #warning REMOVE ME // INJECT SOME OTHER SQLITE DATABASE { // NOTE THIS LEAKS std::pair *data = new std::pair; std::ifstream file("WITH_SOME_STORIES_PRESENT/database.sqlite"); file.seekg(0, std::ios_base::end); data->second = file.tellg(); file.seekg(0); data->first = new unsigned char[data->second]; file.read(reinterpret_cast(data->first), data->second); d_database = MemSqliteDB(data); } */ d_ok = setColumnNames(); } signalbackup-tools-20250313-1/signalbackup/insertrow.cc000066400000000000000000000057241476450434500227440ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #if __cpp_lib_ranges >= 201911L #include #endif bool SignalBackup::insertRow(std::string const &table, std::vector> data, std::string const &returnfield, std::any *returnvalue) const { // check if columns exist... for (auto it = data.begin(); it != data.end();) { if (it->first.empty()) it = data.erase(it); else if (!d_database.tableContainsColumn(table, it->first)) { Logger::warning("Table '", table, "' does not contain any column '", it->first, "'. Removing"); it = data.erase(it); } else ++it; } std::string query = "INSERT INTO " + table + " ("; for (unsigned int i = 0; i < data.size(); ++i) query += data[i].first + (i < data.size() -1 ? ", " : ") "); query += "VALUES ("; for (unsigned int i = 0; i < data.size(); ++i) query += "?"s + (i < data.size() -1 ? ", " : ")"); #if SQLITE_VERSION_NUMBER >= 3035000 // RETURNING was not available prior to 3.35.0 if (!returnfield.empty() && returnvalue) query += " RETURNING " + returnfield; #endif SqliteDB::QueryResults res; #if __cpp_lib_ranges >= 201911L bool ret = d_database.exec(query, std::views::values(data), &res, d_verbose); #else std::vector values; std::transform(data.begin(), data.end(), std::back_inserter(values), [](auto const &pair){ return pair.second; }); bool ret = d_database.exec(query, values, &res, d_verbose); #endif #if SQLITE_VERSION_NUMBER < 3035000 // RETURNING was not available prior to 3.35.0 if (ret && !returnfield.empty() && returnvalue) { long long int lastid = d_database.lastId(); ret = d_database.exec("SELECT " + returnfield + " FROM " + table + " WHERE rowid = ?", lastid, &res, d_verbose); } #endif if (ret && !returnfield.empty() && returnvalue && res.rows() && res.columns()) { if (res.rows() > 1 || res.columns() > 1) [[unlikely]] Logger::warning("Requested return of '", returnfield, "', " "but query returned multiple results. Returning first."); *returnvalue = res.value(0, 0); } if (d_verbose) [[unlikely]] Logger::message("Inserted new row into table '", table, "'. New _id: ", d_database.lastId()); return ret; } signalbackup-tools-20250313-1/signalbackup/listrecipients.cc000066400000000000000000000103051476450434500237400ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::listRecipients() const { /* Note on group types: 0 = individual (no group) 1 = mms 2 = group v1 3 = group v2 4 = distribution list (story) 5 = call link */ /* Note on registration status: 0 = Unknown 1 = Yes 2 = No */ d_database.prettyPrint(d_truncate, "SELECT recipient._id, " "COALESCE(" + (d_database.tableContainsColumn("recipient", "nickname_joined_name") ? "NULLIF(recipient.nickname_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_system_joined_name + ", ''), " + (d_database.tableContainsColumn("recipient", "profile_joined_name") ? "NULLIF(recipient.profile_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_profile_given_name + ", ''), " "NULLIF(groups.title, ''), " + (d_database.containsTable("distribution_list") ? "NULLIF(distribution_list.name, ''), " : "") + "NULLIF(recipient." + d_recipient_e164 + ", ''), " "NULLIF(recipient." + d_recipient_aci + ", ''), " " recipient._id) AS 'display_name', " + "recipient." + d_recipient_e164 + ", " + (d_database.tableContainsColumn("recipient", "blocked") ? "blocked, " : "") + (d_database.tableContainsColumn("recipient", "hidden") ? "hidden, " : "") + "IFNULL(COALESCE(" + d_recipient_profile_avatar + ", groups.avatar_id), 0) IS NOT 0 AS 'has_avatar', " "CASE recipient." + d_recipient_type + " WHEN 0 THEN 'Individual' ELSE " " CASE recipient." + d_recipient_type + " WHEN 3 THEN 'Group (v2)' ELSE " " CASE recipient." + d_recipient_type + " WHEN 4 THEN 'Group (story)' ELSE " " CASE recipient." + d_recipient_type + " WHEN 1 THEN 'Group (mms)' ELSE " " CASE recipient." + d_recipient_type + " WHEN 2 THEN 'Group (v1)' ELSE " " CASE recipient." + d_recipient_type + " WHEN 5 THEN 'Group (call)' ELSE 'unknown' " " END " " END " " END " " END " " END " "END AS 'type', " "CASE WHEN recipient." + d_recipient_type + " IS NOT 0 THEN '(n/a)' ELSE " " CASE registered WHEN 1 THEN 'Yes' ELSE " " CASE registered WHEN 2 THEN 'No' ELSE 'Unknown' " " END " " END " "END AS 'registered', " "COALESCE (recipient.group_id, " + d_recipient_aci + ") IS NOT NULL AS has_id, " "thread._id IS NOT NULL as has_thread " "FROM recipient " "LEFT JOIN groups ON recipient.group_id = groups.group_id " + "LEFT JOIN thread ON recipient._id = thread.recipient_id " + (d_database.containsTable("distribution_list") ? "LEFT JOIN distribution_list ON recipient._id = distribution_list.recipient_id " : " ") + "ORDER BY display_name"); } signalbackup-tools-20250313-1/signalbackup/listthreads.cc000066400000000000000000000112031476450434500232230ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::listThreads() const { SqliteDB::QueryResults results; if (d_database.containsTable("sms")) d_database.exec("SELECT MIN(mindate) AS 'Min Date', MAX(maxdate) AS 'Max Date' FROM " "(SELECT MIN(sms." + d_sms_date_received + ") AS mindate, MAX(sms." + d_sms_date_received + ") AS maxdate FROM sms " "UNION ALL SELECT MIN(" + d_mms_table + ".date_received) AS mindate, MAX(" + d_mms_table + ".date_received) AS maxdate FROM " + d_mms_table + ")", &results); else d_database.exec("SELECT MIN(" + d_mms_table + ".date_received) AS 'Min Date', MAX(" + d_mms_table + ".date_received) AS 'Max Date' FROM " + d_mms_table, &results); results.prettyPrint(d_truncate); if (!d_database.containsTable("recipient")) d_database.exec("SELECT thread._id, thread." + d_thread_recipient_id + ", thread.snippet, COALESCE(recipient_preferences.system_display_name, recipient_preferences.signal_profile_name, groups.title) AS 'Conversation partner' FROM thread LEFT JOIN recipient_preferences ON thread." + d_thread_recipient_id + " = recipient_preferences.recipient_ids LEFT JOIN groups ON thread." + d_thread_recipient_id + " = groups.group_id ORDER BY thread._id ASC", &results); else // has recipient table { bool uuid = d_database.tableContainsColumn("recipient", d_recipient_aci); bool profile_joined_name = d_database.tableContainsColumn("recipient", "profile_joined_name"); // std::cout << d_thread_recipient_id << std::endl; // std::cout << d_sms_recipient_id << std::endl; // std::cout << d_mms_recipient_id << std::endl; d_database.exec("SELECT thread._id, " "COALESCE(recipient." + d_recipient_e164 + ", recipient.group_id" + (uuid ? ", recipient."s + d_recipient_aci : ""s) + ", recipient._id) AS 'recipient_ids', " "thread.snippet, " "COALESCE(" + (d_database.tableContainsColumn("recipient", "nickname_joined_name") ? "NULLIF(recipient.nickname_joined_name, ''), " : "") + "NULLIF(recipient." + d_recipient_system_joined_name + ", ''), " + (profile_joined_name ? "NULLIF(recipient.profile_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_profile_given_name + ", ''), groups.title, " + (d_database.containsTable("distribution_list") ? "NULLIF(distribution_list.name, '')" : "") + ") AS 'Conversation partner' " "FROM thread " "LEFT JOIN recipient ON thread." + d_thread_recipient_id + " = recipient._id " "LEFT JOIN groups ON recipient.group_id = groups.group_id " + (d_database.containsTable("distribution_list") ? "LEFT JOIN distribution_list ON recipient._id = distribution_list.recipient_id " : "") + "ORDER BY thread._id ASC", &results); // results.prettyPrint(); // if (d_database.tableContainsColumn("recipient", "profile_joined_name")) // d_database.exec("SELECT thread._id, COALESCE(recipient.phone, recipient.group_id, recipient.uuid) AS 'recipient_ids', thread.snippet, COALESCE(recipient.system_display_name, recipient.profile_joined_name, recipient.signal_profile_name, groups.title) AS 'Conversation partner' FROM thread LEFT JOIN recipient ON thread." + d_thread_recipient_id + " = recipient._id LEFT JOIN groups ON recipient.group_id = groups.group_id ORDER BY thread._id ASC", &results); // else // d_database.exec("SELECT thread._id, COALESCE(recipient.phone, recipient.group_id, recipient.uuid) AS 'recipient_ids', thread.snippet, COALESCE(recipient.system_display_name, recipient.signal_profile_name, groups.title) AS 'Conversation partner' FROM thread LEFT JOIN recipient ON thread." + d_thread_recipient_id + " = recipient._id LEFT JOIN groups ON recipient.group_id = groups.group_id ORDER BY thread._id ASC", &results); } results.prettyPrint(d_truncate); } signalbackup-tools-20250313-1/signalbackup/makefilenameunique.cc000066400000000000000000000032341476450434500245470ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::makeFilenameUnique(std::string const &path, std::string *file_or_dir) const { while (bepaald::fileOrDirExists(path + "/" + *file_or_dir)) { //std::cout << std::endl << "File exists: " << path << "/" << file_or_dir << " -> "; std::filesystem::path p(*file_or_dir); std::regex numberedfile(".*( \\(([0-9]*)\\))$"); std::smatch sm; std::string filestem(p.stem().string()); //std::string ext(p.extension().string()); int counter = 2; if (regex_match(filestem, sm, numberedfile) && sm.size() >= 3 && sm[2].matched) { // increase the counter counter = bepaald::toNumber(sm[2].str()) + 1; // remove " (xx)" part from stem filestem.erase(sm[1].first, sm[1].second); } *file_or_dir = filestem + " (" + bepaald::toString(counter) + ")" + p.extension().string(); //std::cout << file_or_dir << std::endl; } return true; } signalbackup-tools-20250313-1/signalbackup/makeidsunique.cc000066400000000000000000000177601476450434500235570ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::makeIdsUnique(SignalBackup *source) { Logger::message(__FUNCTION__); Logger::message(" Adjusting indexes in tables..."); for (auto const &dbl : s_databaselinks) { // skip if table/column does not exist, or if skip is set if ((dbl.flags & SKIP) || !d_database.containsTable(dbl.table) || !source->d_database.containsTable(dbl.table) || !source->d_database.tableContainsColumn(dbl.table, dbl.column)) continue; { SqliteDB::QueryResults results; source->d_database.exec("SELECT * FROM " + dbl.table, &results); if (results.rows() == 0) continue; else if (dbl.flags & WARN) Logger::warning("Found entries in a usually empty table. Trying to deal with it, but problems may occur."); } long long int offsetvalue = getMaxUsedId(dbl.table, dbl.column) + 1 - source->getMinUsedId(dbl.table, dbl.column); source->setMinimumId(dbl.table, offsetvalue, dbl.column); for (auto const &c : dbl.connections) { if (source->d_databaseversion >= c.mindbvversion && source->d_databaseversion <= c.maxdbvversion) { if (!source->d_database.containsTable(c.table) || !source->d_database.tableContainsColumn(c.table, c.column)) continue; if (!c.json_path.empty()) { source->d_database.exec("UPDATE " + c.table + " SET " + c.column + " = json_replace(" + c.column + ", " + c.json_path + ", json_extract(" + c.column + ", " + c.json_path + ") + ?) " "WHERE json_extract(" + c.column + ", " + c.json_path + ") IS NOT NULL", offsetvalue); } else if ((c.flags & SET_UNIQUELY)) { // set all values negative source->d_database.exec("UPDATE " + c.table + " SET " + c.column + " = " + c.column + " * -1" + (c.whereclause.empty() ? "" : " WHERE " + c.whereclause)); // set to wanted value source->d_database.exec("UPDATE " + c.table + " SET " + c.column + " = " + c.column + " * -1 + ?" + (c.whereclause.empty() ? "" : " WHERE " + c.whereclause), offsetvalue); } else source->d_database.exec("UPDATE " + c.table + " SET " + c.column + " = " + c.column + " + ? " + (c.whereclause.empty() ? "" : " WHERE " + c.whereclause), offsetvalue); int count = source->d_database.changed(); if (count) Logger::message(" Adjusted '", c.table, ".", c.column, "' to match changes in '", dbl.table, "' : ", count); } } if (dbl.table == d_part_table) { // update rowid's in d_attachments std::map, DeepCopyingUniquePtr> newattdb; for (auto &att : source->d_attachments) { AttachmentFrame *af = reinterpret_cast(att.second.release()); af->setRowId(af->rowId() + offsetvalue); int64_t attachmentid = af->attachmentId(); newattdb.emplace(std::make_pair(af->rowId(), attachmentid ? attachmentid : -1), af); } source->d_attachments.clear(); source->d_attachments = std::move(newattdb); } else if (dbl.table == "sticker") { // update id's in d_stickers std::map> newsdb; for (auto &s : source->d_stickers) { StickerFrame *sf = reinterpret_cast(s.second.release()); sf->setRowId(sf->rowId() + offsetvalue); newsdb.emplace(std::make_pair(sf->rowId(), sf)); } source->d_stickers.clear(); source->d_stickers = std::move(newsdb); } else if (dbl.table == "recipient") { source->updateGroupMembers(offsetvalue); // in groups, during the v1 -> v2 update, members may have been removed from the group, these messages // are of type "GV1_MIGRATION_TYPE" and have a body that looks like '_id,_id,...|_id,_id,_id,...' (I think, I have // not seen one with more than 1 id). These id_s must also be updated. source->updateGV1MigrationMessage(offsetvalue); //update (old-style)reaction authors source->updateReactionAuthors(offsetvalue); source->updateAvatars(offsetvalue); source->updateSnippetExtrasRecipient(offsetvalue); } // compact table if requested if (!(dbl.flags & NO_COMPACT)) source->compactIds(dbl.table, dbl.column); } /* CHECK! These are the tables that are imported by importThread(), check if they are all handled properly */ // get tables std::string q("SELECT sql, name, type FROM sqlite_master"); SqliteDB::QueryResults results; source->d_database.exec(q, &results); std::vector tables; for (unsigned int i = 0; i < results.rows(); ++i) { if (!results.isNull(i, 0)) { //Logger::message("Dealing with: ", results.getValueAs(i, 1)); if (results.valueHasType(i, 1) && (results.getValueAs(i, 1) != "sms_fts" && STRING_STARTS_WITH(results.getValueAs(i, 1), "sms_fts"))) ;//Logger::message("Skipping ", results[i][1].second, " because it is sms_ftssecrettable"); else if (results.valueHasType(i, 1) && (results.getValueAs(i, 1) != d_mms_table + "_fts" && STRING_STARTS_WITH(results.getValueAs(i, 1), d_mms_table + "_fts"))) ;//Logger::message("Skipping ", results[i][1].second, " because it is mms_ftssecrettable"); else if (results.valueHasType(i, 1) && (results.getValueAs(i, 1) != "emoji_search" && STRING_STARTS_WITH(results.getValueAs(i, 1), "emoji_search"))) ;//Logger::message("Skipping ", results.getValueAs(i, 1), " because it is emoji_search_ftssecrettable"); else if (results.valueHasType(i, 1) && STRING_STARTS_WITH(results.getValueAs(i, 1), "sqlite_")) ; else if (results.valueHasType(i, 2) && results.getValueAs(i, 2) == "table") tables.emplace_back(results.getValueAs(i, 1)); } } for (std::string const &table : tables) { if (table == "signed_prekeys" || table == "one_time_prekeys" || table == "sessions" || //table == "job_spec" || // this is in the official export. But it makes testing more difficult. it //table == "constraint_spec" || // should be ok to export these (if present in source), since we are only //table == "dependency_spec" || // dealing with exported backups (not from live installations) -> they should //table == "emoji_search" || // have been excluded + the official import should be able to deal with them STRING_STARTS_WITH(table, "sms_fts") || STRING_STARTS_WITH(table, d_mms_table + "_fts") || STRING_STARTS_WITH(table, "sqlite_")) continue; if (std::find_if(s_databaselinks.begin(), s_databaselinks.end(), [table](DatabaseLink const &d){ return d.table == table; }) == s_databaselinks.end()) Logger::warning("Found table unhandled by ", __FUNCTION__ , " : ", table); } } signalbackup-tools-20250313-1/signalbackup/makeprintable.cc000066400000000000000000000043501476450434500235200ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" std::string SignalBackup::makePrintable(std::string const &in) const { std::string printable_uuid(in); unsigned int offset = (STRING_STARTS_WITH(in, "__signal_group__v2__!") ? STRLEN("__signal_group__v2__!") + 4 : (STRING_STARTS_WITH(in, "__textsecure_group__!") ? STRLEN("__textsecure_group__!") + 4 : (STRING_STARTS_WITH(in, "PNI:") ? STRLEN("PNI:") + 4 : 4))); if (STRING_STARTS_WITH(in, "__signal_group__v2__!") || // new group STRING_STARTS_WITH(in, "__textsecure_group__!") || // old group STRING_STARTS_WITH(in, "PNI:") || // pni (std::all_of(printable_uuid.begin(), printable_uuid.end(), [](char c){ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-'; }) && printable_uuid.size() == 36)) //uuid { if (offset < in.size()) [[likely]] std::replace_if(printable_uuid.begin() + offset, printable_uuid.end(), [](char c){ return c != '-'; }, 'x'); else // almost possible I think... printable_uuid = "xxx"; } else if (std::all_of(printable_uuid.begin(), printable_uuid.end(), [](char c){ return (c >= '0' && c <= '9') || c == ' ' || c == '-' || c == '+' || c == '~'; }) && printable_uuid.size() >= 10) std::replace_if(printable_uuid.begin(), printable_uuid.end() - 4, [](char c){ return (c >= '0' && c <= '9'); }, 'x'); else if (in.empty()) printable_uuid = "(empty)"; else printable_uuid = "xxx"; return printable_uuid; } signalbackup-tools-20250313-1/signalbackup/mergegroups.cc000066400000000000000000000146241476450434500232460ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::mergeGroups(std::vector const &groupids) { if (groupids.size() < 2) { Logger::error("Too few addresses"); return; } Logger::message("\nTHIS FUNCTION MAY NEED UPDATING. PLEASE OPEN AN ISSUE\n" "IF YOU NEED IT.\n"); std::string targetgroup = groupids.back(); SqliteDB::QueryResults res; std::set targetmembersvec; if (d_database.tableContainsColumn("groups", "members")) { d_database.exec("SELECT members FROM groups WHERE group_id = ?", targetgroup, &res); std::string targetmembers = res.getValueAs(0, 0); std::stringstream ss(targetmembers); while (ss.good()) { std::string substr; std::getline(ss, substr, ','); targetmembersvec.insert(substr); } } else { d_database.exec("SELECT DISTINCT recipient_id FROM group_membership WHERE group_id = ?", targetgroup, &res); for (unsigned int i = 0; i < res.rows(); ++i) targetmembersvec.insert(res.valueAsString(i, 0)); } // get the thread_id of the target long long int tid = getThreadIdFromRecipient(targetgroup); if (tid != -1) { // update all messages from this addresses[i] to belong to that same thread and change address in new number for (unsigned int i = 0; i < groupids.size() - 1; ++i) { long long int oldtid = getThreadIdFromRecipient(groupids[i]); if (oldtid == -1) { Logger::error("Failed to find thread for old group: ", groupids[i]); continue; } Logger::message("Dealing with group: ", groupids[i]); if (d_database.containsTable("sms")) { d_database.exec("UPDATE sms SET thread_id = ? WHERE thread_id = ?", {tid, oldtid}); Logger::message("Updated ", d_database.changed(), " entries in 'sms' table"); d_database.exec("UPDATE sms SET " + d_sms_recipient_id + " = ? WHERE " + d_sms_recipient_id + " = ?", {targetgroup, groupids[i]}); Logger::message("Updated ", d_database.changed(), " entries in 'sms' table"); } d_database.exec("UPDATE " + d_mms_table + " SET thread_id = ? WHERE thread_id = ?", {tid, oldtid}); Logger::message("Updated ", d_database.changed(), " entries in 'mms' table"); if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) // < dbv185 { d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_recipient_id + " = ? WHERE " + d_mms_recipient_id + " = ?", {targetgroup, groupids[i]}); Logger::message("Updated ", d_database.changed(), " entries in 'mms' table"); } else { // adjust to_recipient (= group id on outgoing messages) d_database.exec("UPDATE " + d_mms_table + " SET to_recipient_id = ? WHERE to_recipient_id = ?", {targetgroup, groupids[i]}); Logger::message("Updated ", d_database.changed(), " entries in 'mms' table"); } if (d_database.containsTable("mention")) { d_database.exec("UPDATE mention SET thread_id = ? WHERE thread_id = ?", {tid, oldtid}); Logger::message("Updated ", d_database.changed(), " entries in 'sms' table"); } if (d_database.containsTable("msl_recipient")) d_database.exec("UPDATE msl_recipient SET recipient_id = ? WHERE recipient_id = ?", {targetgroup, groupids[i]}); if (d_database.containsTable("reaction")) // dbv >= 121 d_database.exec("UPDATE reaction SET author_id = ? WHERE author_id = ?", {targetgroup, groupids[i]}); // delete old (now empty) thread d_database.exec("DELETE FROM thread WHERE " + d_thread_recipient_id + " = ?", groupids[i]); Logger::message("Removed ", d_database.changed(), " threads from table"); // get members of groupids[i] and merge them into targetgroup if (d_database.tableContainsColumn("groups", "members")) { d_database.exec("SELECT members FROM groups WHERE group_id = ?", groupids[i], &res); std::string members = res.getValueAs(0, 0); std::stringstream ss2(members); while (ss2.good()) { std::string substr; std::getline(ss2, substr, ','); auto [it, inserted] = targetmembersvec.insert(substr); if (inserted) Logger::message("Added ", substr, " to memberlist of group"); else Logger::message("Skipped adding ", substr, " to group: already a member"); } } else { d_database.exec("SELECT DISTINCT recipient_id FROM group_membership WHERE group_id = ?", groupids[i], &res); for (unsigned int g = 0; g < res.rows(); ++g) d_database.exec("INSERT OR IGNORE INTO group_membership (group_id, recipient_id) VALUES (?, ?)", {targetgroup, res.getValueAs(g, "recipient_id")}); } // delete the merged group d_database.exec("DELETE FROM groups WHERE group_id = ?", groupids[i]); Logger::message("Removed ", d_database.changed(), " groups from table"); if (d_database.containsTable("group_membership")) d_database.exec("DELETE FROM group_membership WHERE group_id = ?", groupids[i]); } // set new member list if (d_database.tableContainsColumn("groups", "members")) { Logger::message("Setting new memberlist"); std::string newmemberlist; for (auto const &it : targetmembersvec) newmemberlist += it + ','; newmemberlist.pop_back(); // remove trailing comma... d_database.exec("UPDATE groups SET members = ? WHERE group_id = ?", {newmemberlist, targetgroup}); } } else { Logger::warning("No group thread with id ", tid, " found"); } updateThreadsEntries(); } signalbackup-tools-20250313-1/signalbackup/mergerecipients.cc000066400000000000000000000415231476450434500240720ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::mergeRecipients(std::vector const &addresses/*, bool editgroupmembers*/) // addresses is list of phone numbers { Logger::message(__FUNCTION__); if (addresses.size() != 2) { Logger::error("Need exactly two recipient ID's"); return false; } std::vector r_ids = addresses; std::vector phonenumbers = addresses; // for database version >= 24, addresses = recipient_ids, for db version < 24 addresses = recipient.phone // so convert to recipient._ids if (d_databaseversion >= 24) { for (unsigned int i = 0; i < r_ids.size(); ++i) { SqliteDB::QueryResults res; d_database.exec("SELECT _id FROM recipient WHERE " + d_recipient_e164 + " = ?", r_ids[i], &res); if (res.rows() != 1 || res.columns() != 1 || !res.valueHasType(0, 0)) { Logger::error("Failed to find recipient._id matching phone in target database: '", r_ids[i], "'"); return false; } r_ids[i] = bepaald::toString(res.getValueAs(0, 0)); } } std::string target_rid = r_ids.back(); std::string targetphone = phonenumbers.back(); // deal with one-on-one conversations: // get thread of target address long long int tid = getThreadIdFromRecipient(target_rid); // update all messages from this r_ids[i] to belong to that same thread and change address in new number for (unsigned int i = 0; i < r_ids.size() - 1; ++i) { Logger::message("Dealing with recipient: ", r_ids[i]); long long int oldtid = getThreadIdFromRecipient(r_ids[i]); // update thread info: if (tid != -1 && oldtid != -1) { // update thread_id in sms table if (d_database.containsTable("sms")) { d_database.exec("UPDATE sms SET thread_id = ? WHERE thread_id = ?", {tid, oldtid}); Logger::message("Updated ", d_database.changed(), " thread_ids in 'sms'"); } // update thread_id in message table d_database.exec("UPDATE " + d_mms_table + " SET thread_id = ? WHERE thread_id = ?", {tid, oldtid}); Logger::message("Updated ", d_database.changed(), " thread_ids in '", d_mms_table, "'"); // update thread_id in mention if (d_database.containsTable("mention")) { d_database.exec("UPDATE mention SET thread_id = ? WHERE thread_id = ?", {tid, oldtid}); Logger::message("Updated ", d_database.changed(), " thread_ids in 'mention'"); } } // Update recipient_ids if (d_database.containsTable("sms")) { d_database.exec("UPDATE sms SET " + d_sms_recipient_id + " = ? WHERE " + d_sms_recipient_id + " = ?", {target_rid, r_ids[i]}); Logger::message("Updated ", d_database.changed(), " recipients in 'sms' table"); } if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) // < dbv 185 { d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_recipient_id + " = ? WHERE " + d_mms_recipient_id + " = ?", {target_rid, r_ids[i]}); Logger::message("Updated ", d_database.changed(), " recipients in '", d_mms_table, "' table"); } else { int count = 0; d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_recipient_id + " = ? WHERE " + d_mms_recipient_id + " = ?", {target_rid, r_ids[i]}); count += d_database.changed(); d_database.exec("UPDATE " + d_mms_table + " SET to_recipient_id = ? WHERE to_recipient_id = ?", {target_rid, r_ids[i]}); Logger::message("Updated ", count + d_database.changed(), " recipients in '", d_mms_table, "' table"); } // change quote author if (d_database.tableContainsColumn(d_mms_table, "quote_author")) { d_database.exec("UPDATE " + d_mms_table + " SET quote_author = ? WHERE quote_author = ?", {target_rid, r_ids[i]}); Logger::message("Updated ", d_database.changed(), " quote_authors"); } // change msl_recipient if (d_database.containsTable("msl_recipient")) { d_database.exec("UPDATE msl_recipient SET recipient_id = ? WHERE recipient_id = ?", {target_rid, r_ids[i]}); Logger::message("Updated ", d_database.changed(), " msl_recipients"); } if (d_database.containsTable("mention")) { d_database.exec("UPDATE mention SET recipient_id = ? WHERE recipient_id = ?", {target_rid, r_ids[i]}); Logger::message("Updated ", d_database.changed(), " entries in 'mention' table"); } if (d_database.containsTable("reaction")) { d_database.exec("UPDATE reaction SET author_id = ? WHERE author_id = ?", {target_rid, r_ids[i]}); Logger::message("Updated ", d_database.changed(), " reaction authors table"); } if (d_database.containsTable("call")) { d_database.exec("UPDATE call SET peer = ? WHERE peer = ?", {target_rid, r_ids[i]}); Logger::message("Updated ", d_database.changed(), " recipients in 'call' table"); } if (d_database.containsTable("notification_profile_allowed_members")) { d_database.exec("UPDATE notification_profile_allowed_members SET recipient_id = ? WHERE recipient_id = ?", {target_rid, r_ids[i]}); Logger::message("Updated ", d_database.changed(), " entries in 'notification_profile_allowed_members' table"); } // delete old thread, lets make sure it has no messages if (tid != -1 && oldtid != -1) { if (d_database.getSingleResultAs("SELECT COUNT(*) FROM " + d_mms_table + " WHERE thread_id = ?", oldtid, -1) != 0) { Logger::error("Something went wrong: moved all messages to new thread, but old thread is not empty..."); return false; } else { d_database.exec("DELETE FROM thread WHERE _id = ?", oldtid); Logger::message("Deleted ", d_database.changed(), " empty thread(s) from database"); } } } // OLD STYLE REACTIONS (currently reactions are in their own table, before they were in a column in the message tables for (auto const &t : {"sms"s, d_mms_table}) { // update reaction authors if (d_database.tableContainsColumn(t, "reactions")) { SqliteDB::QueryResults results; d_database.exec("SELECT _id, reactions FROM " + t + " WHERE reactions IS NOT NULL", &results); bool changed = false; for (unsigned int i = 0; i < results.rows(); ++i) { ReactionList reactions(results.getValueAs, size_t>>(i, "reactions")); for (unsigned int k = 0; k < reactions.numReactions(); ++k) { for (unsigned int j = 0; j < r_ids.size() - 1; ++j) if (reactions.getAuthor(k) == bepaald::toNumber(r_ids[j])) { reactions.setAuthor(k, bepaald::toNumber(target_rid)); changed = true; } if (changed) d_database.exec("UPDATE " + t + " SET reactions = ? WHERE _id = ?", {std::make_pair(reactions.data(), static_cast(reactions.size())), results.getValueAs(i, "_id")}); } } } } // // deal with groups // SqliteDB::QueryResults results; // d_database.exec("SELECT group_id,members,title FROM groups", &results); // get id,members and title from all groups // for (unsigned int i = 0; i < results.rows(); ++i) // { // if (results.columns() != 3 || // !results.valueHasType(i, 0) || // !results.valueHasType(i, 1) || // !results.valueHasType(i, 2)) // { // Logger::error(":("); // continue; // } // std::string id = results.getValueAs(i, 0); // std::string members = results.getValueAs(i, 1); // std::string title = results.getValueAs(i, 2); // Logger::message("Dealing with group: ", id, " (title: '", title, "', members: ", members, ")"); // std::string recipient_id = id; // if (d_databaseversion >= 24) // { // SqliteDB::QueryResults res; // d_database.exec("SELECT _id FROM recipient WHERE group_id = ?", id, &res); // if (res.rows() != 1 || res.columns() != 1 || // !res.valueHasType(0, 0)) // { // Logger::error("Failed to find recipient._id matching phone/group_id in target database"); // return false; // } // recipient_id = bepaald::toString(res.getValueAs(0, 0)); // } // // get thread id for this group: // tid = getThreadIdFromRecipient(recipient_id); // if (tid == -1) // { // Logger::error("Failed to find thread for groupchat"); // continue; // } // // for all incoming messages of this group(= this thread), if the (originating) address = oldaddress, change it to target // for (unsigned int j = 0; j < r_ids.size() - 1; ++j) // { // if (d_database.containsTable("sms")) // { // d_database.exec("UPDATE sms SET " + d_sms_recipient_id + " = ? " // "WHERE " + d_sms_recipient_id + " = ? AND thread_id = ?", {target_rid, r_ids[j], tid}); // Logger::message("Updated ", d_database.changed(), " entries in 'sms' table"); // } // d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_recipient_id + " = ? " // "WHERE " + d_mms_recipient_id + " = ? AND thread_id = ?", {target_rid, r_ids[j], tid}); // Logger::message("Updated ", d_database.changed(), " entries in '" + d_mms_table + "' table"); // } // // if (editgroupmembers) // // { // // // maybe former_v1_members needs to be adjusted similarly? // // for (unsigned int j = 0; j < r_ids.size() - 1; ++j) // // { // // // change current member list in group database: // // //std::cout << " GROUP MEMBERS BEFORE: " << members << std::endl; // // std::string::size_type pos = std::string::npos; // // if ((pos = members.find(r_ids[j])) != std::string::npos) // // { // // Logger::message(" GROUP MEMBERS BEFORE: ", members); // // //std::cout << " FOUND ADDRESS TO CHANGE" << std::endl; // // // remove address // // members.erase(pos, r_ids[j].length()); // // // remove left over comma // // if (members[0] == ',') // if removed was first // // members.erase(0, 1); // // else if (members.back() == ',') // if removed was last // // members.erase(members.size() - 1, 1); // // else // if removed was middle // // members.erase(std::unique(members.begin(), members.end(), [](char c1, char c2){ return c1 == ',' && c2 == ',';}), members.end()); // // if (members.find(target_rid) == std::string::npos) // else target already in memberlist // // members += "," + target_rid; // // d_database.exec("UPDATE groups SET members = ? WHERE group_id = ?", {members, recipient_id}); // // Logger::message(" GROUP MEMBERS AFTER : ", members); // // } // // } // // } // } /* NOTE The following two routines only work for old-style (v1) group updates. For groupV2 updates, a group update might look something like this: GroupContextV2 statusmsg(body); statusmsg.print(); GROUP V2 Field 1 (optional::bytes): (hex:) 0a 20 f2 f5 8f 60 6e c1 24 [...] ERROR REQUESTED TYPE TOO SMALL (2) Field 3 (optional::bytes): (hex:) 12 03 57 4c 53 1a 49 67 [...] Field 1 (optional::protobuf): Field 1 (optional::bytes): (hex:) f2 f5 8f 60 6e c1 24 99 [...] Field 2 (optional::uint32): 1 Field 2 (optional::protobuf): Field 1 (optional::bytes): (hex:) 6b 1e 76 87 cc [...] Field 2 (optional::uint32): 1 Field 11 (optional::protobuf): Field 1 (optional::string): groups/6KM9eoH7qE6OxmycqQW[...] Field 3 (optional::protobuf): Field 2 (optional::string): GROUPTITLE Field 3 (optional::string): groups/6KM9eoH7qE6OxmycqQW[...] Field 4 (optional::protobuf): Field 5 (optional::protobuf): Field 1 (optional::enum): 2 Field 2 (optional::enum): 2 Field 6 (optional::uint32): 1 Field 7 (repeated::protobuf) (1/3): Field 1 (optional::bytes): (hex:) 93 72 22 73 78 [...] Field 2 (optional::enum): 2 Field 3 (optional::bytes): (hex:) f6 3f 8f 7b a9 a4 [...] Field 7 (repeated::protobuf) (2/3): Field 1 (optional::bytes): (hex:) 60 f8 08 1b 9f 25 [...] Field 2 (optional::enum): 2 Field 3 (optional::bytes): (hex:) 7e 21 ca f8 cb a8 d[...] Field 7 (repeated::protobuf) (3/3): Field 1 (optional::bytes): (hex:) 6b 1e 76 87 cc 0d [...] Field 2 (optional::enum): 2 Field 3 (optional::bytes): (hex:) 88 28 52 88 87 2c [...] Field 4 (optional::protobuf): Field 2 (optional::string): WLS Field 3 (optional::string): groups/6KM9eoH7qE6OxmycqQW[...] Field 4 (optional::protobuf): Field 5 (optional::protobuf): Field 1 (optional::enum): 2 Field 2 (optional::enum): 2 Field 7 (repeated::protobuf) (1/3): Field 1 (optional::bytes): (hex:) 93 72 22 73 78 e3 [...] Field 2 (optional::enum): 2 Field 3 (optional::bytes): (hex:) f6 3f 8f 7b a9 a4 [...] Field 7 (repeated::protobuf) (2/3): Field 1 (optional::bytes): (hex:) 60 f8 08 1b 9f 25 [...] Field 2 (optional::enum): 2 Field 3 (optional::bytes): (hex:) 7e 21 ca f8 cb a8 [...] Field 7 (repeated::protobuf) (3/3): Field 1 (optional::bytes): (hex:) 6b 1e 76 87 cc 0d [...] Field 2 (optional::enum): 2 Field 3 (optional::bytes): (hex:) 88 28 52 88 87 2c [...] Where the repeating fields 3.7.1 & 4.7.1 corresponds to uuid as found in recipient.uuid */ // get groupV1 status message updates: SqliteDB::QueryResults results2; std::pair smsquery("sms", "SELECT type,body,_id FROM 'sms' " "WHERE thread_id = " + bepaald::toString(tid) + " AND " "(type & " + bepaald::toString(Types::GROUP_UPDATE_BIT) + ") IS NOT 0 AND " "(type & " + bepaald::toString(Types::GROUP_V2_BIT) + ") IS 0"); std::pair mmsquery(d_mms_table, "SELECT " + d_mms_type + " AS type,body,_id FROM '" + d_mms_table + "' " "WHERE thread_id = " + bepaald::toString(tid) + " AND " "(" + d_mms_type + " & " + bepaald::toString(Types::GROUP_UPDATE_BIT) + ") IS NOT 0 AND " "(" + d_mms_type + " & " + bepaald::toString(Types::GROUP_V2_BIT) + ") IS 0"); for (auto const &d : {smsquery, mmsquery}) { if (d_database.containsTable(d.first)) { d_database.exec(d.second, &results2); if (d_verbose) [[unlikely]] results2.prettyPrint(d_truncate); for (unsigned int j = 0; j < results2.rows(); ++j) { std::string body = std::any_cast(results2.value(j, "body")); long long int type = std::any_cast(results2.value(j, "type")); long long int msgid = std::any_cast(results2.value(j, "_id")); GroupContext statusmsg(body); if (Types::isGroupUpdate(type)) Logger::message("Handling group update ", j + 1); bool targetpresent = false; auto field4 = statusmsg.getField<4>(); for (unsigned int k = 0; k < field4.size(); ++k) { Logger::message("memberlist: ", field4[k]); if (field4[k] == targetphone) targetpresent = true; } int removed = 0; for (unsigned int k = 0; k < phonenumbers.size() - 1; ++k) { removed = statusmsg.deleteFields(4, &phonenumbers[k]); Logger::message("deleted ", removed, " members from group update message"); } if (removed) { if (!targetpresent) // add target if not present statusmsg.addField<4>(targetphone); // set body d_database.exec("UPDATE " + d.first + " SET body = ? WHERE _id = ?", {statusmsg.getDataString(), msgid}); Logger::message("Updated ", d_database.changed(), " group updates in '", d.first, "' table"); } } } } return true; } signalbackup-tools-20250313-1/signalbackup/migrate_to_191.cc000066400000000000000000003230421476450434500234300ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ /******************** * * * DEPRECATED * * * * the issue this * * option exists to * * work around has * * been fixed in * * Signal * * * ********************/ #include "signalbackup.ih" #include #include #if __cpp_lib_format >= 201907L #include #else #include #include #endif bool SignalBackup::migrate_to_191(std::string const &selfphone) { if (d_databaseversion < 98) { Logger::error("Sorry, db version too old. Not supported (yet?)"); return false; } if (selfphone.empty()) { Logger::error("Please provide phone of self with the `--setselfid' option (eg.: `--setself \"+31612345678\"')"); return false; } /* if (d_databaseversion < ) { Logger::message("To "); if (!d_database.exec()) return false; } */ if (d_databaseversion < 98) { Logger::message("To 98"); if (!d_database.exec("UPDATE recipient SET storage_service_key = NULL WHERE storage_service_key IS NOT NULL AND (group_type = 1 OR (group_type = 0 AND phone IS NULL AND uuid IS NULL))")) return false; } if (d_databaseversion < 99) { Logger::message("To 99"); if (!d_database.exec("ALTER TABLE sms ADD COLUMN server_guid TEXT DEFAULT NULL") || !d_database.exec("ALTER TABLE mms ADD COLUMN server_guid TEXT DEFAULT NULL")) return false; } if (d_databaseversion < 100) { Logger::message("To 100"); if (!d_database.exec("ALTER TABLE recipient ADD COLUMN chat_colors BLOB DEFAULT NULL") || !d_database.exec("ALTER TABLE recipient ADD COLUMN custom_chat_colors_id INTEGER DEFAULT 0") || !d_database.exec("CREATE TABLE chat_colors (_id INTEGER PRIMARY KEY AUTOINCREMENT,chat_colors BLOB)")) return false; // NOTE skipping the rest here, people can just set new chat_colors if they want... } if (d_databaseversion < 101) { Logger::message("To 101"); SqliteDB::QueryResults recipients_without_color; if (!d_database.exec("SELECT _id FROM recipient WHERE color IS NULL", &recipients_without_color)) return false; std::vector avatar_color_options{"C000", "C010", "C020", "C030", "C040", "C050", "C060", "C070", "C080", "C090", "C100", "C110", "C120", "C130", "C140", "C150", "C160", "C170", "C180", "C190", "C200", "C210", "C220", "C230", "C240", "C250", "C260", "C270", "C280", "C290", "C300", "C310", "C320", "C330", "C340", "C350"}; for (unsigned int i = 0; i < recipients_without_color.rows(); ++i) { uint8_t random_idx = 0; if (RAND_bytes(&random_idx, 1) != 1) Logger::warning("failed to generate random number"); random_idx = (static_cast(random_idx) / (255 + 1)) * ((avatar_color_options.size() - 1) - 0 + 1) + 0; if (!d_database.exec("UPDATE recipient SET color = ? WHERE _id = ?", {avatar_color_options[random_idx], recipients_without_color.value(i, "_id")})) return false; } } if (d_databaseversion < 102) { Logger::message("To 102"); if (!d_database.exec("CREATE VIRTUAL TABLE emoji_search USING fts5(label, emoji UNINDEXED)")) return false; } // QUESTIONABLE if (d_databaseversion < 103) { Logger::message_start("To 103"); if (!d_database.containsTable("sender_keys")) { Logger::message_end(" (really)"); if (!d_database.exec("CREATE TABLE sender_keys ( _id INTEGER PRIMARY KEY AUTOINCREMENT, recipient_id INTEGER NOT NULL, device INTEGER NOT NULL, distribution_id TEXT NOT NULL, record BLOB NOT NULL, created_at INTEGER NOT NULL, UNIQUE(recipient_id, device, distribution_id) ON CONFLICT REPLACE)")) return false; if (!d_database.exec("CREATE TABLE sender_key_shared ( _id INTEGER PRIMARY KEY AUTOINCREMENT, distribution_id TEXT NOT NULL, address TEXT NOT NULL, device INTEGER NOT NULL, UNIQUE(distribution_id, address, device) ON CONFLICT REPLACE )")) return false; if (!d_database.exec("CREATE TABLE pending_retry_receipts ( _id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT NOT NULL, device INTEGER NOT NULL, sent_timestamp INTEGER NOT NULL, received_timestamp TEXT NOT NULL, thread_id INTEGER NOT NULL, UNIQUE(author, sent_timestamp) ON CONFLICT REPLACE );")) return false; if (!d_database.exec("ALTER TABLE groups ADD COLUMN distribution_id TEXT DEFAULT NULL") || !d_database.exec("CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON groups (distribution_id)")) return false; union { struct { uint32_t time_low; uint16_t time_mid; uint16_t time_hi_and_version; uint8_t clk_seq_hi_res; uint8_t clk_seq_low; uint8_t node[6]; } uuidstruct; uint8_t rnd[16]; } uuid; SqliteDB::QueryResults group_results; if (!d_database.exec("SELECT group_id FROM groups WHERE LENGTH(group_id) = 85", &group_results)) return false; for (unsigned int i = 0; i < group_results.rows(); ++i) { // generate a new uuid to use as distribution_id if (RAND_bytes(uuid.rnd, sizeof(uuid)) != 1) { Logger::error("Failed to generate 16 random bytes (2)"); return false; } // Refer Section 4.2 of RFC-4122 // https://tools.ietf.org/html/rfc4122#section-4.2 uuid.uuidstruct.clk_seq_hi_res = (uint8_t) ((uuid.uuidstruct.clk_seq_hi_res & 0x3F) | 0x80); uuid.uuidstruct.time_hi_and_version = (uint16_t) ((uuid.uuidstruct.time_hi_and_version & 0x0FFF) | 0x4000); #if __cpp_lib_format >= 201907L std::string distribution_id = std::format("{:0>8x}-{:0>4x}-{:0>4x}-{:0>2x}{:0>2x}-{:0>2x}{:0>2x}{:0>2x}{:0>2x}{:0>2x}{:0>2x}", uuid.uuidstruct.time_low, uuid.uuidstruct.time_mid, uuid.uuidstruct.time_hi_and_version, uuid.uuidstruct.clk_seq_hi_res, uuid.uuidstruct.clk_seq_low, uuid.uuidstruct.node[0], uuid.uuidstruct.node[1], uuid.uuidstruct.node[2], uuid.uuidstruct.node[3], uuid.uuidstruct.node[4], uuid.uuidstruct.node[5]); #else int size = 1 + std::snprintf(nullptr, 0, "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x", uuid.uuidstruct.time_low, uuid.uuidstruct.time_mid, uuid.uuidstruct.time_hi_and_version, uuid.uuidstruct.clk_seq_hi_res, uuid.uuidstruct.clk_seq_low, uuid.uuidstruct.node[0], uuid.uuidstruct.node[1], uuid.uuidstruct.node[2], uuid.uuidstruct.node[3], uuid.uuidstruct.node[4], uuid.uuidstruct.node[5]); if (size <= 0) { Logger::error("Failed to get size of uuid"); return false; } std::unique_ptr uuid_char(new char[size]); if (std::snprintf(uuid_char.get(), size, "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x", uuid.uuidstruct.time_low, uuid.uuidstruct.time_mid, uuid.uuidstruct.time_hi_and_version, uuid.uuidstruct.clk_seq_hi_res, uuid.uuidstruct.clk_seq_low, uuid.uuidstruct.node[0], uuid.uuidstruct.node[1], uuid.uuidstruct.node[2], uuid.uuidstruct.node[3], uuid.uuidstruct.node[4], uuid.uuidstruct.node[5]) < 0) { Logger::error("failed to format UUID"); return false; } std::string distribution_id(uuid_char.get(), uuid_char.get() + size - 1); #endif if (!d_database.exec("UPDATE groups SET distribution_id = ? WHERE group_id = ?", {distribution_id, group_results.value(i, "group_id")})) return false; } } else Logger::message_end("... (not)"); } if (d_databaseversion < 104) { Logger::message("To 104"); if (!d_database.exec("DROP INDEX sms_date_sent_index") || !d_database.exec("CREATE INDEX sms_date_sent_index on sms(date_sent, address, thread_id)") || !d_database.exec("DROP INDEX mms_date_sent_index") || !d_database.exec("CREATE INDEX mms_date_sent_index on mms(date, address, thread_id)")) return false; } if (d_databaseversion < 105) { Logger::message("To 105"); if (!d_database.exec("CREATE TABLE message_send_log ( _id INTEGER PRIMARY KEY, date_sent INTEGER NOT NULL, content BLOB NOT NULL, related_message_id INTEGER DEFAULT -1, is_related_message_mms INTEGER DEFAULT 0, content_hint INTEGER NOT NULL, group_id BLOB DEFAULT NULL )")) return false; if (!d_database.exec("CREATE INDEX message_log_date_sent_index ON message_send_log (date_sent)") || !d_database.exec("CREATE INDEX message_log_related_message_index ON message_send_log (related_message_id, is_related_message_mms)") || !d_database.exec("CREATE TRIGGER msl_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM message_send_log WHERE related_message_id = old._id AND is_related_message_mms = 0; END") || !d_database.exec("CREATE TRIGGER msl_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM message_send_log WHERE related_message_id = old._id AND is_related_message_mms = 1; END")) return false; if (!d_database.exec("CREATE TABLE message_send_log_recipients ( _id INTEGER PRIMARY KEY, message_send_log_id INTEGER NOT NULL REFERENCES message_send_log (_id) ON DELETE CASCADE, recipient_id INTEGER NOT NULL, device INTEGER NOT NULL )")) return false; if (!d_database.exec("CREATE INDEX message_send_log_recipients_recipient_index ON message_send_log_recipients (recipient_id, device)")) return false; } if (d_databaseversion < 106) { Logger::message("To 106"); if (!d_database.exec("DROP TABLE message_send_log") || !d_database.exec("DROP INDEX IF EXISTS message_log_date_sent_index") || !d_database.exec("DROP INDEX IF EXISTS message_log_related_message_index") || !d_database.exec("DROP TRIGGER msl_sms_delete") || !d_database.exec("DROP TRIGGER msl_mms_delete") || !d_database.exec("DROP TABLE message_send_log_recipients") || !d_database.exec("DROP INDEX IF EXISTS message_send_log_recipients_recipient_index")) return false; if (!d_database.exec("CREATE TABLE msl_payload ( _id INTEGER PRIMARY KEY, date_sent INTEGER NOT NULL, content BLOB NOT NULL, content_hint INTEGER NOT NULL )")) return false; if (!d_database.exec("CREATE INDEX msl_payload_date_sent_index ON msl_payload (date_sent)")) return false; if (!d_database.exec("CREATE TABLE msl_recipient ( _id INTEGER PRIMARY KEY, payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE, recipient_id INTEGER NOT NULL, device INTEGER NOT NULL)")) return false; if (!d_database.exec("CREATE INDEX msl_recipient_recipient_index ON msl_recipient (recipient_id, device, payload_id)") || !d_database.exec("CREATE INDEX msl_recipient_payload_index ON msl_recipient (payload_id)")) return false; if (!d_database.exec("CREATE TABLE msl_message ( _id INTEGER PRIMARY KEY, payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE, message_id INTEGER NOT NULL, is_mms INTEGER NOT NULL )")) return false; if (!d_database.exec("CREATE INDEX msl_message_message_index ON msl_message (message_id, is_mms, payload_id)") || !d_database.exec("CREATE TRIGGER msl_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id AND is_mms = 0); END") || !d_database.exec("CREATE TRIGGER msl_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id AND is_mms = 1); END") || !d_database.exec("CREATE TRIGGER msl_attachment_delete AFTER DELETE ON part BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old.mid AND is_mms = 1); END")) return false; } if (d_databaseversion < 107) { Logger::message("To 107"); if (!d_database.exec("DELETE FROM sms WHERE thread_id NOT IN (SELECT _id FROM thread)")) return false; if (!d_database.exec("DELETE FROM mms WHERE thread_id NOT IN (SELECT _id FROM thread)")) return false; } if (d_databaseversion < 108) { Logger::message("To 108"); if (!d_database.exec("CREATE TABLE thread_tmp ( _id INTEGER PRIMARY KEY AUTOINCREMENT, date INTEGER DEFAULT 0, thread_recipient_id INTEGER, message_count INTEGER DEFAULT 0, snippet TEXT, snippet_charset INTEGER DEFAULT 0, snippet_type INTEGER DEFAULT 0, snippet_uri TEXT DEFAULT NULL, snippet_content_type INTEGER DEFAULT NULL, snippet_extras TEXT DEFAULT NULL, read INTEGER DEFAULT 1, type INTEGER DEFAULT 0, error INTEGER DEFAULT 0, archived INTEGER DEFAULT 0, status INTEGER DEFAULT 0, expires_in INTEGER DEFAULT 0, last_seen INTEGER DEFAULT 0, has_sent INTEGER DEFAULT 0, delivery_receipt_count INTEGER DEFAULT 0, read_receipt_count INTEGER DEFAULT 0, unread_count INTEGER DEFAULT 0, last_scrolled INTEGER DEFAULT 0, pinned INTEGER DEFAULT 0 )")) return false; if (!d_database.exec("INSERT INTO thread_tmp SELECT _id, date, recipient_ids, message_count, snippet, snippet_cs, snippet_type, snippet_uri, snippet_content_type, snippet_extras, read, type, error, archived, status, expires_in, last_seen, has_sent, delivery_receipt_count, read_receipt_count, unread_count, last_scrolled, pinned FROM thread ")) return false; if (!d_database.exec("DROP TABLE thread") || !d_database.exec("ALTER TABLE thread_tmp RENAME TO thread")) return false; if (!d_database.exec("CREATE INDEX thread_recipient_id_index ON thread (thread_recipient_id)") || !d_database.exec("CREATE INDEX archived_count_index ON thread (archived, message_count)") || !d_database.exec("CREATE INDEX thread_pinned_index ON thread (pinned)")) return false; if (!d_database.exec("DELETE FROM remapped_threads")) return false; } if (d_databaseversion < 109) { Logger::message("To 109"); if (!d_database.exec("CREATE TABLE mms_tmp ( _id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER, date INTEGER, date_received INTEGER, date_server INTEGER DEFAULT -1, msg_box INTEGER, read INTEGER DEFAULT 0, body TEXT, part_count INTEGER, ct_l TEXT, address INTEGER, address_device_id INTEGER, exp INTEGER, m_type INTEGER, m_size INTEGER, st INTEGER, tr_id TEXT, delivery_receipt_count INTEGER DEFAULT 0, mismatched_identities TEXT DEFAULT NULL, network_failures TEXT DEFAULT NULL, subscription_id INTEGER DEFAULT -1, expires_in INTEGER DEFAULT 0, expire_started INTEGER DEFAULT 0, notified INTEGER DEFAULT 0, read_receipt_count INTEGER DEFAULT 0, quote_id INTEGER DEFAULT 0, quote_author TEXT, quote_body TEXT, quote_attachment INTEGER DEFAULT -1, quote_missing INTEGER DEFAULT 0, quote_mentions BLOB DEFAULT NULL, shared_contacts TEXT, unidentified INTEGER DEFAULT 0, previews TEXT, reveal_duration INTEGER DEFAULT 0, reactions BLOB DEFAULT NULL, reactions_unread INTEGER DEFAULT 0, reactions_last_seen INTEGER DEFAULT -1, remote_deleted INTEGER DEFAULT 0, mentions_self INTEGER DEFAULT 0, notified_timestamp INTEGER DEFAULT 0, viewed_receipt_count INTEGER DEFAULT 0, server_guid TEXT DEFAULT NULL );")) return false; if (!d_database.exec("INSERT INTO mms_tmp SELECT _id, thread_id, date, date_received, date_server, msg_box, read, body, part_count, ct_l, address, address_device_id, exp, m_type, m_size, st, tr_id, delivery_receipt_count, mismatched_identities, network_failures, subscription_id, expires_in, expire_started, notified, read_receipt_count, quote_id, quote_author, quote_body, quote_attachment, quote_missing, quote_mentions, shared_contacts, unidentified, previews, reveal_duration, reactions, reactions_unread, reactions_last_seen, remote_deleted, mentions_self, notified_timestamp, viewed_receipt_count, server_guid FROM mms")) return false; if (!d_database.exec("DROP TABLE mms") || !d_database.exec("ALTER TABLE mms_tmp RENAME TO mms")) return false; if (!d_database.exec("CREATE INDEX mms_read_and_notified_and_thread_id_index ON mms(read, notified, thread_id)") || !d_database.exec("CREATE INDEX mms_message_box_index ON mms (msg_box)") || !d_database.exec("CREATE INDEX mms_date_sent_index ON mms (date, address, thread_id)") || !d_database.exec("CREATE INDEX mms_date_server_index ON mms (date_server)") || !d_database.exec("CREATE INDEX mms_thread_date_index ON mms (thread_id, date_received)") || !d_database.exec("CREATE INDEX mms_reactions_unread_index ON mms (reactions_unread)")) return false; if (!d_database.exec("CREATE TRIGGER mms_ai AFTER INSERT ON mms BEGIN INSERT INTO mms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END") || !d_database.exec("CREATE TRIGGER mms_ad AFTER DELETE ON mms BEGIN INSERT INTO mms_fts(mms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); END") || !d_database.exec("CREATE TRIGGER mms_au AFTER UPDATE ON mms BEGIN INSERT INTO mms_fts(mms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); INSERT INTO mms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END") || !d_database.exec("CREATE TRIGGER msl_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id AND is_mms = 1); END")) return false; if (!d_database.exec("CREATE TABLE sms_tmp ( _id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER, address INTEGER, address_device_id INTEGER DEFAULT 1, person INTEGER, date INTEGER, date_sent INTEGER, date_server INTEGER DEFAULT -1, protocol INTEGER, read INTEGER DEFAULT 0, status INTEGER DEFAULT -1, type INTEGER, reply_path_present INTEGER, delivery_receipt_count INTEGER DEFAULT 0, subject TEXT, body TEXT, mismatched_identities TEXT DEFAULT NULL, service_center TEXT, subscription_id INTEGER DEFAULT -1, expires_in INTEGER DEFAULT 0, expire_started INTEGER DEFAULT 0, notified DEFAULT 0, read_receipt_count INTEGER DEFAULT 0, unidentified INTEGER DEFAULT 0, reactions BLOB DEFAULT NULL, reactions_unread INTEGER DEFAULT 0, reactions_last_seen INTEGER DEFAULT -1, remote_deleted INTEGER DEFAULT 0, notified_timestamp INTEGER DEFAULT 0, server_guid TEXT DEFAULT NULL )")) return false; if (!d_database.exec("INSERT INTO sms_tmp SELECT _id, thread_id, address, address_device_id, person, date, date_sent, date_server , protocol, read, status , type, reply_path_present, delivery_receipt_count, subject, body, mismatched_identities, service_center, subscription_id , expires_in, expire_started, notified, read_receipt_count, unidentified, reactions BLOB, reactions_unread, reactions_last_seen , remote_deleted, notified_timestamp, server_guid FROM sms")) return false; if (!d_database.exec("DROP TABLE sms") || !d_database.exec("ALTER TABLE sms_tmp RENAME TO sms")) return false; if (!d_database.exec("CREATE INDEX sms_read_and_notified_and_thread_id_index ON sms(read, notified, thread_id)") || !d_database.exec("CREATE INDEX sms_type_index ON sms (type)") || !d_database.exec("CREATE INDEX sms_date_sent_index ON sms (date_sent, address, thread_id)") || !d_database.exec("CREATE INDEX sms_date_server_index ON sms (date_server)") || !d_database.exec("CREATE INDEX sms_thread_date_index ON sms (thread_id, date)") || !d_database.exec("CREATE INDEX sms_reactions_unread_index ON sms (reactions_unread)")) return false; if (!d_database.exec("CREATE TRIGGER sms_ai AFTER INSERT ON sms BEGIN INSERT INTO sms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END;") || !d_database.exec("CREATE TRIGGER sms_ad AFTER DELETE ON sms BEGIN INSERT INTO sms_fts(sms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); END;") || !d_database.exec("CREATE TRIGGER sms_au AFTER UPDATE ON sms BEGIN INSERT INTO sms_fts(sms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); INSERT INTO sms_fts(rowid, body, thread_id) VALUES(new._id, new.body, new.thread_id); END;") || !d_database.exec("CREATE TRIGGER msl_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id AND is_mms = 0); END")) return false; } if (d_databaseversion < 110) { Logger::message("To 110"); if (!d_database.exec("DELETE FROM part WHERE mid != -8675309 AND mid NOT IN (SELECT _id FROM mms)")) return false; } if (d_databaseversion < 111) { Logger::message("To 111"); if (!d_database.exec("CREATE TABLE avatar_picker (" "_id INTEGER PRIMARY KEY AUTOINCREMENT," "last_used INTEGER DEFAULT 0," "group_id TEXT DEFAULT NULL," "avatar BLOB NOT NULL)")) return false; SqliteDB::QueryResults recipients_without_color; if (!d_database.exec("SELECT _id FROM recipient WHERE color IS NULL", &recipients_without_color)) return false; std::vector avatar_color_options{"C000", "C010", "C020", "C030", "C040", "C050", "C060", "C070", "C080", "C090", "C100", "C110", "C120", "C130", "C140", "C150", "C160", "C170", "C180", "C190", "C200", "C210", "C220", "C230", "C240", "C250", "C260", "C270", "C280", "C290", "C300", "C310", "C320", "C330", "C340", "C350"}; for (unsigned int i = 0; i < recipients_without_color.rows(); ++i) { uint8_t random_idx = 0; if (RAND_bytes(&random_idx, 1) != 1) Logger::warning("failed to generate random number"); random_idx = (static_cast(random_idx) / (255 + 1)) * ((avatar_color_options.size() - 1) - 0 + 1) + 0; if (!d_database.exec("UPDATE recipient SET color = ? WHERE _id = ?", {avatar_color_options[random_idx], recipients_without_color.value(i, "_id")})) return false; } } if (d_databaseversion < 112) { Logger::message("To 112"); if (!d_database.exec("DELETE FROM mms WHERE thread_id NOT IN (SELECT _id FROM thread)") || !d_database.exec("DELETE FROM part WHERE mid != -8675309 AND mid NOT IN (SELECT _id FROM mms)")) return false; } if (d_databaseversion < 113) { Logger::message("To 113"); if (!d_database.exec("CREATE TABLE sessions_tmp (" "_id INTEGER PRIMARY KEY AUTOINCREMENT," "address TEXT NOT NULL," "device INTEGER NOT NULL," "record BLOB NOT NULL," "UNIQUE(address, device))")) return false; if (!d_database.exec("INSERT INTO sessions_tmp (address, device, record) " "SELECT " "COALESCE(recipient.uuid, recipient.phone) AS new_address, " "sessions.device, " "sessions.record " "FROM sessions INNER JOIN recipient ON sessions.address = recipient._id " "WHERE new_address NOT NULL")) return false; if (!d_database.exec("DROP TABLE sessions") || !d_database.exec("ALTER TABLE sessions_tmp RENAME TO sessions")) return false; } if (d_databaseversion < 114) { Logger::message("To 114"); if (!d_database.exec("CREATE TABLE identities_tmp (" "_id INTEGER PRIMARY KEY AUTOINCREMENT," "address TEXT UNIQUE NOT NULL," "identity_key TEXT," "first_use INTEGER DEFAULT 0," "timestamp INTEGER DEFAULT 0," "verified INTEGER DEFAULT 0," "nonblocking_approval INTEGER DEFAULT 0)")) return false; if (!d_database.exec("INSERT INTO identities_tmp (address, identity_key, first_use, timestamp, verified, nonblocking_approval) " "SELECT " "COALESCE(recipient.uuid, recipient.phone) AS new_address," "identities.key," "identities.first_use," "identities.timestamp," "identities.verified," "identities.nonblocking_approval " "FROM identities INNER JOIN recipient ON identities.address = recipient._id " "WHERE new_address NOT NULL")) return false; if (!d_database.exec("DROP TABLE identities") || !d_database.exec("ALTER TABLE identities_tmp RENAME TO identities")) return false; } if (d_databaseversion < 115) { Logger::message("To 115"); if (!d_database.exec("CREATE TABLE group_call_ring (_id INTEGER PRIMARY KEY, ring_id INTEGER UNIQUE, date_received INTEGER, ring_state INTEGER)") || !d_database.exec("CREATE INDEX date_received_index on group_call_ring (date_received)")) return false; } if (d_databaseversion < 116) { Logger::message("To 116"); if (!d_database.exec("DELETE FROM sessions WHERE address LIKE '+%'")) return false; // NOT SURE IF THIS IS WHATS INTENDED if (!d_database.exec("UPDATE recipient SET storage_service_key = NULL WHERE " "storage_service_key NOT NULL AND group_id IS NULL AND uuid IS NULL")) return false; } if (d_databaseversion < 117) { Logger::message("To 117"); if (!d_database.exec("ALTER TABLE sms ADD COLUMN receipt_timestamp INTEGER DEFAULT -1") || !d_database.exec("ALTER TABLE mms ADD COLUMN receipt_timestamp INTEGER DEFAULT -1")) return false; } if (d_databaseversion < 118) { Logger::message("To 118"); if (!d_database.exec("ALTER TABLE recipient ADD COLUMN badges BLOB DEFAULT NULL")) return false; } if (d_databaseversion < 119) { Logger::message("To 119"); if (!d_database.exec("CREATE TABLE sender_keys_tmp (" "_id INTEGER PRIMARY KEY AUTOINCREMENT," "address TEXT NOT NULL," "device INTEGER NOT NULL," "distribution_id TEXT NOT NULL," "record BLOB NOT NULL," "created_at INTEGER NOT NULL," "UNIQUE(address, device, distribution_id) ON CONFLICT REPLACE)")) return false; if (!d_database.exec("INSERT INTO sender_keys_tmp (address, device, distribution_id, record, created_at) " "SELECT " "recipient.uuid AS new_address," "sender_keys.device," "sender_keys.distribution_id," "sender_keys.record," "sender_keys.created_at " "FROM sender_keys INNER JOIN recipient ON sender_keys.recipient_id = recipient._id " "WHERE new_address NOT NULL")) return false; if (!d_database.exec("DROP TABLE sender_keys") || !d_database.exec("ALTER TABLE sender_keys_tmp RENAME TO sender_keys")) return false; } if (d_databaseversion < 120) { Logger::message("To 120"); if (!d_database.exec("ALTER TABLE sender_key_shared ADD COLUMN timestamp INTEGER DEFAULT 0")) return false; } if (d_databaseversion < 121) { Logger::message("To 121"); if (!d_database.exec("CREATE TABLE reaction (" "_id INTEGER PRIMARY KEY," "message_id INTEGER NOT NULL," "is_mms INTEGER NOT NULL," "author_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE," "emoji TEXT NOT NULL," "date_sent INTEGER NOT NULL," "date_received INTEGER NOT NULL," "UNIQUE(message_id, is_mms, author_id) ON CONFLICT REPLACE)")) return false; SqliteDB::QueryResults msg_reactions; for (auto const &table : {"sms"s, "mms"s}) { if (!d_database.exec("SELECT _id, reactions FROM "s + table + " WHERE reactions NOT NULL", &msg_reactions)) return false; for (unsigned int i = 0; i < msg_reactions.rows(); ++i) { ReactionList reactions(msg_reactions.getValueAs, size_t>>(i, "reactions")); for (unsigned int j = 0; j < reactions.numReactions(); ++j) { if (!insertRow("reaction", {{"message_id", msg_reactions.value(i, "_id")}, {"is_mms", (table == "sms" ? 0 : 1)}, {"author_id", reactions.getAuthor(j)}, {"emoji", reactions.getEmoji(j)}, {"date_sent", reactions.getSentTime(j)}, {"date_received", reactions.getReceivedTime(j)}})) return false; } } } if (!d_database.exec("UPDATE reaction SET author_id = IFNULL((SELECT new_id FROM remapped_recipients WHERE author_id = old_id), author_id)") || !d_database.exec("CREATE TRIGGER reactions_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM reaction WHERE message_id = old._id AND is_mms = 0; END") || !d_database.exec("CREATE TRIGGER reactions_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM reaction WHERE message_id = old._id AND is_mms = 0; END") || !d_database.exec("UPDATE sms SET reactions = NULL WHERE reactions NOT NULL") || !d_database.exec("UPDATE mms SET reactions = NULL WHERE reactions NOT NULL")) return false; } if (d_databaseversion < 122) { Logger::message("To 122"); if (!d_database.exec("ALTER TABLE recipient ADD COLUMN pni TEXT DEFAULT NULL") || !d_database.exec("CREATE UNIQUE INDEX IF NOT EXISTS recipient_pni_index ON recipient (pni)")) return false; } if (d_databaseversion < 123) { Logger::message("To 123"); if (!d_database.exec("CREATE TABLE notification_profile (" "_id INTEGER PRIMARY KEY AUTOINCREMENT, " "name TEXT NOT NULL UNIQUE," "emoji TEXT NOT NULL," "color TEXT NOT NULL," "created_at INTEGER NOT NULL," "allow_all_calls INTEGER NOT NULL DEFAULT 0," "allow_all_mentions INTEGER NOT NULL DEFAULT 0)")) return false; if (!d_database.exec("CREATE TABLE notification_profile_schedule (" "_id INTEGER PRIMARY KEY AUTOINCREMENT," "notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE," "enabled INTEGER NOT NULL DEFAULT 0," "start INTEGER NOT NULL," "end INTEGER NOT NULL," "days_enabled TEXT NOT NULL)")) return false; if (!d_database.exec("CREATE TABLE notification_profile_allowed_members (" "_id INTEGER PRIMARY KEY AUTOINCREMENT," "notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE," "recipient_id INTEGER NOT NULL," "UNIQUE(notification_profile_id, recipient_id) ON CONFLICT REPLACE)")) return false; if (!d_database.exec("CREATE INDEX notification_profile_schedule_profile_index ON notification_profile_schedule (notification_profile_id)") || !d_database.exec("CREATE INDEX notification_profile_allowed_members_profile_index ON notification_profile_allowed_members (notification_profile_id)")) return false; } if (d_databaseversion < 124) { Logger::message("To 124"); if (!d_database.exec("UPDATE notification_profile_schedule SET end = 2400 WHERE end = 0")) return false; } if (d_databaseversion < 125) { Logger::message("To 125"); if (!d_database.exec("DELETE FROM reaction " "WHERE " "(is_mms = 0 AND message_id NOT IN (SELECT _id FROM sms)) " "OR " "(is_mms = 1 AND message_id NOT IN (SELECT _id FROM mms))")) return false; } if (d_databaseversion < 126) { Logger::message("To 126"); if (!d_database.exec("DELETE FROM reaction " "WHERE " "(is_mms = 0 AND message_id IN (SELECT _id from sms WHERE remote_deleted = 1)) " "OR " "(is_mms = 1 AND message_id IN (SELECT _id from mms WHERE remote_deleted = 1))")) return false; } if (d_databaseversion < 127) { Logger::message("To 127"); if (!d_database.exec("UPDATE recipient SET pni = NULL WHERE phone IS NULL")) return false; } if (d_databaseversion < 128) { Logger::message("To 128"); if (!d_database.exec("ALTER TABLE mms ADD COLUMN ranges BLOB DEFAULT NULL")) return false; } if (d_databaseversion < 129) { Logger::message("To 129"); if (!d_database.exec("DROP TRIGGER reactions_mms_delete") || !d_database.exec("CREATE TRIGGER reactions_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM reaction WHERE message_id = old._id AND is_mms = 1; END")) return false; if (!d_database.exec("DELETE FROM reaction " "WHERE " "(is_mms = 0 AND message_id NOT IN (SELECT _id from sms)) " "OR " "(is_mms = 1 AND message_id NOT IN (SELECT _id from mms))")) return false; } // THIS ONE IS QUESTIONABLE if (d_databaseversion < 130) { Logger::message("To 130"); if (!d_database.exec("CREATE TABLE one_time_prekeys_tmp (" "_id INTEGER PRIMARY KEY," "account_id TEXT NOT NULL," "key_id INTEGER," "public_key TEXT NOT NULL," "private_key TEXT NOT NULL," "UNIQUE(account_id, key_id))")) return false; // localAci should (probably) be set to UUID (aci) of self. Normally it is retrieved from a // separate table (not exported to the backup). Not sure if skipping these next three migrations // even if localACI _IS_ set is harmful // Otherwise, try to use scanself and d_selfuuid; // if localAci != null // migrateExistingOneTimePreKeys // else // warn("no local aci, not migrating any existing one-time prekeys...") if (!d_database.exec("DROP TABLE one_time_prekeys") || !d_database.exec("ALTER TABLE one_time_prekeys_tmp RENAME TO one_time_prekeys")) return false; if (!d_database.exec("CREATE TABLE signed_prekeys_tmp (" "_id INTEGER PRIMARY KEY," "account_id TEXT NOT NULL," "key_id INTEGER," "public_key TEXT NOT NULL," "private_key TEXT NOT NULL," "signature TEXT NOT NULL," "timestamp INTEGER DEFAULT 0," "UNIQUE(account_id, key_id))")) return false; // if localAci != null // migrateExistingSignedPreKeys // else // warn("no local aci, not migrating any existing signed prekeys...") if (!d_database.exec("DROP TABLE signed_prekeys") || !d_database.exec("ALTER TABLE signed_prekeys_tmp RENAME TO signed_prekeys")) return false; if (!d_database.exec("CREATE TABLE sessions_tmp (" "_id INTEGER PRIMARY KEY AUTOINCREMENT," "account_id TEXT NOT NULL," "address TEXT NOT NULL," "device INTEGER NOT NULL," "record BLOB NOT NULL," "UNIQUE(account_id, address, device))")) return false; // if localAci != null // migrateExistingSessions // else // warn("no local aci, not migrating any existing sessions...") if (!d_database.exec("DROP TABLE sessions") || !d_database.exec("ALTER TABLE sessions_tmp RENAME TO sessions")) return false; } if (d_databaseversion < 131) { Logger::message("To 131"); if (!d_database.exec("CREATE TABLE donation_receipt (" "_id INTEGER PRIMARY KEY AUTOINCREMENT," "receipt_type TEXT NOT NULL," "receipt_date INTEGER NOT NULL," "amount TEXT NOT NULL," "currency TEXT NOT NULL," "subscription_level INTEGER NOT NULL)")) return false; if (!d_database.exec("CREATE INDEX IF NOT EXISTS donation_receipt_type_index ON donation_receipt (receipt_type);") || !d_database.exec("CREATE INDEX IF NOT EXISTS donation_receipt_date_index ON donation_receipt (receipt_date);")) return false; } // THIS ONE IS QUESTIONABLE if (d_databaseversion < 132) { Logger::message("To 132"); if (!d_database.exec("ALTER TABLE mms ADD COLUMN is_story INTEGER DEFAULT 0") || !d_database.exec("ALTER TABLE mms ADD COLUMN parent_story_id INTEGER DEFAULT 0") || !d_database.exec("CREATE INDEX IF NOT EXISTS mms_is_story_index ON mms (is_story)") || !d_database.exec("CREATE INDEX IF NOT EXISTS mms_parent_story_id_index ON mms (parent_story_id)") || !d_database.exec("ALTER TABLE recipient ADD COLUMN distribution_list_id INTEGER DEFAULT NULL")) return false; if (!d_database.exec("CREATE TABLE distribution_list (" "_id INTEGER PRIMARY KEY AUTOINCREMENT," "name TEXT UNIQUE NOT NULL," "distribution_id TEXT UNIQUE NOT NULL," "recipient_id INTEGER UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE)")) return false; if (!d_database.exec("CREATE TABLE distribution_list_member (" "_id INTEGER PRIMARY KEY AUTOINCREMENT," "list_id INTEGER NOT NULL REFERENCES distribution_list (_id) ON DELETE CASCADE," "recipient_id INTEGER NOT NULL," "UNIQUE(list_id, recipient_id) ON CONFLICT IGNORE)")) return false; /* THIS IS AN UNKNOWN! */ // inserts a storage_service_key (random 16 bytes) into the newly created my-story recipient unsigned char ssk_buffer[16]; if (RAND_bytes(ssk_buffer, 16) != 1) { Logger::error("Failed to generate 16 random bytes"); return false; } SqliteDB::QueryResults new_id; if (!d_database.exec("INSERT INTO recipient (distribution_list_id, storage_service_key, profile_sharing) VALUES (?, ?, ?) " "RETURNING _id", {1L, Base64::bytesToBase64String(ssk_buffer, 16), 1}, &new_id) || new_id.rows() != 1) return false; long long int new_id_val = new_id.valueAsInt(0, 0, -1); if (new_id_val == -1) return false; union { struct { uint32_t time_low; uint16_t time_mid; uint16_t time_hi_and_version; uint8_t clk_seq_hi_res; uint8_t clk_seq_low; uint8_t node[6]; } uuidstruct; uint8_t rnd[16]; } uuid; if (RAND_bytes(uuid.rnd, sizeof(uuid)) != 1) { Logger::error("Failed to generate 16 random bytes (2)"); return false; } // Refer Section 4.2 of RFC-4122 // https://tools.ietf.org/html/rfc4122#section-4.2 uuid.uuidstruct.clk_seq_hi_res = (uint8_t) ((uuid.uuidstruct.clk_seq_hi_res & 0x3F) | 0x80); uuid.uuidstruct.time_hi_and_version = (uint16_t) ((uuid.uuidstruct.time_hi_and_version & 0x0FFF) | 0x4000); #if __cpp_lib_format >= 201907L std::string uuid_str = std::format("{:0>8x}-{:0>4x}-{:0>4x}-{:0>2x}{:0>2x}-{:0>2x}{:0>2x}{:0>2x}{:0>2x}{:0>2x}{:0>2x}", uuid.uuidstruct.time_low, uuid.uuidstruct.time_mid, uuid.uuidstruct.time_hi_and_version, uuid.uuidstruct.clk_seq_hi_res, uuid.uuidstruct.clk_seq_low, uuid.uuidstruct.node[0], uuid.uuidstruct.node[1], uuid.uuidstruct.node[2], uuid.uuidstruct.node[3], uuid.uuidstruct.node[4], uuid.uuidstruct.node[5]); #else int size = 1 + std::snprintf(nullptr, 0, "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x", uuid.uuidstruct.time_low, uuid.uuidstruct.time_mid, uuid.uuidstruct.time_hi_and_version, uuid.uuidstruct.clk_seq_hi_res, uuid.uuidstruct.clk_seq_low, uuid.uuidstruct.node[0], uuid.uuidstruct.node[1], uuid.uuidstruct.node[2], uuid.uuidstruct.node[3], uuid.uuidstruct.node[4], uuid.uuidstruct.node[5]); if (size <= 0) { Logger::error("Failed to get size of uuid"); return false; } std::unique_ptr uuid_char(new char[size]); if (std::snprintf(uuid_char.get(), size, "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x", uuid.uuidstruct.time_low, uuid.uuidstruct.time_mid, uuid.uuidstruct.time_hi_and_version, uuid.uuidstruct.clk_seq_hi_res, uuid.uuidstruct.clk_seq_low, uuid.uuidstruct.node[0], uuid.uuidstruct.node[1], uuid.uuidstruct.node[2], uuid.uuidstruct.node[3], uuid.uuidstruct.node[4], uuid.uuidstruct.node[5]) < 0) { Logger::error("failed to format UUID"); return false; } std::string uuid_str(uuid_char.get(), uuid_char.get() + size - 1); #endif if (!d_database.exec("INSERT INTO distribution_list (_id, name, distribution_id, recipient_id) VALUES (?, ?, ?, ?)", {1L, uuid_str, uuid_str, new_id_val})) return false; } if (d_databaseversion < 133) { Logger::message("To 133"); if (!d_database.exec("ALTER TABLE distribution_list ADD COLUMN allows_replies INTEGER DEFAULT 1")) return false; } if (d_databaseversion < 134) { Logger::message("To 134"); if (!d_database.exec("ALTER TABLE groups ADD COLUMN display_as_story INTEGER DEFAULT 0")) return false; } if (d_databaseversion < 135) { Logger::message("To 135"); if (!d_database.exec("CREATE INDEX IF NOT EXISTS mms_thread_story_parent_story_index ON mms (thread_id, date_received, is_story, parent_story_id)")) return false; } if (d_databaseversion < 136) { Logger::message("To 136"); if (!d_database.exec("CREATE TABLE story_sends ( _id INTEGER PRIMARY KEY, message_id INTEGER NOT NULL REFERENCES mms (_id) ON DELETE CASCADE, recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, sent_timestamp INTEGER NOT NULL, allows_replies INTEGER NOT NULL )") || !d_database.exec("CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)")) return false; } if (d_databaseversion < 137) { Logger::message("To 137"); if (!d_database.exec("ALTER TABLE distribution_list ADD COLUMN deletion_timestamp INTEGER DEFAULT 0") || !d_database.exec("UPDATE recipient SET group_type = 4 WHERE distribution_list_id IS NOT NULL") || !d_database.exec("UPDATE distribution_list SET name = '00000000-0000-0000-0000-000000000000', distribution_id = '00000000-0000-0000-0000-000000000000' WHERE _id = 1")) return false; } if (d_databaseversion < 138) { Logger::message("To 138"); if (!d_database.exec("UPDATE recipient SET storage_service_key = NULL WHERE distribution_list_id IS NOT NULL AND NOT EXISTS(SELECT _id from distribution_list WHERE _id = distribution_list_id)")) return false; } if (d_databaseversion < 139) { Logger::message("To 139"); if (!d_database.exec("DELETE FROM storage_key WHERE type <= 4")) return false; } if (d_databaseversion < 140) { Logger::message("To 140"); if (!d_database.exec("CREATE INDEX IF NOT EXISTS recipient_service_id_profile_key ON recipient (uuid, profile_key) WHERE uuid NOT NULL AND profile_key NOT NULL") || !d_database.exec("CREATE TABLE cds ( _id INTEGER PRIMARY KEY, e164 TEXT NOT NULL UNIQUE ON CONFLICT IGNORE, last_seen_at INTEGER DEFAULT 0)")) return false; } if (d_databaseversion < 141) { Logger::message("To 141"); if (!d_database.exec("ALTER TABLE groups ADD COLUMN auth_service_id TEXT DEFAULT NULL")) return false; } if (d_databaseversion < 142) { Logger::message("To 142"); if (!d_database.exec("ALTER TABLE mms ADD COLUMN quote_type INTEGER DEFAULT 0")) return false; } if (d_databaseversion < 143) { Logger::message("To 143"); if (!d_database.exec("ALTER TABLE distribution_list ADD COLUMN is_unknown INTEGER DEFAULT 0")) return false; if (!d_database.exec("CREATE TABLE story_sends_tmp ( _id INTEGER PRIMARY KEY, message_id INTEGER NOT NULL REFERENCES mms (_id) ON DELETE CASCADE, recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, sent_timestamp INTEGER NOT NULL, allows_replies INTEGER NOT NULL, distribution_id TEXT NOT NULL REFERENCES distribution_list (distribution_id) ON DELETE CASCADE )")) return false; if (!d_database.exec("INSERT INTO story_sends_tmp (_id, message_id, recipient_id, sent_timestamp, allows_replies, distribution_id) SELECT story_sends._id, story_sends.message_id, story_sends.recipient_id, story_sends.sent_timestamp, story_sends.allows_replies, distribution_list.distribution_id FROM story_sends INNER JOIN mms ON story_sends.message_id = mms._id INNER JOIN distribution_list ON distribution_list.recipient_id = mms.address")) return false; if (!d_database.exec("DROP TABLE story_sends") || !d_database.exec("DROP INDEX IF EXISTS story_sends_recipient_id_sent_timestamp_allows_replies_index") || !d_database.exec("ALTER TABLE story_sends_tmp RENAME TO story_sends") || !d_database.exec("CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)")) return false; } if (d_databaseversion < 144) { Logger::message("To 144"); if (!d_database.exec("UPDATE mms SET read = 1 WHERE parent_story_id > 0")) return false; } if (d_databaseversion < 145) { Logger::message("To 145"); if (!d_database.exec("DELETE FROM mms WHERE parent_story_id > 0 AND parent_story_id NOT IN (SELECT _id FROM mms WHERE remote_deleted = 0)")) return false; } if (d_databaseversion < 146) { Logger::message("To 146"); if (!d_database.exec("CREATE TABLE remote_megaphone ( _id INTEGER PRIMARY KEY, uuid TEXT UNIQUE NOT NULL, priority INTEGER NOT NULL, countries TEXT, minimum_version INTEGER NOT NULL, dont_show_before INTEGER NOT NULL, dont_show_after INTEGER NOT NULL, show_for_days INTEGER NOT NULL, conditional_id TEXT, primary_action_id TEXT, secondary_action_id TEXT, image_url TEXT, image_uri TEXT DEFAULT NULL, title TEXT NOT NULL, body TEXT NOT NULL, primary_action_text TEXT, secondary_action_text TEXT, shown_at INTEGER DEFAULT 0, finished_at INTEGER DEFAULT 0)")) return false; } if (d_databaseversion < 147) { Logger::message("To 147"); if (!d_database.exec("CREATE INDEX IF NOT EXISTS mms_quote_id_quote_author_index ON mms (quote_id, quote_author)")) return false; } if (d_databaseversion < 148) { Logger::message("To 148"); if (!d_database.exec("ALTER TABLE distribution_list ADD COLUMN privacy_mode INTEGER DEFAULT 0") || !d_database.exec("UPDATE distribution_list SET privacy_mode = 1 WHERE _id = 1")) return false; if (!d_database.exec("CREATE TABLE distribution_list_member_tmp (_id INTEGER PRIMARY KEY AUTOINCREMENT, list_id INTEGER NOT NULL REFERENCES distribution_list (_id) ON DELETE CASCADE, recipient_id INTEGER NOT NULL REFERENCES recipient (_id), privacy_mode INTEGER DEFAULT 0)") || !d_database.exec("INSERT INTO distribution_list_member_tmp SELECT _id, list_id, recipient_id, 0 FROM distribution_list_member") || !d_database.exec("DROP TABLE distribution_list_member") || !d_database.exec("ALTER TABLE distribution_list_member_tmp RENAME TO distribution_list_member") || !d_database.exec("UPDATE distribution_list_member SET privacy_mode = 1 WHERE list_id = 1") || !d_database.exec("CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON distribution_list_member (list_id, recipient_id, privacy_mode)")) return false; } if (d_databaseversion < 149) { Logger::message("To 149"); if (!d_database.exec("UPDATE recipient SET profile_key_credential = NULL")) return false; } if (d_databaseversion < 150) { Logger::message("To 150"); if (!d_database.exec("ALTER TABLE msl_payload ADD COLUMN urgent INTEGER NOT NULL DEFAULT 1")) return false; // setDBV 150 } // NOTE THIS IS A DUPLICATE OF 153 FOR SOME REASON if (d_databaseversion < 151) { Logger::message("To 151"); std::string mystory_dist_id = d_database.getSingleResultAs("SELECT distribution_id FROM distribution_list WHERE _id = 1", std::string()); if (mystory_dist_id == "00000000-0000-0000-0000-000000000000") // ok! ; else { if (mystory_dist_id.empty()) // need to create... { // get mystoryrecipient_id long long int mystory_rid = d_database.getSingleResultAs("SELECT _id FROM recipient WHERE distribution_list_id = 1", -1); if (mystory_rid == -1) // create { Logger::error("Unable to create new recipient"); return false; } if (!d_database.exec("INSERT INTO distribution_list (_id, name, distribution_id, recipient_id, privacy_mode) " "VALLUES " "(?, ?, ?, ?, ?)", {1, "00000000-0000-0000-0000-000000000000", "00000000-0000-0000-0000-000000000000", mystory_rid, 2})) return false; } else // wrong dist id { if (!d_database.exec("UPDATE distribution_list SET distribution_id = ? WHERE _id = 1", "00000000-0000-0000-0000-000000000000")) return false; } } // setDBV 151 } if (d_databaseversion < 152) { Logger::message("To 152"); if (!d_database.exec("UPDATE recipient SET group_type = 4 WHERE distribution_list_id IS NOT NULL")) return false; // setDBV 152 } // NOTE THIS IS A DUPLICATE OF 151 FOR SOME REASON if (d_databaseversion < 153) { Logger::message("To 153"); std::string mystory_dist_id = d_database.getSingleResultAs("SELECT distribution_id FROM distribution_list WHERE _id = 1", std::string()); if (mystory_dist_id == "00000000-0000-0000-0000-000000000000") // ok! ; else { if (mystory_dist_id.empty()) // need to create... { // get mystoryrecipient_id long long int mystory_rid = d_database.getSingleResultAs("SELECT _id FROM recipient WHERE distribution_list_id = 1", -1); if (mystory_rid == -1) // create { Logger::error("Unable to create new recipient"); return false; } if (!d_database.exec("INSERT INTO distribution_list (_id, name, distribution_id, recipient_id, privacy_mode) " "VALLUES " "(?, ?, ?, ?, ?)", {1, "00000000-0000-0000-0000-000000000000", "00000000-0000-0000-0000-000000000000", mystory_rid, 2})) return false; } else // wrong dist id { if (!d_database.exec("UPDATE distribution_list SET distribution_id = ? WHERE _id = 1", "00000000-0000-0000-0000-000000000000")) return false; } } // setDBV 153 } if (d_databaseversion < 154) { Logger::message("To 154"); if (!d_database.exec("ALTER TABLE recipient ADD COLUMN needs_pni_signature") || !d_database.exec("CREATE TABLE pending_pni_signature_message (_id INTEGER PRIMARY KEY, recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, sent_timestamp INTEGER NOT NULL, device_id INTEGER NOT NULL)") || !d_database.exec("CREATE UNIQUE INDEX pending_pni_recipient_sent_device_index ON pending_pni_signature_message (recipient_id, sent_timestamp, device_id)")) return false; // setDBV154 } if (d_databaseversion < 155) { Logger::message("To 155"); if (!d_database.exec("ALTER TABLE mms ADD COLUMN export_state BLOB DEFAULT NULL") || !d_database.exec("ALTER TABLE mms ADD COLUMN exported INTEGER DEFAULT 0") || !d_database.exec("ALTER TABLE sms ADD COLUMN export_state BLOB DEFAULT NULL") || !d_database.exec("ALTER TABLE sms ADD COLUMN exported INTEGER DEFAULT 0")) return false; // setDBV155 } if (d_databaseversion < 156) { Logger::message("To 156"); if (!d_database.exec("ALTER TABLE recipient ADD COLUMN unregistered_timestamp INTEGER DEFAULT 0")) return false; // 2678400000 is suppoedly 31 days in millisecond if (!d_database.exec("UPDATE recipient SET unregistered_timestamp = ? WHERE registered = ? AND group_type = ?", {2678400000, 2, 0})) return false; // setDBV156 } if (d_databaseversion < 157) { Logger::message("To 157"); if (!d_database.exec("ALTER TABLE recipient ADD COLUMN hidden INTEGER DEFAULT 0")) return false; // setDBV157 } if (d_databaseversion < 158) { Logger::message("To 158"); if (!d_database.exec("ALTER TABLE groups ADD COLUMN last_force_update_timestamp INTEGER DEFAULT 0")) return false; // setDBV158 } if (d_databaseversion < 159) { Logger::message("To 159"); if (!d_database.exec("ALTER TABLE thread ADD COLUMN unread_self_mention_count INTEGER DEFAULT 0")) return false; // setDBV159 } if (d_databaseversion < 160) { Logger::message("To 160"); if (!d_database.exec("CREATE INDEX IF NOT EXISTS sms_exported_index ON sms (exported)") || !d_database.exec("CREATE INDEX IF NOT EXISTS mms_exported_index ON mms (exported)")) return false; // setDBV } if (d_databaseversion < 161) { Logger::message("To 161"); if (!d_database.exec("CREATE INDEX IF NOT EXISTS story_sends_message_id_distribution_id_index ON story_sends (message_id, distribution_id)")) return false; // setDBV161 } if (d_databaseversion < 162) { Logger::message("To 162"); if (!d_database.tableContainsColumn("thread", "unread_self_mention_count")) if (!d_database.exec("ALTER TABLE thread ADD COLUMN unread_self_mention_count INTEGER DEFAULT 0")) return false; // setDBV162 } if (d_databaseversion < 163) { Logger::message("To 163"); if (!d_database.tableContainsColumn("remote_megaphone", "primary_action_data")) if (!d_database.exec("ALTER TABLE remote_megaphone ADD COLUMN primary_action_data TEXT DEFAULT NULL")) return false; if (!d_database.tableContainsColumn("remote_megaphone", "secondary_action_data")) if (!d_database.exec("ALTER TABLE remote_megaphone ADD COLUMN secondary_action_data TEXT DEFAULT NULL")) return false; if (!d_database.tableContainsColumn("remote_megaphone", "snoozed_at")) if (!d_database.exec("ALTER TABLE remote_megaphone ADD COLUMN snoozed_at INTEGER DEFAULT 0")) return false; if (!d_database.tableContainsColumn("remote_megaphone", "seen_count")) if (!d_database.exec("ALTER TABLE remote_megaphone ADD COLUMN seen_count INTEGER DEFAULT 0")) return false; // setDBV163 } if (d_databaseversion < 164) { Logger::message("To 164"); if (!d_database.exec("CREATE INDEX IF NOT EXISTS thread_read ON thread (read)")) return false; // setDBV164 } if (d_databaseversion < 165) { Logger::message("To 165"); if (!d_database.exec("CREATE INDEX IF NOT EXISTS mms_id_msg_box_payment_transactions_index ON mms (_id, msg_box) WHERE msg_box & 0x300000000 != 0")) return false; // setDBV165 } if (d_databaseversion < 166) { Logger::message("To 166"); if (d_database.tableContainsColumn("thread", "thread_recipient_id")) { // removeDuplicateThreadEntries(db) SqliteDB::QueryResults res; if (!d_database.exec("SELECT thread_recipient_id, COUNT(*) AS thread_count FROM thread GROUP BY thread_recipient_id HAVING thread_count > 1", &res)) return false; for (unsigned int i = 0; i < res.rows(); ++i) { long long int rid = res.valueAsInt(i, "recipient_id", -1); if (rid == -1) return false; SqliteDB::QueryResults res2; if (!d_database.exec("SELECT _id, date FROM thread WHERE thread_recipient_id = ? ORDER BY date DESC", rid, &res2)) return false; long long int mainthread = res2.valueAsInt(0, "_id", -1); if (mainthread == -1) return false; for (unsigned int j = 1; j < res2.rows(); ++j) { // merge into mainthread if (!d_database.exec("UPDATE drafts SET thread_id = ? WHERE thread_id = ?", {mainthread, res2.value(j, "_id")}) || !d_database.exec("UPDATE mention SET thread_id = ? WHERE thread_id = ?", {mainthread, res2.value(j, "_id")}) || !d_database.exec("UPDATE mms SET thread_id = ? WHERE thread_id = ?", {mainthread, res2.value(j, "_id")}) || !d_database.exec("UPDATE sms SET thread_id = ? WHERE thread_id = ?", {mainthread, res2.value(j, "_id")}) || !d_database.exec("UPDATE pending_retry_receipts SET thread_id = ? WHERE thread_id = ?", {mainthread, res2.value(j, "_id")}) || !d_database.exec("UPDATE remapped_threads SET new_id = ? WHERE new_id = ?", {mainthread, res2.value(j, "_id")})) return false; if (!d_database.exec("DELETE FROM thread WHERE _id = ?", res2.value(j, "_id"))) return false; } } // updateThreadTableSchema(db) if (!d_database.exec("CREATE TABLE thread_tmp (_id INTEGER PRIMARY KEY AUTOINCREMENT, date INTEGER DEFAULT 0, meaningful_messages INTEGER DEFAULT 0,recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE,read INTEGER DEFAULT 1, type INTEGER DEFAULT 0, error INTEGER DEFAULT 0,snippet TEXT, snippet_type INTEGER DEFAULT 0, snippet_uri TEXT DEFAULT NULL, snippet_content_type TEXT DEFAULT NULL, snippet_extras TEXT DEFAULT NULL, unread_count INTEGER DEFAULT 0, archived INTEGER DEFAULT 0, status INTEGER DEFAULT 0, delivery_receipt_count INTEGER DEFAULT 0, read_receipt_count INTEGER DEFAULT 0, expires_in INTEGER DEFAULT 0, last_seen INTEGER DEFAULT 0, has_sent INTEGER DEFAULT 0, last_scrolled INTEGER DEFAULT 0, pinned INTEGER DEFAULT 0, unread_self_mention_count INTEGER DEFAULT 0)")) return false; if (!d_database.exec("INSERT INTO thread_tmp SELECT _id,date,message_count,thread_recipient_id,read,type,error,snippet,snippet_type,snippet_uri,snippet_content_type,snippet_extras,unread_count,archived,status,delivery_receipt_count,read_receipt_count,expires_in,last_seen,has_sent,last_scrolled,pinned,unread_self_mention_count FROM thread")) return false; if (!d_database.exec("DROP TABLE thread") || !d_database.exec("ALTER TABLE thread_tmp RENAME TO thread") || !d_database.exec("CREATE INDEX thread_recipient_id_index ON thread (recipient_id)") || !d_database.exec("CREATE INDEX archived_count_index ON thread (archived, meaningful_messages)") || !d_database.exec("CREATE INDEX thread_pinned_index ON thread (pinned)") || !d_database.exec("CREATE INDEX thread_read ON thread (read)")) return false; // fixDanglingSmsMessages(db) if (!d_database.exec("DELETE FROM sms WHERE address IS NULL OR address NOT IN (SELECT _id FROM recipient)") || !d_database.exec("DELETE FROM sms WHERE thread_id IS NULL OR thread_id NOT IN (SELECT _id FROM thread)")) return false; // fixDanglingMmsMessages(db) if (!d_database.exec("DELETE FROM mms WHERE address IS NULL OR address NOT IN (SELECT _id FROM recipient)") || !d_database.exec("DELETE FROM mms WHERE thread_id IS NULL OR thread_id NOT IN (SELECT _id FROM thread)")) return false; // updateSmsTableSchema(db) if (!d_database.exec("CREATE TABLE sms_tmp (_id INTEGER PRIMARY KEY AUTOINCREMENT,date_sent INTEGER NOT NULL,date_received INTEGER NOT NULL,date_server INTEGER DEFAULT -1,thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE,recipient_id NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,recipient_device_id INTEGER DEFAULT 1,type INTEGER,body TEXT,read INTEGER DEFAULT 0,status INTEGER DEFAULT -1,delivery_receipt_count INTEGER DEFAULT 0,mismatched_identities TEXT DEFAULT NULL,subscription_id INTEGER DEFAULT -1,expires_in INTEGER DEFAULT 0,expire_started INTEGER DEFAULT 0,notified INTEGER DEFAULT 0,read_receipt_count INTEGER DEFAULT 0,unidentified INTEGER DEFAULT 0,reactions_unread INTEGER DEFAULT 0,reactions_last_seen INTEGER DEFAULT -1,remote_deleted INTEGER DEFAULT 0,notified_timestamp INTEGER DEFAULT 0,server_guid TEXT DEFAULT NULL,receipt_timestamp INTEGER DEFAULT -1,export_state BLOB DEFAULT NULL,exported INTEGER DEFAULT 0)")) return false; if (!d_database.exec("INSERT INTO sms_tmp SELECT _id, date_sent, date, date_server, thread_id, address, address_device_id, type, body, read, status, delivery_receipt_count, mismatched_identities, subscription_id, expires_in, expire_started, notified, read_receipt_count, unidentified, reactions_unread, reactions_last_seen, remote_deleted, notified_timestamp, server_guid, receipt_timestamp, export_state, exported FROM sms")) return false; if (!d_database.exec("DROP TABLE sms") || !d_database.exec("ALTER TABLE sms_tmp RENAME TO sms") || !d_database.exec("CREATE INDEX sms_read_and_notified_and_thread_id_index ON sms(read, notified, thread_id)") || !d_database.exec("CREATE INDEX sms_type_index ON sms (type)") || !d_database.exec("CREATE INDEX sms_date_sent_index ON sms (date_sent, recipient_id, thread_id)") || !d_database.exec("CREATE INDEX sms_date_server_index ON sms (date_server)") || !d_database.exec("CREATE INDEX sms_thread_date_index ON sms (thread_id, date_received)") || !d_database.exec("CREATE INDEX sms_reactions_unread_index ON sms (reactions_unread)") || !d_database.exec("CREATE INDEX sms_exported_index ON sms (exported)") || !d_database.exec("CREATE TRIGGER sms_ai AFTER INSERT ON sms BEGIN INSERT INTO sms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END;") || !d_database.exec("CREATE TRIGGER sms_ad AFTER DELETE ON sms BEGIN INSERT INTO sms_fts(sms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); END;") || !d_database.exec("CREATE TRIGGER sms_au AFTER UPDATE ON sms BEGIN INSERT INTO sms_fts(sms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); INSERT INTO sms_fts(rowid, body, thread_id) VALUES(new._id, new.body, new.thread_id); END;") || !d_database.exec("CREATE TRIGGER msl_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id AND is_mms = 0); END")) return false; // updateMmsTableSchema(db) if (!d_database.exec("CREATE TABLE mms_tmp ( _id INTEGER PRIMARY KEY AUTOINCREMENT, date_sent INTEGER NOT NULL, date_received INTEGER NOT NULL, date_server INTEGER DEFAULT -1, thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE, recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, recipient_device_id INTEGER, type INTEGER NOT NULL, body TEXT, read INTEGER DEFAULT 0, ct_l TEXT, exp INTEGER, m_type INTEGER, m_size INTEGER, st INTEGER, tr_id TEXT, subscription_id INTEGER DEFAULT -1, receipt_timestamp INTEGER DEFAULT -1, delivery_receipt_count INTEGER DEFAULT 0, read_receipt_count INTEGER DEFAULT 0, viewed_receipt_count INTEGER DEFAULT 0, mismatched_identities TEXT DEFAULT NULL, network_failures TEXT DEFAULT NULL, expires_in INTEGER DEFAULT 0, expire_started INTEGER DEFAULT 0, notified INTEGER DEFAULT 0, quote_id INTEGER DEFAULT 0, quote_author INTEGER DEFAULT 0, quote_body TEXT DEFAULT NULL, quote_missing INTEGER DEFAULT 0, quote_mentions BLOB DEFAULT NULL, quote_type INTEGER DEFAULT 0, shared_contacts TEXT DEFAULT NULL, unidentified INTEGER DEFAULT 0, link_previews TEXT DEFAULT NULL, view_once INTEGER DEFAULT 0, reactions_unread INTEGER DEFAULT 0, reactions_last_seen INTEGER DEFAULT -1, remote_deleted INTEGER DEFAULT 0, mentions_self INTEGER DEFAULT 0, notified_timestamp INTEGER DEFAULT 0, server_guid TEXT DEFAULT NULL, message_ranges BLOB DEFAULT NULL, story_type INTEGER DEFAULT 0, parent_story_id INTEGER DEFAULT 0, export_state BLOB DEFAULT NULL, exported INTEGER DEFAULT 0)")) return false; if (!d_database.exec("INSERT INTO mms_tmp SELECT _id, date, date_received, date_server, thread_id, address, address_device_id, msg_box, body, read, ct_l, exp, m_type, m_size, st, tr_id, subscription_id, receipt_timestamp, delivery_receipt_count, read_receipt_count, viewed_receipt_count, mismatched_identities, network_failures, expires_in, expire_started, notified, quote_id, quote_author, quote_body, quote_missing, quote_mentions, quote_type, shared_contacts, unidentified, previews, reveal_duration, reactions_unread, reactions_last_seen, remote_deleted, mentions_self, notified_timestamp, server_guid, ranges, is_story, parent_story_id, export_state, exported FROM mms")) return false; if (!d_database.exec("DROP TABLE mms") || !d_database.exec("ALTER TABLE mms_tmp RENAME TO mms") || !d_database.exec("CREATE INDEX mms_read_and_notified_and_thread_id_index ON mms(read, notified, thread_id)") || !d_database.exec("CREATE INDEX mms_type_index ON mms (type)") || !d_database.exec("CREATE INDEX mms_date_sent_index ON mms (date_sent, recipient_id, thread_id)") || !d_database.exec("CREATE INDEX mms_date_server_index ON mms (date_server)") || !d_database.exec("CREATE INDEX mms_thread_date_index ON mms (thread_id, date_received)") || !d_database.exec("CREATE INDEX mms_reactions_unread_index ON mms (reactions_unread)") || !d_database.exec("CREATE INDEX IF NOT EXISTS mms_story_type_index ON mms (story_type)") || !d_database.exec("CREATE INDEX IF NOT EXISTS mms_parent_story_id_index ON mms (parent_story_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS mms_thread_story_parent_story_index ON mms (thread_id, date_received, story_type, parent_story_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS mms_quote_id_quote_author_index ON mms (quote_id, quote_author)") || !d_database.exec("CREATE INDEX IF NOT EXISTS mms_exported_index ON mms (exported)") || !d_database.exec("CREATE INDEX IF NOT EXISTS mms_id_type_payment_transactions_index ON mms (_id, type) WHERE type & 0x300000000 != 0") || !d_database.exec("CREATE TRIGGER mms_ai AFTER INSERT ON mms BEGIN INSERT INTO mms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END") || !d_database.exec("CREATE TRIGGER mms_ad AFTER DELETE ON mms BEGIN INSERT INTO mms_fts(mms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); END") || !d_database.exec("CREATE TRIGGER mms_au AFTER UPDATE ON mms BEGIN INSERT INTO mms_fts(mms_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); INSERT INTO mms_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END") || !d_database.exec("CREATE TRIGGER msl_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id AND is_mms = 1); END")) return false; } // setDBV166 } if (d_databaseversion < 167) { Logger::message("To 167"); if (!d_database.exec("DELETE FROM reaction WHERE (is_mms = 0 AND message_id NOT IN (SELECT _id FROM sms)) OR(is_mms = 1 AND message_id NOT IN (SELECT _id FROM mms))") || !d_database.exec("CREATE TRIGGER IF NOT EXISTS reactions_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM reaction WHERE message_id = old._id AND is_mms = 0; END") || !d_database.exec("CREATE TRIGGER IF NOT EXISTS reactions_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM reaction WHERE message_id = old._id AND is_mms = 1; END")) return false; // setDBV167 } if (d_databaseversion < 168) { Logger::message("To 168"); if (!d_database.exec("DROP TRIGGER msl_sms_delete") || !d_database.exec("DROP TRIGGER reactions_sms_delete") || !d_database.exec("DROP TRIGGER sms_ai") || !d_database.exec("DROP TRIGGER sms_au") || !d_database.exec("DROP TRIGGER sms_ad") || !d_database.exec("DROP TABLE sms_fts") || !d_database.exec("DROP INDEX mms_read_and_notified_and_thread_id_index") || !d_database.exec("DROP INDEX mms_type_index") || !d_database.exec("DROP INDEX mms_date_sent_index") || !d_database.exec("DROP INDEX mms_date_server_index") || !d_database.exec("DROP INDEX mms_thread_date_index") || !d_database.exec("DROP INDEX mms_reactions_unread_index") || !d_database.exec("DROP INDEX mms_story_type_index") || !d_database.exec("DROP INDEX mms_parent_story_id_index") || !d_database.exec("DROP INDEX mms_thread_story_parent_story_index") || !d_database.exec("DROP INDEX mms_quote_id_quote_author_index") || !d_database.exec("DROP INDEX mms_exported_index") || !d_database.exec("DROP INDEX mms_id_type_payment_transactions_index") || !d_database.exec("DROP TRIGGER mms_ai")) return false; long long int id_offset = 0; if (d_database.getSingleResultAs("SELECT COUNT(*) FROM sms", 0) > 0) { // copySmsToMms(db, nextMmsId) SqliteDB::QueryResults minmax; if (!d_database.exec("SELECT MIN(_id) AS min, MAX(_id) AS max FROM sms", &minmax)) return false; long long int minsms = minmax.getValueAs(0, "min"); if (!d_database.exec("SELECT MIN(_id) AS min, MAX(_id) AS max FROM mms", &minmax)) return false; long long int maxmms = minmax.getValueAs(0, "max"); id_offset = (maxmms - minsms) + 1; if (!d_database.exec("INSERT INTO mms SELECT _id + ?, date_sent, date_received, date_server, thread_id, recipient_id, recipient_device_id, type, body, read, null, 0, 0, 0, status, null, subscription_id, receipt_timestamp, delivery_receipt_count, read_receipt_count, 0, mismatched_identities, null, expires_in, expire_started, notified, 0, 0, null, 0, null, 0, null, unidentified, null, 0, reactions_unread, reactions_last_seen, remote_deleted, 0, notified_timestamp, server_guid, null, 0, 0, export_state, exported FROM sms", id_offset)) return false; } if (!d_database.exec("DROP TABLE sms")) return false; if (!d_database.exec("CREATE INDEX mms_read_and_notified_and_thread_id_index ON mms(read, notified, thread_id)") || !d_database.exec("CREATE INDEX mms_type_index ON mms (type)") || !d_database.exec("CREATE INDEX mms_date_sent_index ON mms (date_sent, recipient_id, thread_id)") || !d_database.exec("CREATE INDEX mms_date_server_index ON mms (date_server)") || !d_database.exec("CREATE INDEX mms_thread_date_index ON mms (thread_id, date_received)") || !d_database.exec("CREATE INDEX mms_reactions_unread_index ON mms (reactions_unread)") || !d_database.exec("CREATE INDEX mms_story_type_index ON mms (story_type)") || !d_database.exec("CREATE INDEX mms_parent_story_id_index ON mms (parent_story_id)") || !d_database.exec("CREATE INDEX mms_thread_story_parent_story_index ON mms (thread_id, date_received, story_type, parent_story_id)") || !d_database.exec("CREATE INDEX mms_quote_id_quote_author_index ON mms (quote_id, quote_author)") || !d_database.exec("CREATE INDEX mms_exported_index ON mms (exported)") || !d_database.exec("CREATE INDEX mms_id_type_payment_transactions_index ON mms (_id, type) WHERE type & 0x300000000 != 0") || !d_database.exec("CREATE TRIGGER mms_ai AFTER INSERT ON mms BEGIN INSERT INTO mms_fts (rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END;")) return false; if (!d_database.exec("UPDATE reaction SET message_id = message_id + ? WHERE is_mms = 0", id_offset) || !d_database.exec("UPDATE msl_message SET message_id = message_id + ? WHERE is_mms = 0", id_offset)) return false; // setDBV168 } if (d_databaseversion < 169) { Logger::message("To 169"); if (!d_database.exec("CREATE TABLE emoji_search_tmp ( _id INTEGER PRIMARY KEY, label TEXT NOT NULL, emoji TEXT NOT NULL, rank INTEGER DEFAULT 2147483647)" /*int.MAX_VALUE*/) || !d_database.exec("INSERT INTO emoji_search_tmp (label, emoji) SELECT label, emoji from emoji_search") || !d_database.exec("DROP TABLE emoji_search") || !d_database.exec("ALTER TABLE emoji_search_tmp RENAME TO emoji_search") || !d_database.exec("CREATE INDEX emoji_search_rank_covering ON emoji_search (rank, label, emoji)")) return false; // setDBV169 } if (d_databaseversion < 170) { Logger::message("To 170"); if (!d_database.exec("CREATE TABLE call ( _id INTEGER PRIMARY KEY, call_id INTEGER NOT NULL UNIQUE, message_id INTEGER NOT NULL REFERENCES mms (_id) ON DELETE CASCADE, peer INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, type INTEGER NOT NULL, direction INTEGER NOT NULL, event INTEGER NOT NULL)") || !d_database.exec("CREATE INDEX call_call_id_index ON call (call_id)") || !d_database.exec("CREATE INDEX call_message_id_index ON call (message_id)")) return false; // setDBV170 } if (d_databaseversion < 171) { Logger::message("To 171"); // removeDuplicateThreadEntries // almost (no sms) duplicates 166 SqliteDB::QueryResults res; if (!d_database.exec("SELECT recipient_id, COUNT(*) AS thread_count FROM thread GROUP BY recipient_id HAVING thread_count > 1", &res)) return false; for (unsigned int i = 0; i < res.rows(); ++i) { long long int rid = res.valueAsInt(i, "recipient_id", -1); if (rid == -1) return false; SqliteDB::QueryResults res2; if (!d_database.exec("SELECT _id, date FROM thread WHERE recipient_id = ? ORDER BY date DESC", rid, &res2)) return false; long long int mainthread = res2.valueAsInt(0, "recipient_id", -1); if (mainthread == -1) return false; for (unsigned int j = 1; j < res2.rows(); ++j) { // merge into mainthread if (!d_database.exec("UPDATE drafts SET thread_id = ? WHERE thread_id = ?", {mainthread, res2.value(j, "_id")}) || !d_database.exec("UPDATE mention SET thread_id = ? WHERE thread_id = ?", {mainthread, res2.value(j, "_id")}) || !d_database.exec("UPDATE mms SET thread_id = ? WHERE thread_id = ?", {mainthread, res2.value(j, "_id")}) || !d_database.exec("UPDATE pending_retry_receipts SET thread_id = ? WHERE thread_id = ?", {mainthread, res2.value(j, "_id")}) || !d_database.exec("UPDATE remapped_threads SET new_id = ? WHERE new_id = ?", {mainthread, res2.value(j, "_id")})) return false; if (!d_database.exec("DELETE FROM thread WHERE _id = ?", res2.value(j, "_id"))) return false; } } // updateThreadTableSchema(db) if (!d_database.exec("CREATE TABLE thread_tmp ( _id INTEGER PRIMARY KEY AUTOINCREMENT, date INTEGER DEFAULT 0, meaningful_messages INTEGER DEFAULT 0, recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE, read INTEGER DEFAULT 1, type INTEGER DEFAULT 0, error INTEGER DEFAULT 0, snippet TEXT, snippet_type INTEGER DEFAULT 0, snippet_uri TEXT DEFAULT NULL, snippet_content_type TEXT DEFAULT NULL, snippet_extras TEXT DEFAULT NULL, unread_count INTEGER DEFAULT 0, archived INTEGER DEFAULT 0, status INTEGER DEFAULT 0, delivery_receipt_count INTEGER DEFAULT 0, read_receipt_count INTEGER DEFAULT 0, expires_in INTEGER DEFAULT 0, last_seen INTEGER DEFAULT 0, has_sent INTEGER DEFAULT 0, last_scrolled INTEGER DEFAULT 0, pinned INTEGER DEFAULT 0, unread_self_mention_count INTEGER DEFAULT 0)")) return false; if (!d_database.exec("INSERT INTO thread_tmp SELECT _id, date, meaningful_messages, recipient_id, read, type, error, snippet, snippet_type, snippet_uri, snippet_content_type, snippet_extras, unread_count, archived, status, delivery_receipt_count, read_receipt_count, expires_in, last_seen, has_sent, last_scrolled, pinned, unread_self_mention_count FROM thread")) return false; if (!d_database.exec("DROP TABLE thread") || !d_database.exec("ALTER TABLE thread_tmp RENAME TO thread") || !d_database.exec("CREATE INDEX thread_recipient_id_index ON thread (recipient_id)") || !d_database.exec("CREATE INDEX archived_count_index ON thread (archived, meaningful_messages)") || !d_database.exec("CREATE INDEX thread_pinned_index ON thread (pinned)") || !d_database.exec("CREATE INDEX thread_read ON thread (read)")) return false; // setDBV171 } if (d_databaseversion < 172) { Logger::message("To 172"); if (!d_database.exec("CREATE TABLE group_membership (_id INTEGER PRIMARY KEY,group_id TEXT NOT NULL,recipient_id INTEGER NOT NULL,UNIQUE(group_id, recipient_id))")) return false; SqliteDB::QueryResults res; if (!d_database.exec("SELECT members, group_id FROM groups", &res)) return false; for (unsigned int i = 0; i < res.rows(); ++i) { std::string group_id(res(i, "group_id")); if (group_id.empty()) return false; std::string membersstring(res.valueAsString(i, "members")); std::regex comma(","); std::sregex_token_iterator iter(membersstring.begin(), membersstring.end(), comma, -1); std::vector members_list; std::transform(iter, std::sregex_token_iterator(), std::back_inserter(members_list), [](std::string const &m) -> long long int { return bepaald::toNumber(m); }); for (unsigned int j = 0; j < members_list.size(); ++j) if (!d_database.exec("INSERT INTO group_membership (group_id, recipient_id) VALUES (?, ?)", {group_id, members_list[j]})) return false; } if (!d_database.exec("ALTER TABLE groups DROP COLUMN members")) return false; // setDBV } if (d_databaseversion < 173) { Logger::message("To 173"); if (!d_database.exec("ALTER TABLE mms ADD COLUMN scheduled_date INTEGER DEFAULT -1") || !d_database.exec("DROP INDEX mms_thread_story_parent_story_index") || !d_database.exec("CREATE INDEX IF NOT EXISTS mms_thread_story_parent_story_scheduled_date_index ON mms (thread_id, date_received,story_type,parent_story_id,scheduled_date);")) return false; // setDBV } if (d_databaseversion < 174) { Logger::message("To 174"); if (!d_database.exec("CREATE TABLE reaction_tmp ( _id INTEGER PRIMARY KEY, message_id INTEGER NOT NULL REFERENCES mms (_id) ON DELETE CASCADE, author_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, emoji TEXT NOT NULL, date_sent INTEGER NOT NULL, date_received INTEGER NOT NULL, UNIQUE(message_id, author_id) ON CONFLICT REPLACE )")) return false; if (!d_database.exec("INSERT INTO reaction_tmp SELECT _id, message_id, author_id, emoji, date_sent, date_received FROM reaction WHERE message_id IN (SELECT _id FROM mms)")) return false; if (!d_database.exec("DROP TABLE reaction") || !d_database.exec("DROP TRIGGER IF EXISTS reactions_mms_delete") || !d_database.exec("ALTER TABLE reaction_tmp RENAME TO reaction") || !d_database.exec("ALTER TABLE mms RENAME TO message")) return false; // setDBV } if (d_databaseversion < 175) { Logger::message("To 175"); if (!d_database.exec("DROP TABLE mms_fts") || !d_database.exec("DROP TRIGGER IF EXISTS mms_ai") || !d_database.exec("DROP TRIGGER IF EXISTS mms_ad") || !d_database.exec("DROP TRIGGER IF EXISTS mms_au")) return false; if (!d_database.containsTable("message_fts")) { if (!d_database.exec("CREATE VIRTUAL TABLE message_fts USING fts5(body, thread_id UNINDEXED, content=message, content_rowid=_id)") || !d_database.exec("INSERT INTO message_fts(message_fts) VALUES('rebuild')") || !d_database.exec("CREATE TRIGGER message_ai AFTER INSERT ON message BEGIN INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END;") || !d_database.exec("CREATE TRIGGER message_ad AFTER DELETE ON message BEGIN INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES ('delete', old._id, old.body, old.thread_id); END;") || !d_database.exec("CREATE TRIGGER message_au AFTER UPDATE ON message BEGIN INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id); INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END;")) return false; } // setDBV } if (d_databaseversion < 176) { Logger::message("To 176"); if (!d_database.exec("DROP INDEX IF EXISTS mms_quote_id_quote_author_index") || !d_database.exec("CREATE INDEX IF NOT EXISTS message_quote_id_quote_author_scheduled_date_index ON message (quote_id, quote_author, scheduled_date);")) return false; // setDBV } if (d_databaseversion < 177) { Logger::message("To 177"); if (!d_database.exec("DROP TRIGGER IF EXISTS msl_mms_delete") || !d_database.exec("DROP TRIGGER IF EXISTS msl_attachment_delete") || !d_database.exec("DROP INDEX IF EXISTS msl_message_message_index")) return false; if (!d_database.exec("DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id NOT IN (SELECT _id FROM message))") || !d_database.exec("CREATE TABLE msl_message_tmp (_id INTEGER PRIMARY KEY,payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,message_id INTEGER NOT NULL)") || !d_database.exec("INSERT INTO msl_message_tmp SELECT _id, payload_id, message_id FROM msl_message") || !d_database.exec("DROP TABLE msl_message") || !d_database.exec("ALTER TABLE msl_message_tmp RENAME TO msl_message") || !d_database.exec("CREATE INDEX msl_message_message_index ON msl_message (message_id, payload_id)") || !d_database.exec("CREATE TRIGGER msl_message_delete AFTER DELETE ON message BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id); END") || !d_database.exec("CREATE TRIGGER msl_attachment_delete AFTER DELETE ON part BEGIN DELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old.mid); END")) return false; // setDBV } if (d_databaseversion < 178) { Logger::message("To 178"); if (!d_database.exec("ALTER TABLE recipient ADD COLUMN reporting_token BLOB DEFAULT NULL")) return false; // setDBV } if (d_databaseversion < 179) { Logger::message("To 179"); if (!d_database.exec("DELETE FROM msl_message WHERE payload_id NOT IN (SELECT _id FROM msl_payload)") || !d_database.exec("DELETE FROM msl_recipient WHERE payload_id NOT IN (SELECT _id FROM msl_payload)")) return false; // setDBV } if (d_databaseversion < 180) { Logger::message("To 180"); if (!d_database.exec("ALTER TABLE recipient ADD COLUMN system_nickname TEXT DEFAULT NULL")) return false; // setDBV } if (d_databaseversion < 181) { Logger::message("To 181"); if (!d_database.exec("DELETE FROM thread WHERE recipient_id NOT IN (SELECT _id FROM recipient)")) return false; if (d_database.changed()) { if (!d_database.exec("DELETE FROM message WHERE thread_id NOT IN (SELECT _id FROM thread)")) return false; if (d_database.changed()) { if (!d_database.exec("DELETE FROM story_send WHERE message_id NOT IN (SELECT _id FROM message)") || !d_database.exec("DELETE FROM reaction WHERE message_id NOT IN (SELECT _id FROM message)") || !d_database.exec("DELETE FROM call WHERE message_id NOT IN (SELECT _id FROM message)")) return false; } } // setDBV } if (d_databaseversion < 182) { Logger::message("To 182"); if (!d_database.exec("CREATE TABLE call_tmp ( _id INTEGER PRIMARY KEY, call_id INTEGER NOT NULL UNIQUE, message_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE SET NULL, peer INTEGER DEFAULT NULL REFERENCES recipient (_id) ON DELETE CASCADE, type INTEGER NOT NULL, direction INTEGER NOT NULL, event INTEGER NOT NULL, timestamp INTEGER NOT NULL, ringer INTEGER DEFAULT NULL, deletion_timestamp INTEGER DEFAULT 0 )")) return false; if (!d_database.exec("INSERT INTO call_tmp SELECT _id, call_id, message_id, peer, type, direction, event, (SELECT date_sent FROM message WHERE message._id = call.message_id) as timestamp, NULL as ringer, 0 as deletion_timestamp FROM call")) return false; if (!d_database.exec("DROP TABLE group_call_ring") || !d_database.exec("DROP TABLE call") || !d_database.exec("ALTER TABLE call_tmp RENAME TO call")) return false; // setDBV } if (d_databaseversion < 183) { Logger::message("To 183"); if (!d_database.exec("CREATE TABLE call_link (_id INTEGER PRIMARY KEY)")) return false; if (!d_database.exec("CREATE TABLE call_tmp ( _id INTEGER PRIMARY KEY, call_id INTEGER NOT NULL, message_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE SET NULL, peer INTEGER DEFAULT NULL REFERENCES recipient (_id) ON DELETE CASCADE, call_link INTEGER DEFAULT NULL REFERENCES call_link (_id) ON DELETE CASCADE, type INTEGER NOT NULL, direction INTEGER NOT NULL, event INTEGER NOT NULL, timestamp INTEGER NOT NULL, ringer INTEGER DEFAULT NULL, deletion_timestamp INTEGER DEFAULT 0, UNIQUE (_id, peer, call_link) ON CONFLICT FAIL, CHECK ((peer IS NULL AND call_link IS NOT NULL) OR (peer IS NOT NULL AND call_link IS NULL)) )")) return false; if (!d_database.exec("INSERT INTO call_tmp SELECT _id, call_id, message_id, peer, NULL as call_link, type, direction, event, timestamp, ringer, deletion_timestamp FROM call")) return false; if (!d_database.exec("DROP TABLE call") || !d_database.exec("ALTER TABLE call_tmp RENAME TO call")) return false; // setDBV } if (d_databaseversion < 184) { Logger::message("To 184"); if (!d_database.exec("CREATE TABLE call_tmp ( _id INTEGER PRIMARY KEY, call_id INTEGER NOT NULL, message_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE SET NULL, peer INTEGER DEFAULT NULL REFERENCES recipient (_id) ON DELETE CASCADE, call_link INTEGER DEFAULT NULL REFERENCES call_link (_id) ON DELETE CASCADE, type INTEGER NOT NULL, direction INTEGER NOT NULL, event INTEGER NOT NULL, timestamp INTEGER NOT NULL, ringer INTEGER DEFAULT NULL, deletion_timestamp INTEGER DEFAULT 0, UNIQUE (call_id, peer, call_link) ON CONFLICT FAIL, CHECK ((peer IS NULL AND call_link IS NOT NULL) OR (peer IS NOT NULL AND call_link IS NULL)) )")) return false; if (!d_database.exec("INSERT INTO call_tmp SELECT _id, call_id, message_id, peer, NULL as call_link, type, direction, event, timestamp, ringer, deletion_timestamp FROM call")) return false; if (!d_database.exec("DROP TABLE call") || !d_database.exec("ALTER TABLE call_tmp RENAME TO call") || !d_database.exec("CREATE INDEX call_call_id_index ON call (call_id)") || !d_database.exec("CREATE INDEX call_message_id_index ON call (message_id)")) return false; // setDBV184 } if (d_databaseversion < 185) { Logger::message("To 185"); d_selfid = d_database.getSingleResultAs("SELECT _id FROM recipient WHERE " + d_recipient_e164 + " = ?", selfphone, -1); if (d_selfid == -1) { Logger::error("Failed to get _id of self. This will probably end badly..."); return false; } d_selfuuid = d_database.getSingleResultAs("SELECT " + d_recipient_aci + " FROM recipient WHERE _id = ?", d_selfid, std::string()); //getdeps SqliteDB::QueryResults res; if (!d_database.exec("SELECT type, name, sql FROM sqlite_schema WHERE tbl_name = 'message' AND type != 'table'", &res)) return false; for (unsigned int i = 0; i < res.rows(); ++i) if (!d_database.exec("DROP " + res(i, "type") + " IF EXISTS " + res(i, "name"))) return false; // redo message table if (!d_database.exec("CREATE TABLE message_tmp ( _id INTEGER PRIMARY KEY AUTOINCREMENT, date_sent INTEGER NOT NULL, date_received INTEGER NOT NULL, date_server INTEGER DEFAULT -1, thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE, from_recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, from_device_id INTEGER, to_recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, type INTEGER NOT NULL, body TEXT, read INTEGER DEFAULT 0, ct_l TEXT, exp INTEGER, m_type INTEGER, m_size INTEGER, st INTEGER, tr_id TEXT, subscription_id INTEGER DEFAULT -1, receipt_timestamp INTEGER DEFAULT -1, delivery_receipt_count INTEGER DEFAULT 0, read_receipt_count INTEGER DEFAULT 0, viewed_receipt_count INTEGER DEFAULT 0, mismatched_identities TEXT DEFAULT NULL, network_failures TEXT DEFAULT NULL, expires_in INTEGER DEFAULT 0, expire_started INTEGER DEFAULT 0, notified INTEGER DEFAULT 0, quote_id INTEGER DEFAULT 0, quote_author INTEGER DEFAULT 0, quote_body TEXT DEFAULT NULL, quote_missing INTEGER DEFAULT 0, quote_mentions BLOB DEFAULT NULL, quote_type INTEGER DEFAULT 0, shared_contacts TEXT DEFAULT NULL, unidentified INTEGER DEFAULT 0, link_previews TEXT DEFAULT NULL, view_once INTEGER DEFAULT 0, reactions_unread INTEGER DEFAULT 0, reactions_last_seen INTEGER DEFAULT -1, remote_deleted INTEGER DEFAULT 0, mentions_self INTEGER DEFAULT 0, notified_timestamp INTEGER DEFAULT 0, server_guid TEXT DEFAULT NULL, message_ranges BLOB DEFAULT NULL, story_type INTEGER DEFAULT 0, parent_story_id INTEGER DEFAULT 0, export_state BLOB DEFAULT NULL, exported INTEGER DEFAULT 0, scheduled_date INTEGER DEFAULT -1, latest_revision_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE CASCADE, original_message_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE CASCADE, revision_number INTEGER DEFAULT 0 )")) return false; if (!d_database.exec("INSERT INTO message_tmp SELECT _id, date_sent, date_received, date_server, thread_id, recipient_id, recipient_device_id, recipient_id, type, body, read, ct_l, exp, m_type, m_size, st, tr_id, subscription_id, receipt_timestamp, delivery_receipt_count, read_receipt_count, viewed_receipt_count, mismatched_identities, network_failures, expires_in, expire_started, notified, quote_id, quote_author, quote_body, quote_missing, quote_mentions, quote_type, shared_contacts, unidentified, link_previews, view_once, reactions_unread, reactions_last_seen, remote_deleted, mentions_self, notified_timestamp, server_guid, message_ranges, story_type, parent_story_id, export_state, exported, scheduled_date, NULL AS latest_revision_id, NULL AS original_message_id, 0 as revision_number FROM message")) return false; if (!d_database.exec("UPDATE message_tmp SET to_recipient_id = from_recipient_id,from_recipient_id = ?,from_device_id = 1 WHERE (type & 0x1f IN (21, 23, 22, 24, 25, 26, 2, 11))", d_selfid)) return false; if (!d_database.exec("DROP TABLE message") || !d_database.exec("ALTER TABLE message_tmp RENAME TO message")) return false; for (unsigned int i = 0; i < res.rows(); ++i) { if (res(i, "name") == "mms_thread_story_parent_story_scheduled_date_index") { if (!d_database.exec("CREATE INDEX message_thread_story_parent_story_scheduled_date_latest_revision_id_index ON message (thread_id, date_received, story_type, parent_story_id, scheduled_date, latest_revision_id)")) return false; } else if (res(i, "name") == "mms_quote_id_quote_author_scheduled_date_index") { if (!d_database.exec("CREATE INDEX message_quote_id_quote_author_scheduled_date_latest_revision_id_index ON message (quote_id, quote_author, scheduled_date, latest_revision_id)")) return false; } else if (res(i, "name") == "mms_date_sent_index") { if (!d_database.exec("CREATE INDEX message_date_sent_from_to_thread_index ON message (date_sent, from_recipient_id, to_recipient_id, thread_id)")) return false; } else { std::string statement(res(i, "sql")); if (!statement.empty()) { statement = std::regex_replace(statement, std::regex("CREATE INDEX mms_"), "CREATE INDEX message_"); if (!d_database.exec(statement)) return false; } } } if (!checkDbIntegrity()) return false; // setDBV } if (d_databaseversion < 186) { Logger::message("To 186"); if (d_database.tableContainsColumn("message", "from_recipient_id")) { if (!d_database.exec("CREATE INDEX IF NOT EXISTS message_original_message_id_index ON message (original_message_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS message_latest_revision_id_index ON message (latest_revision_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS message_from_recipient_id_index ON message (from_recipient_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS message_to_recipient_id_index ON message (to_recipient_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS reaction_author_id_index ON reaction (author_id)") || !d_database.exec("DROP INDEX IF EXISTS message_quote_id_quote_author_scheduled_date_index") || !d_database.exec("CREATE INDEX IF NOT EXISTS message_quote_id_quote_author_scheduled_date_latest_revision_id_index ON message (quote_id, quote_author, scheduled_date, latest_revision_id)")) return false; if (!d_database.exec("ANALYZE message")) return false; } // setDBV } if (d_databaseversion < 187) { Logger::message("To 187"); if (!d_database.exec("CREATE INDEX IF NOT EXISTS call_call_link_index ON call (call_link)") || !d_database.exec("CREATE INDEX IF NOT EXISTS call_peer_index ON call (peer)") || !d_database.exec("CREATE INDEX IF NOT EXISTS distribution_list_member_recipient_id ON distribution_list_member (recipient_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS msl_message_payload_index ON msl_message (payload_id)")) return false; // setDBV } if (d_databaseversion < 188) { Logger::message("To 188"); if (!d_database.tableContainsColumn("message", "from_recipient_id")) { Logger::error("This should not happen... "); return false; } // These are the indexes that should have been created in V186 -- conditionally done here in case it didn't run properly if (!d_database.exec("CREATE INDEX IF NOT EXISTS message_original_message_id_index ON message (original_message_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS message_latest_revision_id_index ON message (latest_revision_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS message_from_recipient_id_index ON message (from_recipient_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS message_to_recipient_id_index ON message (to_recipient_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS reaction_author_id_index ON reaction (author_id)") || !d_database.exec("DROP INDEX IF EXISTS message_quote_id_quote_author_scheduled_date_index") || !d_database.exec("CREATE INDEX IF NOT EXISTS message_quote_id_quote_author_scheduled_date_latest_revision_id_index ON message (quote_id, quote_author, scheduled_date, latest_revision_id)")) return false; // setDBV } if (d_databaseversion < 189) { Logger::message("To 189"); if (!d_database.exec("CREATE TABLE call_link_tmp ( _id INTEGER PRIMARY KEY, root_key BLOB NOT NULL, room_id TEXT NOT NULL UNIQUE, admin_key BLOB, name TEXT NOT NULL, restrictions INTEGER NOT NULL, revoked INTEGER NOT NULL, expiration INTEGER NOT NULL, avatar_color TEXT NOT NULL )")) return false; if (!d_database.exec("CREATE TABLE call_tmp ( _id INTEGER PRIMARY KEY, call_id INTEGER NOT NULL, message_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE SET NULL, peer INTEGER DEFAULT NULL REFERENCES recipient (_id) ON DELETE CASCADE, call_link TEXT DEFAULT NULL REFERENCES call_link (room_id) ON DELETE CASCADE, type INTEGER NOT NULL, direction INTEGER NOT NULL, event INTEGER NOT NULL, timestamp INTEGER NOT NULL, ringer INTEGER DEFAULT NULL, deletion_timestamp INTEGER DEFAULT 0, UNIQUE (call_id, peer, call_link) ON CONFLICT FAIL, CHECK ((peer IS NULL AND call_link IS NOT NULL) OR (peer IS NOT NULL AND call_link IS NULL)))")) return false; if (!d_database.exec("INSERT INTO call_tmp SELECT _id, call_id, message_id, peer, NULL as call_link, type, direction, event, timestamp, ringer, deletion_timestamp FROM call")) return false; if (!d_database.exec("DROP TABLE call") || !d_database.exec("ALTER TABLE call_tmp RENAME TO call") || !d_database.exec("DROP TABLE call_link") || !d_database.exec("ALTER TABLE call_link_tmp RENAME TO call_link") || !d_database.exec("CREATE INDEX IF NOT EXISTS call_call_id_index ON call (call_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS call_message_id_index ON call (message_id)") || !d_database.exec("CREATE INDEX IF NOT EXISTS call_call_link_index ON call (call_link)") || !d_database.exec("CREATE INDEX IF NOT EXISTS call_peer_index ON call (peer)")) return false; // setDBV } if (d_databaseversion < 190) { Logger::message("To 190"); // (yes this one's empty, it was buggy and replaced by 191 // setDBV } if (d_databaseversion < 191) { Logger::message("To 191"); if (!d_database.exec("DROP INDEX IF EXISTS message_unique_sent_from_thread")) return false; // change date for bad decrypt, expiration_timer_update and chat_session_refreshed duplicates if (!d_database.exec("WITH needs_update AS " "(" " SELECT _id FROM message M WHERE " " (" " type & 0x40000 != 0 OR " " type & 0x10000000 != 0 OR " " type = 13" " )" " AND " " (" " SELECT COUNT(*) FROM message INDEXED BY message_date_sent_from_to_thread_index WHERE " " date_sent = M.date_sent AND " " from_recipient_id = M.from_recipient_id AND " " thread_id = M.thread_id" " ) > 1" ") " "UPDATE message SET date_sent = date_sent - 1 WHERE _id IN needs_update")) return false; int changed = d_database.changed(); if (changed) Logger::message("Updated ", changed, " doubled messages"); if (d_database.exec("SELECT COUNT(*) FROM message") && d_database.exec("PRAGMA quick_check")) Logger::message("Database still appears ok"); // delete other duplicates if (body is identical or type = groupupdate) if (!d_database.exec("WITH needs_delete AS " "(" " SELECT _id FROM message M WHERE " " _id > " " (" " SELECT min(_id) FROM message INDEXED BY message_date_sent_from_to_thread_index WHERE " " date_sent = M.date_sent AND " " from_recipient_id = M.from_recipient_id AND " " thread_id = M.thread_id AND " " ( " " COALESCE(body, '') = COALESCE(M.body, '') OR " " type & 0x10000 != 0" " )" " )" ") " "DELETE FROM message WHERE _id IN needs_delete")) return false; changed = d_database.changed(); if (changed) Logger::message("Updated ", changed, " doubled messages"); if (d_database.exec("SELECT COUNT(*) FROM message") && d_database.exec("PRAGMA quick_check")) Logger::message("Database still appears ok"); // #warning !!! REMOVE THIS !!! // SqliteDB::QueryResults res2; // if (d_database.exec("SELECT type, date_received, date_sent, from_recipient_id, to_recipient_id, thread_id, body FROM message WHERE _id IN (6851,6852,6853,6867,6868,6869,6803,6804,6805)", &res2)) // { // for (unsigned int i = 0; i < 5; ++i) // for (unsigned int j = 0; j < res2.rows(); ++j) // if (d_database.exec("INSERT INTO message (type, date_received, date_sent, from_recipient_id, to_recipient_id, thread_id, body) VALUES (?, ?, ?, ?, ?, ?, ?)", // {res2.value(j, "type"), res2.value(j, "date_received"), res2.value(0, "date_sent"), res2.value(j, "from_recipient_id"), res2.value(j, "to_recipient_id"), res2.value(j, "thread_id"), res2.valueAsString(j, "body") + " " + bepaald::toString(i + 1)})) // ;//std::cout << "added some dupes"); // } //findRemainingDuplicates(db) SqliteDB::QueryResults res; if (!d_database.exec("WITH dupes AS " "(" " SELECT _id, date_sent, from_recipient_id, thread_id, type FROM message M WHERE " " (" " SELECT COUNT(*) FROM message INDEXED BY message_date_sent_from_to_thread_index WHERE " " date_sent = M.date_sent AND " " from_recipient_id = M.from_recipient_id AND " " thread_id = M.thread_id" " ) > 1" ") " "SELECT _id, date_sent, from_recipient_id, thread_id, type, body FROM message WHERE " " _id IN (SELECT _id FROM dupes) ORDER BY date_sent ASC, _id ASC", &res)) return false; if (!res.empty()) { //std::cout << "DUPES REMAINING: " << res.rows()); //res.prettyPrint(d_truncate); struct DupeKey { long long int date_sent; long long int thread_id; long long int from_recipient_id; bool operator<(DupeKey const &other) const { return date_sent < other.date_sent || (date_sent == other.date_sent && thread_id < other.thread_id) || (date_sent == other.date_sent && thread_id == other.thread_id && from_recipient_id < other.from_recipient_id); } }; std::map> dupemap; for (unsigned int i = 0; i < res.rows(); ++i) dupemap[{res.valueAsInt(i, "date_sent"), res.valueAsInt(i, "thread_id"), res.valueAsInt(i, "from_recipient_id")}].push_back(res.valueAsInt(i, "_id")); auto dateTaken = [&](long long int date, long long int frid, long long int tid) { return d_database.getSingleResultAs("SELECT EXISTS (SELECT 1 FROM message INDEXED BY message_date_sent_from_to_thread_index WHERE " "date_sent = ? AND from_recipient_id = ? AND thread_id = ?)", {date, frid, tid}, 1) == 1; }; for (auto const &p : dupemap) { // 'p' is one dupe-group, fix it... std::vector dupe_ids(p.second); // make sure the dupes are sorted highest to lowest _id std::sort(dupe_ids.begin(), dupe_ids.end(), std::greater{}); // pun intended long long int candiDATE = p.first.date_sent - 1; // drop the first (highest _id), it can stay dupe_ids.erase(dupe_ids.begin()); for (auto const &i : dupe_ids) { while (dateTaken(candiDATE, p.first.from_recipient_id, p.first.thread_id)) --candiDATE; if (!d_database.exec("UPDATE message SET date_sent = ? WHERE _id = ?", {candiDATE, i})) return false; --candiDATE; } } //d_database.prettyPrint(d_truncate, "SELECT _id, date_sent, from_recipient_id, thread_id, type, body FROM message WHERE _id = 6803 OR _id BETWEEN 73817 AND 73861 ORDER BY from_recipient_id ASC, thread_id ASC, date_sent ASC"); } // setDBV } // if (d_databaseversion < 229) // { // Logger::message("To 229"); // if (!d_database.exec("UPDATE message SET notified = 1 WHERE (type = 3) OR (type = 8)")) // return false; // } // adjust DatabaseVersionFrame DeepCopyingUniquePtr d_new_dbvframe; if (!setFrameFromStrings(&d_new_dbvframe, std::vector{"VERSION:uint32:191"})) { Logger::error("Failed to create new databaseversionframe"); return false; } d_databaseversionframe.reset(d_new_dbvframe.release()); d_databaseversion = 191; setColumnNames(); return checkDbIntegrity(); } signalbackup-tools-20250313-1/signalbackup/migratedatabase.cc000066400000000000000000000702641476450434500240260ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::migrateDatabase(int from, int to) const { // NOTE: This function does not perform a full migrate to a (necessarily) working // backup file. Its only just enough for this programs exportHTML() function. // interpreted from // https://github.com/signalapp/Signal-Android/blob/main/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V168_SingleMessageTableMigration.kt Logger::message("Attempting to migrate database from version ", from, " to version ", to, "..."); if (!d_database.exec("BEGIN TRANSACTION")) return false; // this is a tough one, from 23 -> ~27 if (d_database.containsTable("recipient_preferences") && !d_database.containsTable("recipient")) { // -> 24, adapted from RecipientidMigrationHelper.java // insert missing recipients mentioned in other tables auto insertMissingRecipients = [&](std::string const &table, std::string const &column) { if (!d_database.exec("INSERT INTO recipient_preferences(recipient_ids) SELECT DISTINCT " + column + " FROM " + table + " WHERE " + column + " != '' AND " + column + " != 'insert-address-column' AND " + column + " NOT NULL AND " + column + " NOT IN (SELECT recipient_ids FROM recipient_preferences)")) return false; return true; }; for (auto const &p : {std::pair{"identities", "address"}, std::pair{"sessions", "address"}, std::pair{"thread", "recipient_ids"}, std::pair{"sms", "address"}, std::pair{"mms", "address"}, std::pair{"mms", "quote_author"}, std::pair{"group_receipts", "address"}, std::pair{"groups", "group_id"}}) { if (!insertMissingRecipients(p.first, p.second)) { d_database.exec("ROLLBACK TRANSACTION"); return false; } } // update invalid/missing addresses auto updateMissingAddress = [&](std::string const &table, std::string const &column) { if (!d_database.exec("UPDATE " + table + " SET " + column + " = -1 " + "WHERE " + column + " = '' OR " + column + " IS NULL OR " + column + " = 'insert-address-token'")) return false; return true; }; for (auto const &p : {std::pair{"sms", "address"}, std::pair{"mms", "address"}, std::pair{"mms", "quote_author"}}) { if (!updateMissingAddress(p.first, p.second)) { d_database.exec("ROLLBACK TRANSACTION"); return false; } } // add column to groups if (!d_database.exec("ALTER TABLE groups ADD COLUMN recipient_id INTEGER DEFAULT 0")) { d_database.exec("ROLLBACK TRANSACTION"); return false; } // update address -> recipient_id auto addressTorecipientId = [&](std::string const &table, std::string const &column) { if (!d_database.exec("UPDATE " + table + " SET " + column + " = " "(SELECT _id FROM recipient_preferences WHERE recipient_preferences.recipient_ids = " + table + "." + column + ")")) return false; return true; }; for (auto const &p : {std::pair{"identities", "address"}, std::pair{"sessions", "address"}, std::pair{"thread", "recipient_ids"}, std::pair{"sms", "address"}, std::pair{"mms", "address"}, std::pair{"mms", "quote_author"}, std::pair{"group_receipts", "address"}}) { if (!addressTorecipientId(p.first, p.second)) { d_database.exec("ROLLBACK TRANSACTION"); return false; } } // same for new groups.recipient_id column if (!d_database.exec("UPDATE groups SET recipient_id = (SELECT _id FROM recipient_preferences WHERE recipient_preferences.recipient_ids = groups.group_id)")) { d_database.exec("ROLLBACK TRANSACTION"); return false; } // find missing recipients in group members std::set missinggroupmembers; SqliteDB::QueryResults groupmembers; if (!d_database.exec("SELECT members FROM groups", &groupmembers)) { d_database.exec("ROLLBACK TRANSACTION"); return false; } for (unsigned int i = 0; i < groupmembers.rows(); ++i) { std::vector individual_groupmembers; std::string membersstring(groupmembers(i, "members")); std::regex comma(","); std::sregex_token_iterator iter(membersstring.begin(), membersstring.end(), comma, -1); std::transform(iter, std::sregex_token_iterator(), std::back_inserter(individual_groupmembers), [](std::string const &m) -> std::string { return m; }); for (auto const &m : individual_groupmembers) if (!m.empty() && d_database.getSingleResultAs("SELECT _id FROM recipient_preferences WHERE recipient_ids = ?", m, -1) == -1) missinggroupmembers.insert(m); } // insert missing group members: for (auto const &mm : missinggroupmembers) { //std::cout << "Missing member: " << mm << std::endl; if (!d_database.exec("INSERT INTO recipient_preferences(recipient_ids) VALUES (?)", mm)) { d_database.exec("ROLLBACK TRANSACTION"); return false; } } // now migrate group members address -> _id if (!d_database.exec("SELECT _id, members FROM groups", &groupmembers)) { d_database.exec("ROLLBACK TRANSACTION"); return false; } for (unsigned int i = 0; i < groupmembers.rows(); ++i) { long long int gid = groupmembers.getValueAs(i, "_id"); std::vector individual_groupmembers; std::string membersstring(groupmembers(i, "members")); std::regex comma(","); std::sregex_token_iterator iter(membersstring.begin(), membersstring.end(), comma, -1); std::transform(iter, std::sregex_token_iterator(), std::back_inserter(individual_groupmembers), [](std::string const &m) -> std::string { return m; }); std::string members_id_str; for (auto const &m : individual_groupmembers) { long long int mid = d_database.getSingleResultAs("SELECT _id FROM recipient_preferences WHERE recipient_ids = ?", m, -1); if (mid == -1) { d_database.exec("ROLLBACK TRANSACTION"); return false; } members_id_str += (members_id_str.empty() ? "" : ",") + bepaald::toString(mid); } //std::cout << membersstring << " -> " << members_id_str << std::endl; if (!d_database.exec("UPDATE groups SET members = ? WHERE _id = ?", {members_id_str, gid})) { d_database.exec("ROLLBACK TRANSACTION"); return false; } } // create recipient table // NOTE setColumnNames() WAS ALREADY CALLED AT THIS POINT, SINCE IT CHECKS FOR COLUMN NAMES IN THE 'recipient' TABLE WHICH // DID NOT EXIST AT THIS POINT, SOME COLUMNS ARE EXPECTED TO HAVE DIFFERENT (MORE MODERN) NAMES if (!d_database.exec("CREATE TABLE recipient (_id INTEGER PRIMARY KEY AUTOINCREMENT, " + d_recipient_aci + " TEXT UNIQUE DEFAULT NULL, " + d_recipient_e164 + " TEXT UNIQUE DEFAULT NULL, email TEXT UNIQUE DEFAULT NULL, group_id TEXT UNIQUE DEFAULT NULL, blocked INTEGER DEFAULT 0, message_ringtone TEXT DEFAULT NULL, message_vibrate INTEGER DEFAULT 0, call_ringtone TEXT DEFAULT NULL, call_vibrate INTEGER DEFAULT 0, notification_channel TEXT DEFAULT NULL, mute_until INTEGER DEFAULT 0, " + d_recipient_avatar_color + " TEXT DEFAULT NULL, seen_invite_reminder INTEGER DEFAULT 0, default_subscription_id INTEGER DEFAULT -1, message_expiration_time INTEGER DEFAULT 0, registered INTEGER DEFAULT 0, " + d_recipient_system_joined_name + " TEXT DEFAULT NULL, system_photo_uri TEXT DEFAULT NULL, system_phone_label TEXT DEFAULT NULL, system_contact_uri TEXT DEFAULT NULL, profile_key TEXT DEFAULT NULL, " + d_recipient_profile_given_name + " TEXT DEFAULT NULL, " + d_recipient_profile_avatar + " TEXT DEFAULT NULL, profile_sharing INTEGER DEFAULT 0, unidentified_access_mode INTEGER DEFAULT 0, force_sms_selection INTEGER DEFAULT 0)")) { d_database.exec("ROLLBACK TRANSACTION"); return false; } // fill new table SqliteDB::QueryResults recipient_preferences_contents; if (!d_database.exec("SELECT * FROM recipient_preferences", &recipient_preferences_contents)) { d_database.exec("ROLLBACK TRANSACTION"); return false; } for (unsigned int i = 0; i < recipient_preferences_contents.rows(); ++i) { std::string address = recipient_preferences_contents(i, "recipient_ids"); bool isgroup = STRING_STARTS_WITH(address, "__textsecure_group__!"); bool isemail = (address.find('@') != std::string::npos) && (address.find('.') != std::string::npos); // THIS IS CERTAINLY NOT CORRECT bool isphone = !isgroup && !isemail; insertRow("recipient", {{"_id", recipient_preferences_contents.value(i, "_id")}, {isphone ? d_recipient_e164 : "", address}, {isemail ? "email" : "", address}, {isgroup ? "group_id" : "", address}, {"blocked", recipient_preferences_contents.value(i, "block")}, {"message_ringtone", recipient_preferences_contents.value(i, "notification")}, {"message_vibrate", recipient_preferences_contents.value(i, "vibrate")}, {"call_ringtone", recipient_preferences_contents.value(i, "call_ringtone")}, {"call_vibrate", recipient_preferences_contents.value(i, "call_vibrate")}, {"notification_channel", recipient_preferences_contents.value(i, "notification_channel")}, {"mute_until", recipient_preferences_contents.value(i, "mute_until")}, {d_recipient_avatar_color, recipient_preferences_contents.value(i, "color")}, {"seen_invite_reminder", recipient_preferences_contents.value(i, "seen_invite_reminder")}, {"default_subscription_id", recipient_preferences_contents.value(i, "default_subscription_id")}, {"message_expiration_time", recipient_preferences_contents.value(i, "expire_messages")}, {"registered", recipient_preferences_contents.value(i, "registered")}, {d_recipient_system_joined_name, recipient_preferences_contents.value(i, "system_display_name")}, {"system_photo_uri", recipient_preferences_contents.value(i, "system_phone_label")}, {"system_contact_uri", recipient_preferences_contents.value(i, "system_contact_uri")}, {"profile_key", recipient_preferences_contents.value(i, "profile_key")}, {d_recipient_profile_given_name, recipient_preferences_contents.value(i, "signal_profile_name")}, {d_recipient_profile_avatar, recipient_preferences_contents.value(i, "signal_profile_avatar")}, {"profile_sharing", recipient_preferences_contents.value(i, "profile_sharing_approval")}, {"unidentified_access_mode", recipient_preferences_contents.value(i, "unidentified_access_mode")}, {"force_sms_selection", recipient_preferences_contents.value(i, "force_sms_selection")}}); } // drop old d_database.exec("DROP TABLE recipient_preferences"); // -> 25 if (!d_database.exec("ALTER TABLE recipient ADD COLUMN system_phone_type INTEGER DEFAULT -1")) { d_database.exec("ROLLBACK TRANSACTION"); return false; } // then it makes sure own phone number is in recipient table and // sets phone/registered/profile_sharing/signal_profile_name columns // but we can't do that because we cant know own phone number... // let's assume it is present already (I think it usually is) // -> 26 // this migration attempts to find non-group recipients that are not used anywhere to delete them (unless // their 'email' column is not null for some reason). we dont care and will just leave them. // -> 27 // appears to set address to -1 if address is 0 in mms table... if (!d_database.exec("UPDATE mms SET address = -1 WHERE address = 0")) { d_database.exec("ROLLBACK TRANSACTION"); return false; } } // create reaction table if not present if (!d_database.containsTable("reaction")) { if (!d_database.exec("CREATE TABLE reaction (_id INTEGER PRIMARY KEY, message_id INTEGER NOT NULL, is_mms INTEGER NOT NULL, author_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, emoji TEXT NOT NULL, date_sent INTEGER NOT NULL, date_received INTEGER NOT NULL, UNIQUE(message_id, is_mms, author_id) ON CONFLICT REPLACE)")) { d_database.exec("ROLLBACK TRANSACTION"); return false; } // fill it for (auto const &msgtable : {"sms"s, d_mms_table}) { // skip if not present if (!d_database.tableContainsColumn(msgtable, "reactions")) continue; SqliteDB::QueryResults results; d_database.exec("SELECT _id, reactions FROM "s + msgtable + " WHERE reactions IS NOT NULL", &results); for (unsigned int i = 0; i < results.rows(); ++i) { ReactionList reactions(results.getValueAs, size_t>>(i, "reactions")); for (unsigned int j = 0; j < reactions.numReactions(); ++j) { if (!insertRow("reaction", {{"message_id", results.getValueAs(i, "_id")}, {"is_mms", (msgtable == "sms" ? 0 : 1)}, {"author_id", reactions.getAuthor(j)}, {"emoji", reactions.getEmoji(j)}, {"date_sent", reactions.getSentTime(j)}, {"date_received", reactions.getReceivedTime(j)}})) { d_database.exec("ROLLBACK TRANSACTION"); return false; } } } d_database.exec("ALTER TABLE " + msgtable + " DROP COLUMN reactions"); } } // create mention table if not present if (!d_database.containsTable("mention")) { if (!d_database.exec("CREATE TABLE mention (_id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER, message_id INTEGER, recipient_id INTEGER, range_start INTEGER, range_length INTEGER)")) { d_database.exec("ROLLBACK TRANSACTION"); return false; } } // add any missing columns to mms (from dbv 123 ( or hopefully -> 99) auto ensureColumns = [&](std::string const &table, std::string const &column, std::string const &columndefinition) { if (!d_database.tableContainsColumn(table, column)) if (!d_database.exec("ALTER TABLE " + table + " ADD COLUMN " + column + " " + columndefinition)) return false; return true; }; for (auto const &p : {std::pair{"receipt_timestamp", "INTEGER DEFAULT -1"}, std::pair{"date_server", "INTEGER DEFAULT -1"}, std::pair{"reactions_unread", "INTEGER DEFAULT 0"}, std::pair{"remote_deleted", "INTEGER DEFAULT 0"}, std::pair{"mentions_self", "INTEGER DEFAULT 0"}, std::pair{"reactions_last_seen", "INTEGER DEFAULT -1"}, std::pair{"quote_mentions", "BLOB DEFAULT NULL"}, std::pair{"quote_type", "INTEGER DEFAULT 0"}, std::pair{"link_previews", "TEXT DEFAULT NULL"}, std::pair{"view_once", "INTEGER DEFAULT 0"}, std::pair{d_mms_ranges, "BLOB DEFAULT NULL"}, std::pair{"story_type", "INTEGER DEFAULT 0"}, std::pair{"parent_story_id", "INTEGER DEFAULT 0"}, std::pair{"export_state", "BLOB DEFAULT NULL"}, std::pair{"server_guid", "TEXT DEFAULT NULL"}, std::pair{"exported", "INTEGER DEFAULT 0"}, std::pair{"viewed_receipt_count", "INTEGER DEFAULT 0"}, std::pair{"notified_timestamp", "INTEGER DEFAULT 0"}}) { if (!ensureColumns(d_mms_table, p.first, p.second)) { d_database.exec("ROLLBACK TRANSACTION"); return false; } } for (auto const &p : {std::pair{"receipt_timestamp", "INTEGER DEFAULT -1"}, std::pair{"date_server", "INTEGER DEFAULT -1"}, std::pair{"reactions_unread", "INTEGER DEFAULT 0"}, std::pair{"reactions_last_seen", "INTEGER DEFAULT -1"}, std::pair{"remote_deleted", "INTEGER DEFAULT 0"}, std::pair{"export_state", "BLOB DEFAULT NULL"}, std::pair{"server_guid", "TEXT DEFAULT NULL"}, std::pair{"exported", "INTEGER DEFAULT 0"}, std::pair{"notified_timestamp", "INTEGER DEFAULT 0"}}) { if (!ensureColumns("sms", p.first, p.second)) { d_database.exec("ROLLBACK TRANSACTION"); return false; } } for (auto const &p : {std::pair{"wallpaper", "BLOB DEFAULT NULL"}, std::pair{"chat_colors", "BLOB DEFAULT NULL"}, std::pair{"username", "TEXT DEFAULT NULL"}, std::pair{"hidden", "INTEGER DEFAULT 0"}}) { if (!ensureColumns("recipient", p.first, p.second)) { d_database.exec("ROLLBACK TRANSACTION"); return false; } } if (!d_database.exec("DROP TRIGGER IF EXISTS msl_sms_delete") || !d_database.exec("DROP TRIGGER IF EXISTS reactions_sms_delete") || !d_database.exec("DROP TRIGGER IF EXISTS sms_ai") || !d_database.exec("DROP TRIGGER IF EXISTS sms_au") || !d_database.exec("DROP TRIGGER IF EXISTS sms_ad") || !d_database.exec("DROP TABLE IF EXISTS sms_fts") || !d_database.exec("DROP INDEX IF EXISTS mms_read_and_notified_and_thread_id_index") || !d_database.exec("DROP INDEX IF EXISTS mms_type_index") || !d_database.exec("DROP INDEX IF EXISTS mms_date_sent_index") || !d_database.exec("DROP INDEX IF EXISTS mms_date_server_index") || !d_database.exec("DROP INDEX IF EXISTS mms_thread_date_index") || !d_database.exec("DROP INDEX IF EXISTS mms_reactions_unread_index") || !d_database.exec("DROP INDEX IF EXISTS mms_story_type_index") || !d_database.exec("DROP INDEX IF EXISTS mms_parent_story_id_index") || !d_database.exec("DROP INDEX IF EXISTS mms_thread_story_parent_story_index") || !d_database.exec("DROP INDEX IF EXISTS mms_quote_id_quote_author_index") || !d_database.exec("DROP INDEX IF EXISTS mms_exported_index") || !d_database.exec("DROP INDEX IF EXISTS mms_id_type_payment_transactions_index") || !d_database.exec("DROP TRIGGER IF EXISTS mms_ai")) { d_database.exec("ROLLBACK TRANSACTION"); return false; } SqliteDB::QueryResults minmax; if (!d_database.exec("SELECT MIN(_id) AS min, MAX(_id) AS max FROM sms", &minmax)) { d_database.exec("ROLLBACK TRANSACTION"); return false; } long long int min = minmax.getValueAs(0, "min"); long long int max = minmax.getValueAs(0, "max"); for (unsigned int i = min; i <= max; ++i) { SqliteDB::QueryResults newmmsid; if (!d_database.exec("INSERT INTO mms " "(" + d_mms_date_sent + ", " "date_received, " "date_server, " "thread_id, " + d_mms_recipient_id + ", " + d_mms_recipient_device_id + ", " + d_mms_type + ", " "body, " "read, " "ct_l, " "exp, " "m_type, " "m_size, " "st, " "tr_id, " "subscription_id, " "receipt_timestamp, " "delivery_receipt_count, " // renamed (has_delivery_receipt), but after v170 "read_receipt_count, " // " "viewed_receipt_count, " // " "mismatched_identities, " "network_failures, " "expires_in, " "expire_started, " "notified, " "quote_id, " "quote_author, " "quote_body, " "quote_missing, " "quote_mentions, " "quote_type, " "shared_contacts, " "unidentified, " "link_previews, " "view_once, " "reactions_unread, " "reactions_last_seen, " "remote_deleted, " "mentions_self, " "notified_timestamp, " "server_guid, " + d_mms_ranges + ", " "story_type, " "parent_story_id, " "export_state, " "exported) " "SELECT " "date_sent, " + d_sms_date_received + ", " "date_server, " "thread_id, " + d_sms_recipient_id + ", " + d_sms_recipient_device_id + ", " "type, " "body, " "read, " "null, " "0, " "0, " "0, " "status, " "null, " "subscription_id, " "receipt_timestamp, " "delivery_receipt_count, " "read_receipt_count, " "0, " // view_receipt (not present in sms table) "mismatched_identities, " "null, " "expires_in, " "expire_started, " "notified, " "0, " "0, " "null, " "0, " "null, " "0, " "null, " "unidentified, " "null, " "0, " "reactions_unread, " "reactions_last_seen, " "remote_deleted, " "0, " "notified_timestamp, " "server_guid, " "null, " "0, " "0, " "export_state, " "exported " "FROM " "sms " "WHERE " #if SQLITE_VERSION_NUMBER < 3035000 // RETURNING was not available prior to 3.35.0 "_id IS ?", i)) #else "_id IS ? RETURNING _id", i, &newmmsid)) #endif { Logger::error("copying sms._id: ", i); d_database.exec("ROLLBACK TRANSACTION"); return false; } #if SQLITE_VERSION_NUMBER < 3035000 // RETURNING was not available prior to 3.35.0 if (true) { long long int newestmmsid = d_database.lastId(); #else if (newmmsid.rows()) { long long int newestmmsid = newmmsid.getValueAs(0, "_id"); #endif // update reactions if (!d_database.exec("UPDATE reaction SET message_id = ?, is_mms = 1 WHERE message_id IS ? AND is_mms = 0", {newestmmsid, i})) { d_database.exec("ROLLBACK TRANSACTION"); return false; } // update msl_tables (probably not necessary) if (d_database.containsTable("msl_message")) { if (!d_database.exec("UPDATE msl_message SET message_id = ?, is_mms = 1 WHERE message_id IS ? AND is_mms = 0", {newestmmsid, i})) { d_database.exec("ROLLBACK TRANSACTION"); return false; } } } } if (!d_database.exec("DROP TABLE sms")) { d_database.exec("ROLLBACK TRANSACTION"); return false; } if (!d_database.exec("CREATE INDEX mms_read_and_notified_and_thread_id_index ON mms(read, notified, thread_id)") || !d_database.exec("CREATE INDEX mms_type_index ON mms (" + d_mms_type + ")") || !d_database.exec("CREATE INDEX mms_date_sent_index ON mms (" + d_mms_date_sent + ", " + d_mms_recipient_id + ", thread_id)") || !d_database.exec("CREATE INDEX mms_date_server_index ON mms (date_server)") || !d_database.exec("CREATE INDEX mms_thread_date_index ON mms (thread_id, date_received)") || !d_database.exec("CREATE INDEX mms_reactions_unread_index ON mms (reactions_unread)") || !d_database.exec("CREATE INDEX mms_story_type_index ON mms (story_type)") || !d_database.exec("CREATE INDEX mms_parent_story_id_index ON mms (parent_story_id)") || !d_database.exec("CREATE INDEX mms_thread_story_parent_story_index ON mms (thread_id, date_received, story_type, parent_story_id)") || !d_database.exec("CREATE INDEX mms_quote_id_quote_author_index ON mms (quote_id, quote_author)") || !d_database.exec("CREATE INDEX mms_exported_index ON mms (exported)") || !d_database.exec("CREATE INDEX mms_id_type_payment_transactions_index ON mms (_id, " + d_mms_type + ") WHERE " + d_mms_type + " & " + bepaald::toString(Types::SPECIAL_TYPE_PAYMENTS_NOTIFICATION) + " != 0") || !d_database.exec("CREATE TRIGGER mms_ai AFTER INSERT ON mms BEGIN INSERT INTO mms_fts (rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id); END;")) { d_database.exec("ROLLBACK TRANSACTION"); return false; } if (d_database.exec("COMMIT")) return true; return false; } signalbackup-tools-20250313-1/signalbackup/missingattachmentexpected.cc000066400000000000000000000110761476450434500261510ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::missingAttachmentExpected(uint64_t rowid, int64_t unique_id) const { // if the attachment was never successfully completely downloaded, the data is expected to be missing. // this is shown by the 'pending_push' field in the part table which can have the following values: // public static final int TRANSFER_PROGRESS_DONE = 0; // public static final int TRANSFER_PROGRESS_STARTED = 1; // public static final int TRANSFER_PROGRESS_PENDING = 2; // public static final int TRANSFER_PROGRESS_FAILED = 3; SqliteDB::QueryResults results; if (d_database.exec("SELECT " + d_part_pending + " FROM " + d_part_table + " WHERE _id = ?" + (d_database.tableContainsColumn(d_part_table, "unique_id") ? " AND unique_id = " + bepaald::toString(unique_id) : "") + " AND " + d_part_pending + " != 0", rowid, &results)) if (results.rows() == 1) return true; // if the attachment is a view once type, it is expected to be missing if (d_database.getSingleResultAs("SELECT " + d_part_ct + " FROM " + d_part_table + " WHERE _id = ?" + (d_database.tableContainsColumn(d_part_table, "unique_id") ? " AND unique_id = " + bepaald::toString(unique_id) : ""), rowid, std::string()) == "application/x-signal-view-once") return true; SqliteDB::QueryResults isquote; d_database.exec("SELECT " + d_part_mid + " FROM " + d_part_table + " WHERE _id = ?" + (d_database.tableContainsColumn(d_part_table, "unique_id") ? " AND unique_id = " + bepaald::toString(unique_id) : "") + " AND quote = 1", rowid, &isquote); if (isquote.rows()) { long long int mid = isquote.getValueAs(0, d_part_mid); // if the attachment is in a quote and the original quote is missing, attachment is expected to be missing (NOT ALWAYS) if (d_database.exec("SELECT _id FROM " + d_mms_table + " WHERE quote_missing = 1 AND _id = ?", mid, &results)) if (results.rows() == 1) return true; // quote_missing is not always (often not?) set to 1 even if quote is missing, so manually check: // check for remote deleted if (d_database.exec("SELECT _id FROM " + d_mms_table + " WHERE remote_deleted IS 1 AND " + d_mms_date_sent + " IS (SELECT quote_id FROM " + d_mms_table + " WHERE _id = ?)", mid, &results)) if (results.rows()) // can be > 1 if message are doubled (and before date_sent had UNIQUE) return true; // check when self-deleted long long int quoteid = 0; if ((quoteid = d_database.getSingleResultAs("SELECT IFNULL(quote_id, 0)_id FROM " + d_mms_table + " WHERE _id = ?", mid, 0)) != 0) { d_database.exec("SELECT _id FROM " + d_mms_table + " WHERE " + d_mms_date_sent + " = ? AND " "thread_id IS (SELECT thread_id FROM " + d_mms_table + " WHERE _id = ?)", {quoteid, mid}, &results); if (results.rows() == 0) return true; } } // if the attachment is in a quote, but required no preview (is not an image or video), attachment // is expected to be missing (though not always) // NOTE // I have seen this fail for a 'image/webp' type, maybe because that particular image type was not supported? (for that phone??) if (d_database.exec("SELECT " + d_part_ct + " FROM " + d_part_table + " WHERE quote = 1 AND _id = ?" + (d_database.tableContainsColumn(d_part_table, "unique_id") ? " AND unique_id = " + bepaald::toString(unique_id) : "") + " AND " + d_part_ct + " NOT LIKE 'image%' AND " + d_part_ct + " NOT LIKE 'video%'", rowid, &results)) if (results.rows() == 1) return true; return false; } signalbackup-tools-20250313-1/signalbackup/msgrange.h000066400000000000000000000026371476450434500223550ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef MSGRANGE_H_ #define MSGRANGE_H_ // data that defines ranges in a message body to be replaced for html output struct Range { long long int start; long long int length; std::string pre; std::string replacement; std::string post; bool nobreak; bool operator<(Range const &other) const { return (start < other.start) || (start == other.start && start + length < other.start + other.length) || (start == other.start && start + length == other.start + other.length && replacement < other.replacement); //return std::tie(start, start + length, replacement) < // std::tie(other.start, other.start + other.length, other.replacement); }; }; #endif signalbackup-tools-20250313-1/signalbackup/prepareoutputdirectory.cc000066400000000000000000000051521476450434500255470ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::prepareOutputDirectory(std::string const &directory, bool overwrite, bool allowappend, bool append) const { // check if dir exists, create if not if (!bepaald::fileOrDirExists(directory)) { // try to create if (!bepaald::createDir(directory)) { Logger::error("Failed to create directory `", directory, "'", " (errno: ", std::strerror(errno), ")"); // note: errno is not required to be set by std // temporary !! { std::error_code ec; std::filesystem::space_info const si = std::filesystem::space(directory, ec); if (!ec) { Logger::message("Available : ", static_cast(si.available)); Logger::message("Backup size: ", d_fd->total()); } } return false; } } // directory exists, but is it a dir? if (!bepaald::isDir(directory)) { Logger::error("`", directory, "' is not a directory."); return false; } // and is it empty? if (!bepaald::isEmpty(directory) && // NOT EMPTY, but we cant write into it, ((allowappend && !append) || !allowappend)) // because append is not allowed, or allowed but not requested { if (!overwrite) { if (allowappend) { Logger::error("Directory '", directory, "' is not empty. Use --overwrite to clear directory contents before"); Logger::error_indent("export, or --append to only write new files."); } else Logger::error("Directory '", directory, "' is not empty. Use --overwrite to clear directory contents before export."); return false; } Logger::message("Clearing contents of directory '", directory, "'..."); if (!bepaald::clearDirectory(directory)) { Logger::error("Failed to empty directory '", directory, "'"); return false; } } return true; } signalbackup-tools-20250313-1/signalbackup/ptcreaterecipient.cc000066400000000000000000000171571476450434500244250ustar00rootroot00000000000000/* Copyright (C) 2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../signalplaintextbackupdatabase/signalplaintextbackupdatabase.h" #if __cpp_lib_bitops >= 201907L #include #endif long long int SignalBackup::ptCreateRecipient(std::unique_ptr const &ptdb, std::map *contactmap, bool *warned_createcontacts, std::string const &contact_name, std::string const &address, bool isgroup) const { Logger::message("Creating recipient for address ", makePrintable(address), " (group: ", std::boolalpha, isgroup, ")"); auto random_from_address = [](std::string const &a) { unsigned int result = 0; for (auto c : a) #if __cpp_lib_bitops >= 201907L result = std::rotl(result, 3) ^ static_cast(c); #else result = ((result << 3) | (result >> (sizeof(int) - 3))) ^ static_cast(c); // for (value in data) hash = hash.rotateLeft(3) xor value.toInt() #endif return result; }; // createcontact: if (*warned_createcontacts == false) { Logger::warning("Chat partner was not found in recipient-table. Attempting to create."); Logger::warning_indent(Logger::Control::BOLD, "NOTE THE RESULTING BACKUP CAN MOST LIKELY NOT BE RESTORED"); Logger::warning_indent("ON SIGNAL ANDROID. IT IS ONLY MEANT TO EXPORT TO HTML.", Logger::Control::NORMAL); *warned_createcontacts = true; } if (contact_name.empty()) [[unlikely]] Logger::warning("Failed to get name for new contact (", makePrintable(address), ")"); if (isgroup) { // std::cout << "GROUP: " << address << std::endl; // std::cout << "NAME: " << contact_name << std::endl; // ptdb->d_database.prettyPrint(false, "SELECT DISTINCT sourceaddress FROM smses WHERE address = ?", address); // ptdb->d_database.prettyPrint(false, "SELECT DISTINCT value FROM smses, json_each(targetaddresses) WHERE smses.address = ?", address); std::set group_members; SqliteDB::QueryResults group_members_res; ptdb->d_database.exec("SELECT DISTINCT sourceaddress FROM smses WHERE address = ?", address, &group_members_res); for (unsigned int i = 0; i < group_members_res.rows(); ++i) { if (group_members_res.isNull(i, "sourceaddress")) [[unlikely]] Logger::warning("Got 'NULL' sourceaddress in group '", makePrintable(address), "'"); else group_members.insert(group_members_res(i, "sourceaddress")); } ptdb->d_database.exec("SELECT DISTINCT value FROM smses, json_each(targetaddresses) WHERE smses.address = ?", address, &group_members_res); for (unsigned int i = 0; i < group_members_res.rows(); ++i) { if (group_members_res.isNull(i, "value")) [[unlikely]] Logger::warning("Got 'NULL' targetaddress in group '", makePrintable(address), "'"); else group_members.insert(group_members_res(i, "value")); } if (d_verbose) [[unlikely]] { Logger::message("ALL GROUP MEMBERS:"); for (auto const &gm : group_members) Logger::message(makePrintable(gm)); } // ensure all group members exist. for (auto const &gm : group_members) { if (!bepaald::contains(contactmap, gm)) { std::string cn = ptdb->d_database.getSingleResultAs("SELECT contact_name FROM smses WHERE address = ? LIMIT 1", gm, std::string()); if (cn.empty()) [[unlikely]] { Logger::warning("Unexpectedly got empty contact name for group recipient '", gm, "'");//makePrintable(gm)); cn = "(unknown)"; } //std::cout << "Need to create group member(2): " << group_members(i, "address") << std::endl; if (ptCreateRecipient(ptdb, contactmap, warned_createcontacts, cn, gm, false) == -1) { Logger::error("Failed to create group member (", makePrintable(gm), ")"); return -1; } // else // std::cout << "Created contact: " << group_members(i, "address") << std::endl; } else if (d_verbose) [[unlikely]] Logger::message("Address already present in contactmap: ", makePrintable(gm)); } d_database.exec("BEGIN TRANSACTION"); // things could still go bad... // create recipient for group std::any group_rid; std::string group_id = "__signal_group__fake__" + address; std::string color = s_html_random_colors[random_from_address(address) % s_html_random_colors.size()].first; if (!insertRow("recipient", {{"group_id", group_id}, {d_recipient_avatar_color, color}, {d_recipient_type, 3}}, // group type "_id", &group_rid)) { Logger::error("Failed to insert new (group) recipient into database."); return -1; } if (group_rid.type() != typeid(long long int)) [[unlikely]] { Logger::error("New (group) recipient _id has unexpected type."); d_database.exec("ROLLBACK TRANSACTION"); return -1; } long long int new_group_rid = std::any_cast(group_rid); // create group if (!insertRow("groups", {{"title", contact_name}, {"group_id", group_id}, {"recipient_id", new_group_rid}, {"avatar_id", 0}, {"revision", 0}})) { Logger::error("Failed to insert new group into database."); d_database.exec("ROLLBACK TRANSACTION"); return -1; } // set group members for (auto const &gm : group_members) { //std::cout << "Create group membership (" << (i + 1) << ")" << std::endl; long long int member_rid = contactmap->at(gm); // they should all exist at this point. if (!insertRow("group_membership", {{"group_id", group_id}, {"recipient_id", member_rid}})) { Logger::error("Failed to set new groups membership."); d_database.exec("ROLLBACK TRANSACTION"); return -1; } } d_database.exec("COMMIT TRANSACTION"); contactmap->emplace(address, new_group_rid); return new_group_rid; } // else : NOT A GROUP std::any new_rid; long long int rid = -1; std::string color = s_html_random_colors[random_from_address(contact_name) % s_html_random_colors.size()].first; insertRow("recipient", {{d_recipient_profile_given_name, contact_name}, {"profile_joined_name", contact_name}, {d_recipient_avatar_color, color}, {d_recipient_e164, address}}, "_id", &new_rid); if (new_rid.type() == typeid(long long int)) [[likely]] rid = std::any_cast(new_rid); if (rid == -1) [[unlikely]] { Logger::warning("Failed to create missing recipient. Skipping message."); return rid; } contactmap->emplace(address, rid); return rid; } signalbackup-tools-20250313-1/signalbackup/remaprecipients.cc000066400000000000000000000022711476450434500240740ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" // called on source! void SignalBackup::remapRecipients() { // CALLED ON SOURCE Logger::message(" Remapping recipients! "); SqliteDB::QueryResults results; d_database.exec("SELECT * FROM remapped_recipients", &results); for (unsigned int i = 0; i < results.rows(); ++i) { updateRecipientId(results.getValueAs(i, "new_id"), results.getValueAs(i, "old_id")); } } signalbackup-tools-20250313-1/signalbackup/removedoubles.cc000066400000000000000000000251461476450434500235630ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::removeDoubles(long long int milliseconds) { Logger::message(__FUNCTION__); //auto t1 = std::chrono::high_resolution_clock::now(); SqliteDB::QueryResults threads; if (!d_database.exec("SELECT _id FROM thread ORDER BY _id ASC", &threads)) return; // note: doing this per thread is not needed, but this gives some sense // of progress as the query can be quite slow long long int removed_total = 0; for (unsigned int i = 0; i < threads.rows(); ++i) { long long int tid = threads.valueAsInt(i, "_id"); long long int removed_last = 0; //SqliteDB::QueryResults todelete; if (!d_database.exec("WITH messages_with_attachmentsize AS " "(" " SELECT " + d_mms_table + "._id, " + d_mms_recipient_id + ", thread_id, " + d_mms_date_sent + ", " + d_mms_type + ", body, IFNULL(COUNT(data_size), 0) AS numattachments, IFNULL(SUM(data_size), 0) AS totalfilesize FROM " + d_mms_table + " LEFT JOIN " + d_part_table + " ON message_id IS " + d_mms_table + "._id" " WHERE thread_id = ?" " GROUP BY " + d_mms_table + "._id, " + d_mms_recipient_id + ", thread_id, " + d_mms_date_sent + ", " + d_mms_type + ", body" "), " "candidates AS " "(" " SELECT M._id, M." + d_mms_recipient_id + ", M.thread_id, M." + d_mms_date_sent + ", M." + d_mms_type + ", M.body, M.numattachments, M.totalfilesize FROM" " (" " SELECT _id, " + d_mms_recipient_id + ", thread_id, " + d_mms_date_sent + ", " + d_mms_type + ", body, numattachments, totalfilesize FROM messages_with_attachmentsize" " GROUP BY " + d_mms_recipient_id + ", thread_id, " + d_mms_type + ", COALESCE(body, ''), numattachments, totalfilesize" " HAVING COUNT(*) > 1" " ) AS D" " JOIN messages_with_attachmentsize AS M ON" " COALESCE(M.body, '') = COALESCE(D.body, '') AND" " M.numattachments = D.numattachments AND" " M.totalfilesize = D.totalfilesize AND" " M." + d_mms_recipient_id + " = D." + d_mms_recipient_id + " AND" " M." + d_mms_type + " = D." + d_mms_type + " ORDER BY M._id" ")," "to_delete AS " "(" " SELECT _id FROM candidates C WHERE " " _id > " " (" " SELECT min(_id) FROM candidates WHERE " " COALESCE(body, '') = COALESCE(C.body, '') AND " " numattachments = C.numattachments AND" " totalfilesize = C.totalfilesize AND" " " + d_mms_recipient_id + " = C." + d_mms_recipient_id + " AND " " " + d_mms_type + " = C." + d_mms_type + " AND" " ABS(" + d_mms_date_sent + " - C." + d_mms_date_sent + ") <= ?" " )" ") " //" SELECT _id FROM to_delete", {tid, milliseconds}, &todelete)) "DELETE FROM " + d_mms_table + " WHERE _id IN to_delete", {tid, milliseconds})) { Logger::error("Failed to delete doubles"); return; } removed_last = d_database.changed(); removed_total += removed_last; Logger::message("Deleted ", (removed_last ? Logger::Control::BOLD : Logger::Control::NORMAL), removed_last, Logger::Control::NORMAL, " duplicate entries from thread ", tid, " (", i + 1, "/", threads.rows(), ")"); //todelete.prettyPrint(false); } Logger::message("Removed ", (removed_total ? Logger::Control::BOLD : Logger::Control::NORMAL), removed_total, Logger::Control::NORMAL, " doubled messages from database"); // auto t2 = std::chrono::high_resolution_clock::now(); // auto ms_int = std::chrono::duration_cast(t2 - t1); // std::cout << " *** TIME: " << ms_int.count() << "ms\n"; //cleanDatabaseByMessages(); // cleandatabasebymessages is a complicated (risky?) function because it messes with recipients. // theoretically, removing doubled messages should not affect the recipient table // here we duplicate the relevant parts of cleanDatabaseByMessages() // remove dangling parts Logger::message(" Deleting attachment entries from '", d_part_table, "' not belonging to remaining mms entries"); d_database.exec("DELETE FROM " + d_part_table + " WHERE " + d_part_mid + " NOT IN (SELECT DISTINCT _id FROM " + d_mms_table + ")"); Logger::message(" Removed ", d_database.changed(), " ", d_part_table, "-entries."); // remove unused attachments Logger::message(" Deleting unused attachments..."); SqliteDB::QueryResults results; d_database.exec("SELECT _id," + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id"s : "-1 AS unique_id"s) + " FROM " + d_part_table, &results); int constexpr INVALID_ID = -10; for (auto it = d_attachments.begin(); it != d_attachments.end();) { bool found = false; for (unsigned int i = 0; i < results.rows(); ++i) { long long int rowid = INVALID_ID; if (results.valueHasType(i, "_id")) rowid = results.getValueAs(i, "_id"); long long int uniqueid = INVALID_ID; if (results.valueHasType(i, "unique_id")) uniqueid = results.getValueAs(i, "unique_id"); if (rowid != INVALID_ID && uniqueid != INVALID_ID && it->first.first == static_cast(rowid) && it->first.second == static_cast(uniqueid)) { found = true; break; } } if (!found) it = d_attachments.erase(it); else ++it; } // remove unused group_receipts Logger::message(" Deleting group receipts entries from deleted messages..."); d_database.exec("DELETE FROM group_receipts WHERE mms_id NOT IN (SELECT DISTINCT _id FROM " + d_mms_table + ")"); Logger::message(" Removed ", d_database.changed(), " group_receipts-entries."); // remove unused mentions if (d_database.containsTable("mention")) { Logger::message_start(" Deleting entries from 'mention' not belonging to remaining mms entries"); d_database.exec("DELETE FROM mention WHERE message_id NOT IN (SELECT DISTINCT _id FROM " + d_mms_table + ")"); Logger::message_end(" (", d_database.changed(), ")"); } // remove unused call details if (d_database.containsTable("call")) { Logger::message_start(" Deleting entries from 'call' not belonging to remaining message entries"); d_database.exec("DELETE FROM call WHERE message_id NOT IN (SELECT DISTINCT _id FROM " + d_mms_table + ")"); Logger::message_end(" (", d_database.changed(), ")"); } // remove unreferencing reactions if (d_database.containsTable("reaction")) { if (d_database.containsTable("sms")) { Logger::message_start(" Deleting entries from 'reaction' not belonging to remaining sms entries"); d_database.exec("DELETE FROM reaction WHERE is_mms IS NOT 1 AND message_id NOT IN (SELECT DISTINCT _id FROM sms)"); Logger::message_end(" (", d_database.changed(), ")"); } Logger::message_start(" Deleting entries from 'reaction' not belonging to remaining mms entries"); d_database.exec("DELETE FROM reaction WHERE message_id NOT IN (SELECT DISTINCT _id FROM " + d_mms_table + ")" + (d_database.tableContainsColumn("reaction", "is_mms") ? " AND is_mms IS 1" : "")); Logger::message_end(" (", d_database.changed(), ")"); } // remove msl_message if (d_database.containsTable("msl_message")) { // note this function is generally called because messages (and/or attachments) have been deleted // the msl_payload table has triggers that delete its entries: // (delete from msl_payload where _id in (select payload_id from message where message_id = (message.deleted_id/part.deletedmid))) // apparently these triggers even when editing within this program, even though the 'ON DELETE CASCADE' stuff does not and // foreign key constraints are not enforced... This causes a sort of circular thing here but I think we can just clean up the // msl_message table according to still-existing msl_payloads first d_database.exec("DELETE FROM msl_message WHERE payload_id NOT IN (SELECT DISTINCT _id FROM msl_payload)"); Logger::message(" Deleting entries from 'msl_message' not belonging to remaining messages"); if (d_database.containsTable("sms")) { d_database.exec("DELETE FROM msl_message WHERE is_mms IS NOT 1 AND message_id NOT IN (SELECT DISTINCT _id FROM sms)"); d_database.exec("DELETE FROM msl_message WHERE is_mms IS 1 AND message_id NOT IN (SELECT DISTINCT _id FROM " + d_mms_table + ")"); } else d_database.exec("DELETE FROM msl_message WHERE message_id NOT IN (SELECT DISTINCT _id FROM " + d_mms_table + ")"); // now delete msl_payloads from non-existing msl_messages ? d_database.exec("DELETE FROM msl_payload WHERE _id NOT IN (SELECT DISTINCT payload_id FROM msl_message)"); // now delete msl_recipients from non existing payloads? d_database.exec("DELETE FROM msl_recipient WHERE payload_id NOT IN (SELECT DISTINCT _id FROM msl_payload)"); } if (d_database.containsTable("story_sends")) { Logger::message_start(" Deleting entries from 'story_sends' not belonging to remaining message entries"); d_database.exec("DELETE FROM story_sends WHERE message_id NOT IN (SELECT DISTINCT _id FROM " + d_mms_table + ")"); Logger::message_end(" (", d_database.changed(), ")"); } } signalbackup-tools-20250313-1/signalbackup/reordermmssmsids.cc000066400000000000000000000256251476450434500243140ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" //#include bool SignalBackup::reorderMmsSmsIds() const { //auto t1 = std::chrono::high_resolution_clock::now(); Logger::message_start(__FUNCTION__); // get all mms in the correct order SqliteDB::QueryResults res; if (!d_database.exec("SELECT _id FROM " + d_mms_table + " ORDER BY date_received ASC", &res)) // for sms table, use 'date' return false; d_database.exec("BEGIN TRANSACTION"); // set all id's 'negatively ascending' (negative because of UNIQUE constraint) long long int negative_id_tmp = 0; long long int total = res.rows(); bool adjustmention = d_database.containsTable("mention"); bool adjustmsl_message = d_database.containsTable("msl_message"); bool msl_has_is_mms = adjustmsl_message && d_database.tableContainsColumn("msl_message", "is_mms"); bool adjustreaction = d_database.containsTable("reaction"); bool reaction_has_is_mms = adjustreaction && d_database.tableContainsColumn("reaction", "is_mms"); bool adjuststorysends = d_database.containsTable("story_sends"); bool adjustcall = d_database.containsTable("call"); bool adjustoriginal_message_id = d_database.tableContainsColumn(d_mms_table, "original_message_id"); bool adjustlatest_revision_id = d_database.tableContainsColumn(d_mms_table, "latest_revision_id"); // we purposefully do this in a dozen or so separate for-loops so SqliteDB can re-use its prepared statements! for (unsigned int i = 0; i < total; ++i) { std::any oldid = res.value(i, 0); ++negative_id_tmp; if (!d_database.exec("UPDATE " + d_mms_table + " SET _id = ? WHERE _id = ?", {-1 * negative_id_tmp, oldid})) [[unlikely]] return false; } negative_id_tmp = 0; Logger::message_continue("."); for (unsigned int i = 0; i < total; ++i) { std::any oldid = res.value(i, 0); ++negative_id_tmp; if (!d_database.exec("UPDATE " + d_part_table + " SET " + d_part_mid + " = ? WHERE " + d_part_mid + " = ?", {-1 * negative_id_tmp, oldid})) [[unlikely]] return false; } negative_id_tmp = 0; for (unsigned int i = 0; i < total; ++i) { std::any oldid = res.value(i, 0); ++negative_id_tmp; if (!d_database.exec("UPDATE group_receipts SET mms_id = ? WHERE mms_id = ?", {-1 * negative_id_tmp, oldid})) [[unlikely]] return false; } negative_id_tmp = 0; if (adjustmention) for (unsigned int i = 0; i < total; ++i) { std::any oldid = res.value(i, 0); ++negative_id_tmp; if (!d_database.exec("UPDATE mention SET message_id = ? WHERE message_id = ?", {-1 * negative_id_tmp, oldid})) [[unlikely]] return false; } negative_id_tmp = 0; if (adjustmsl_message) for (unsigned int i = 0; i < total; ++i) { std::any oldid = res.value(i, 0); ++negative_id_tmp; if (!d_database.exec("UPDATE msl_message SET message_id = ? WHERE message_id = ?"s + (msl_has_is_mms ? " AND is_mms IS 1" : ""), {-1 * negative_id_tmp, oldid})) [[unlikely]] return false; } negative_id_tmp = 0; if (adjustreaction) for (unsigned int i = 0; i < total; ++i) { std::any oldid = res.value(i, 0); ++negative_id_tmp; if (!d_database.exec("UPDATE reaction SET message_id = ? WHERE message_id = ?"s + (reaction_has_is_mms ? " AND is_mms IS 1" : ""), {-1 * negative_id_tmp, oldid})) [[unlikely]] return false; } negative_id_tmp = 0; if (adjuststorysends) for (unsigned int i = 0; i < total; ++i) { std::any oldid = res.value(i, 0); ++negative_id_tmp; if (!d_database.exec("UPDATE story_sends SET message_id = ? WHERE message_id = ?", {-1 * negative_id_tmp, oldid})) [[unlikely]] return false; } negative_id_tmp = 0; if (adjustcall) for (unsigned int i = 0; i < total; ++i) { std::any oldid = res.value(i, 0); ++negative_id_tmp; if (!d_database.exec("UPDATE call SET message_id = ? WHERE message_id = ?", {-1 * negative_id_tmp, oldid})) [[unlikely]] return false; } negative_id_tmp = 0; if (adjustoriginal_message_id) for (unsigned int i = 0; i < total; ++i) { std::any oldid = res.value(i, 0); ++negative_id_tmp; if (!d_database.exec("UPDATE " + d_mms_table + " SET original_message_id = ? WHERE original_message_id = ?", {-1 * negative_id_tmp, oldid})) [[unlikely]] return false; } negative_id_tmp = 0; Logger::message_continue("."); if (adjustlatest_revision_id) for (unsigned int i = 0; i < total; ++i) { std::any oldid = res.value(i, 0); ++negative_id_tmp; if (!d_database.exec("UPDATE " + d_mms_table + " SET latest_revision_id = ? WHERE latest_revision_id = ?", {-1 * negative_id_tmp, oldid})) [[unlikely]] return false; } /* for (unsigned int i = 0; i < total; ++i) { if (d_showprogress && i % 1000 == 0) Logger::message_overwrite(__FUNCTION__, " (", i, "/", total, ")"); std::any oldid = res.value(i, 0); ++negative_id_tmp; if (!d_database.exec("UPDATE " + d_mms_table + " SET _id = ? WHERE _id = ?", {-1 * negative_id_tmp, oldid}) || !d_database.exec("UPDATE " + d_part_table + " SET " + d_part_mid + " = ? WHERE " + d_part_mid + " = ?", {-1 * negative_id_tmp, oldid}) || !d_database.exec("UPDATE group_receipts SET mms_id = ? WHERE mms_id = ?", {-1 * negative_id_tmp, oldid})) return false; if (adjustmention && !d_database.exec("UPDATE mention SET message_id = ? WHERE message_id = ?", {-1 * negative_id_tmp, oldid})) return false; if (adjustmsl_message && !d_database.exec("UPDATE msl_message SET message_id = ? WHERE message_id = ?"s + (msl_has_is_mms ? " AND is_mms IS 1" : ""), {-1 * negative_id_tmp, oldid})) return false; if (adjustreaction && !d_database.exec("UPDATE reaction SET message_id = ? WHERE message_id = ?"s + (reaction_has_is_mms ? " AND is_mms IS 1" : ""), {-1 * negative_id_tmp, oldid})) return false; if (adjuststorysends && !d_database.exec("UPDATE story_sends SET message_id = ? WHERE message_id = ?", {-1 * negative_id_tmp, oldid})) return false; if (adjustcall && !d_database.exec("UPDATE call SET message_id = ? WHERE message_id = ?", {-1 * negative_id_tmp, oldid})) return false; if (adjustoriginal_message_id && !d_database.exec("UPDATE " + d_mms_table + " SET original_message_id = ? WHERE original_message_id = ?", {-1 * negative_id_tmp, oldid})) return false; if (adjustlatest_revision_id && !d_database.exec("UPDATE " + d_mms_table + " SET latest_revision_id = ? WHERE latest_revision_id = ?", {-1 * negative_id_tmp, oldid})) return false; } */ d_database.exec("COMMIT"); // now make all id's positive again d_database.exec("BEGIN TRANSACTION"); Logger::message_continue("."); if (!d_database.exec("UPDATE " + d_mms_table + " SET _id = _id * -1 WHERE _id < 0")) [[unlikely]] return false; if (!d_database.exec("UPDATE " + d_part_table + " SET " + d_part_mid + " = " + d_part_mid + " * -1 WHERE " + d_part_mid + " < 0")) [[unlikely]] return false; if(!d_database.exec("UPDATE group_receipts SET mms_id = mms_id * -1 WHERE mms_id < 0")) [[unlikely]] return false; if (adjustmention && !d_database.exec("UPDATE mention SET message_id = message_id * -1 WHERE message_id < 0")) [[unlikely]] return false; if (adjustmsl_message && !d_database.exec("UPDATE msl_message SET message_id = message_id * -1 WHERE message_id < 0"s + (msl_has_is_mms ? " AND is_mms IS 1" : ""))) [[unlikely]] return false; if (adjustreaction && !d_database.exec("UPDATE reaction SET message_id = message_id * -1 WHERE message_id < 0"s + (reaction_has_is_mms ? " AND is_mms IS 1" : ""))) [[unlikely]] return false; if (adjuststorysends && !d_database.exec("UPDATE story_sends SET message_id = message_id * -1 WHERE message_id < 0")) [[unlikely]] return false; if (adjustcall && !d_database.exec("UPDATE call SET message_id = message_id * -1 WHERE message_id < 0")) [[unlikely]] return false; if (adjustoriginal_message_id && !d_database.exec("UPDATE " + d_mms_table + " SET original_message_id = original_message_id * -1 WHERE original_message_id < 0")) [[unlikely]] return false; if (adjustlatest_revision_id && !d_database.exec("UPDATE " + d_mms_table + " SET latest_revision_id = latest_revision_id * -1 WHERE latest_revision_id < 0")) [[unlikely]] return false; d_database.exec("COMMIT"); // SAME FOR SMS if (d_database.containsTable("sms")) // removed in 168 { if (!d_database.exec("SELECT _id FROM sms ORDER BY " + d_sms_date_received + " ASC", &res)) return false; negative_id_tmp = 0; for (unsigned int i = 0; i < res.rows(); ++i) { long long int oldid = res.getValueAs(i, 0); ++negative_id_tmp; if (!d_database.exec("UPDATE sms SET _id = ? WHERE _id = ?", {-1 * negative_id_tmp, oldid})) return false; if (d_database.containsTable("msl_message")) if (!d_database.exec("UPDATE msl_message SET message_id = ? WHERE message_id = ?"s + (d_database.tableContainsColumn("msl_message", "is_mms") ? " AND is_mms IS NOT 1" : ""), {-1 * negative_id_tmp, oldid})) return false; if (d_database.containsTable("reaction")) // dbv >= 121 if (!d_database.exec("UPDATE reaction SET message_id = ? WHERE message_id = ?"s + (d_database.tableContainsColumn("reaction", "is_mms") ? " AND is_mms IS NOT 1" : ""), {-1 * negative_id_tmp, oldid})) return false; } if (!d_database.exec("UPDATE sms SET _id = _id * -1 WHERE _id < 0")) return false; if (d_database.containsTable("msl_message")) if (!d_database.exec("UPDATE msl_message SET message_id = message_id * -1 WHERE message_id < 0"s + (d_database.tableContainsColumn("msl_message", "is_mms") ? " AND is_mms IS NOT 1" : ""))) return false; if (d_database.containsTable("reaction")) // dbv >= 121 if (!d_database.exec("UPDATE reaction SET message_id = message_id * -1 WHERE message_id < 0"s + (d_database.tableContainsColumn("reaction", "is_mms") ? " AND is_mms IS NOT 1" : ""))) return false; } Logger::message_end("ok"); // auto t2 = std::chrono::high_resolution_clock::now(); // auto ms_int = std::chrono::duration_cast(t2 - t1); // std::cout << " *** TIME: " << ms_int.count() << "ms\n"; return true; } signalbackup-tools-20250313-1/signalbackup/sanitizefilename.cc000066400000000000000000000052231476450434500242310ustar00rootroot00000000000000/* Copyright (C) 2021-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" std::string SignalBackup::sanitizeFilename(std::string const &filename) const { std::string result; #if !defined(_WIN32) && !defined(__MINGW64__) // filter disallowed characters. (Note this is not an exact science) for (char c : filename) result += ((c == '/' || c == '\0' || c == '\n') ? '_' : c); // newline is technically allowed I think #else // WINDOWS, NOT TESTED auto icasecmp = [](char a, char b) { return ((a == b) || (tolower(static_cast(a)) == tolower(static_cast(b)))); }; std::vector const reserved = { "CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", /* these are ntfs things, best just not allow them.... */ "$Mft", "$MftMirr", "$LogFile", "$Volume", "$AttrDef", "$Bitmap", "$Boot", "$BadClus", "$Secure", "$Upcase", "$Extend", "$Quota", "$ObjId", "$Reparse", }; // filter reserved filenames for (auto const &r : reserved) if (filename.size() == r.size() && std::equal(filename.begin(), filename.end(), r.begin(), r.end(), icasecmp)) return "_" + filename; // filter disallowed characters. (Note this is not an exact science) for (char c : filename) result += ((c == '/' || c == '\\' || c == '?' || c == '*' || c == ':' || c == '|' || c == '"' || c == '<' || c == '>' || c <= 0x1f || c == 0x7f) ? '_' : c); // trailing whitespace or periods are (possibly) technically allowed // by the filesystem, but not supported by windows shell and UI while (result.back() == ' ' || result.back() == '.') result.pop_back(); #endif return result; } signalbackup-tools-20250313-1/signalbackup/scanmissingattachments.cc000066400000000000000000000154561476450434500254650ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::scanMissingAttachments() const { // get 'missing' attachments SqliteDB::QueryResults res; d_database.exec("SELECT _id," + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id"s : "-1 AS unique_id"s) + " FROM " + d_part_table, &res); std::vector> missing; for (unsigned int i = 0; i < res.rows(); ++i) if (/*true || */d_attachments.find({res.getValueAs(i, "_id"), res.getValueAs(i, "unique_id")}) == d_attachments.end()) missing.emplace_back(std::make_pair(res.getValueAs(i, "_id"), res.getValueAs(i, "unique_id"))); Logger::message("Got ", missing.size(), " attachments with data not found"); for (unsigned int i = 0; i < missing.size(); ++i) { Logger::message_start("Checking ", (i + 1), " of ", missing.size(), ": ", missing[i].first, ",", missing[i].second, "... "); SqliteDB::QueryResults isquote; d_database.exec("SELECT " + d_part_mid + " FROM " + d_part_table + " WHERE _id = ?" + (d_database.tableContainsColumn(d_part_table, "unique_id") ? " AND unique_id = " + bepaald::toString(missing[i].second) : "") + " AND quote = 1", missing[i].first, &isquote); if (isquote.rows()) { long long int mid = isquote.getValueAs(0, d_part_mid); d_database.exec("SELECT _id FROM " + d_mms_table + " WHERE quote_missing = 1 AND _id = ?", mid, &res); if (res.rows() == 1) { if (d_attachments.find({missing[i].first, missing[i].second}) == d_attachments.end()) Logger::message_end("OK, EXPECTED (quote missing)"); else Logger::message_end("FALSE HIT! (quote missing)"); continue; } // quote_missing is not always (often not?) set to 1 even if quote is missing, so manually check if (d_database.tableContainsColumn(d_mms_table, "remote_deleted")) { d_database.exec("SELECT _id FROM " + d_mms_table + " WHERE remote_deleted IS 1 AND " + d_mms_date_sent + " IS (SELECT quote_id FROM " + d_mms_table + " WHERE _id = ?)", mid, &res); if (res.rows()) // can be more than 1 row if messages were doubled (before date_sent (=quote_id) had UNIQUE constraint) { if (d_attachments.find({missing[i].first, missing[i].second}) == d_attachments.end()) Logger::message_end("OK, EXPECTED (original message missing (remote deleted))"); else Logger::message_end("FALSE HIT! (remote delete)"); continue; } } if (d_database.getSingleResultAs("SELECT IFNULL(quote_id, 0)_id FROM " + d_mms_table + " WHERE _id = ?", mid, 0) != 0) { d_database.exec("SELECT _id FROM " + d_mms_table + " WHERE " + d_mms_date_sent + " IS (SELECT quote_id FROM " + d_mms_table + " WHERE _id = ?)" " AND " "thread_id IS (SELECT thread_id FROM " + d_mms_table + " WHERE _id = ?)", {mid, mid}, &res); if (res.rows() == 0) { if (d_attachments.find({missing[i].first, missing[i].second}) == d_attachments.end()) Logger::message_end("OK, EXPECTED (original message missing (deleted))"); else Logger::message_end("FALSE HIT! (delete)"); continue; } } } d_database.exec("SELECT " + d_part_ct + " FROM " + d_part_table + " WHERE " "quote = 1 " "AND _id = ?" + (d_database.tableContainsColumn(d_part_table, "unique_id") ? " AND unique_id = " + bepaald::toString(missing[i].second) : ""s) + " AND " + d_part_ct + " NOT LIKE 'image%' AND " + d_part_ct + " NOT LIKE 'video%'", missing[i].first, &res); if (res.rows() == 1) { if (d_attachments.find({missing[i].first, missing[i].second}) == d_attachments.end()) Logger::message_end("OK, EXPECTED (type = ",res.valueAsString(0, 0), ")"); else Logger::message_end("FALSE HIT! (type)"); continue; } d_database.exec("SELECT " + d_part_pending + " FROM " + d_part_table + " WHERE " + d_part_pending + " IS NOT 0 AND _id = ?" + (d_database.tableContainsColumn(d_part_table, "unique_id") ? " AND unique_id = " + bepaald::toString(missing[i].second) : ""s), missing[i].first, &res); if (res.rows() == 1) { if (d_attachments.find({missing[i].first, missing[i].second}) == d_attachments.end()) Logger::message_end("OK, EXPECTED (pending_push = ", res.valueAsString(0, 0), ")"); else Logger::message_end("FALSE HIT! (pending_push)"); continue; } d_database.exec("SELECT " + d_part_ct + " FROM " + d_part_table + " WHERE _id = ?" + (d_database.tableContainsColumn(d_part_table, "unique_id") ? " AND unique_id = " + bepaald::toString(missing[i].second) : ""s), missing[i].first, &res); if (res.rows() == 1 && res(0, d_part_ct) == "application/x-signal-view-once") { if (d_attachments.find({missing[i].first, missing[i].second}) == d_attachments.end()) Logger::message_end("OK, EXPECTED (content_type = application/x-signal-view-once)"); else Logger::message_end("FALSE HIT! (view_once)"); continue; } if (d_attachments.find({missing[i].first, missing[i].second}) != d_attachments.end()) { Logger::message_end("OK, EXPECTED (no special circumstances, but not missing)"); continue; } Logger::message(Logger::Control::BOLD, "UNEXPECTED!", Logger::Control::NORMAL, " details:"); d_database.exec("SELECT quote," + d_part_ct + "," + d_part_pending + " FROM " + d_part_table + " WHERE _id = ?" + (d_database.tableContainsColumn(d_part_table, "unique_id") ? " AND unique_id = " + bepaald::toString(missing[i].second) : ""s), missing[i].first, &res); res.prettyPrint(d_truncate); } } signalbackup-tools-20250313-1/signalbackup/scanself.cc000066400000000000000000000355471476450434500225140ustar00rootroot00000000000000/* Copyright (C) 2021-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" long long int SignalBackup::scanSelf() const { if (!d_database.containsTable("recipient")) return false; ///// FIRST TRY BY GETTING KEY 'account.pni_identity_public_key' FROM KeyValues, and matching it to uuid from identites-table std::string identity_public_key; for (auto const &kv : d_keyvalueframes) if (kv->key() == "account.pni_identity_public_key" && !kv->value().empty()) { identity_public_key = kv->value(); break; } if (!identity_public_key.empty()) { long long int selfid = d_database.getSingleResultAs("SELECT _id FROM recipient WHERE " + d_recipient_aci + " IN " "(SELECT address FROM identities WHERE identity_key IS ?)", identity_public_key, -1); if (selfid != -1) return selfid; } ///// NEXT TRY BY GETTING KEY 'account.aci_identity_public_key' FROM KeyValues, and matching it to uuid from identites-table identity_public_key.clear(); for (auto const &kv : d_keyvalueframes) if (kv->key() == "account.aci_identity_public_key" && !kv->value().empty()) { identity_public_key = kv->value(); break; } if (!identity_public_key.empty()) { long long int selfid = d_database.getSingleResultAs("SELECT _id FROM recipient WHERE " + d_recipient_aci + " IN " "(SELECT address FROM identities WHERE identity_key IS ?)", identity_public_key, -1); if (selfid != -1) return selfid; } // only 'works' on 'newer' versions if (!d_database.tableContainsColumn("thread", d_thread_recipient_id) || !d_database.tableContainsColumn(d_mms_table, "quote_author") || (!d_database.tableContainsColumn(d_mms_table, "reactions") && !d_database.containsTable("reaction"))) return -1; // in newer databases (>= dbv185), message.from_recipient_id should always be set to self on outgoing messages. long long int selfid = d_database.getSingleResultAs("SELECT DISTINCT " + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE (" + d_mms_type + " & 0x1f) IN (?, ?, ?, ?, ?, ?, ?, ?)", {Types::BASE_OUTBOX_TYPE, Types::BASE_SENT_TYPE, Types::BASE_SENDING_TYPE, Types::BASE_SENT_FAILED_TYPE, Types::BASE_PENDING_SECURE_SMS_FALLBACK,Types:: BASE_PENDING_INSECURE_SMS_FALLBACK , Types::OUTGOING_CALL_TYPE, Types::OUTGOING_VIDEO_CALL_TYPE}, -1); if (selfid != -1) return selfid; // get thread ids of all 1-on-1 conversations SqliteDB::QueryResults res; if (!d_database.exec("SELECT _id, " + d_thread_recipient_id + " FROM thread WHERE " + d_thread_recipient_id + " IN (SELECT _id FROM recipient WHERE group_id IS NULL)", &res)) return -1; std::set options; for (unsigned int i = 0; i < res.rows(); ++i) { // try to find another recipient in this one-on-one thread long long int tid = res.getValueAs(i, "_id"); long long int rid = bepaald::toNumber(res.valueAsString(i, d_thread_recipient_id)); SqliteDB::QueryResults res2; //std::cout << "Dealing with thread: " << tid << " (recipient: " << rid << ")" << std::endl; // in earlier versions, it was possible to quote someone cross-thread. So we need to limit // this query to quotes with quote_id referencing a quote in the same thread. if (!d_database.exec("SELECT DISTINCT quote_author FROM " + d_mms_table + " " "WHERE thread_id IS ? AND quote_id IS NOT 0 AND quote_id IS NOT NULL " "AND quote_author IS NOT NULL AND quote_author IS NOT ? " "AND (quote_id IN (SELECT " + d_mms_date_sent + " FROM " + d_mms_table + " WHERE thread_id = ?)" + (d_database.containsTable("sms") ? " OR quote_id IN (SELECT date_sent FROM sms WHERE thread_id = " + bepaald::toString(tid) + "))" : ")"), {tid, rid, tid}, &res2)) continue; for (unsigned int j = 0; j < res2.rows(); ++j) { //std::cout << " From quote:" << res2.valueAsString(j, "quote_author") << std::endl; options.insert(bepaald::toNumber(res2.valueAsString(j, "quote_author"))); } for (auto const &t : {"sms"s, d_mms_table}) // OLD STYLE REACTIONS { if (d_database.tableContainsColumn(t, "reactions")) { if (!d_database.exec("SELECT reactions FROM " + t + " WHERE thread_id IS ? AND reactions IS NOT NULL", tid, &res2)) continue; for (unsigned int j = 0; j < res2.rows(); ++j) { ReactionList reactions(res2.getValueAs, size_t>>(j, "reactions")); for (unsigned int k = 0; k < reactions.numReactions(); ++k) { if (reactions.getAuthor(k) != static_cast(rid)) { //std::cout << " From " + t + ".reaction (old): " << reactions.getAuthor(k) << std::endl; options.insert(reactions.getAuthor(k)); } } } } } if (d_database.containsTable("reaction")) // NEW STYLE REACTIONS { if (d_database.containsTable("sms")) { if (!d_database.exec("SELECT DISTINCT author_id FROM reaction WHERE is_mms IS 0 AND "s + "author_id IS NOT ? AND message_id IN (SELECT DISTINCT _id FROM sms WHERE thread_id = ?)", {rid, tid}, &res2)) continue; for (unsigned int j = 0; j < res2.rows(); ++j) { //std::cout << " From reaction (new):" << res2.valueAsString(j, "author_id") << std::endl; options.insert(bepaald::toNumber(res2.valueAsString(j, "author_id"))); } } if (!d_database.exec("SELECT DISTINCT author_id FROM reaction WHERE "s + (d_database.tableContainsColumn("reaction", "is_mms") ? "is_mms IS 1 AND " : "") + "author_id IS NOT ? AND message_id IN (SELECT DISTINCT _id FROM " + d_mms_table + " WHERE thread_id = ?)", {rid, tid} , &res2)) continue; for (unsigned int j = 0; j < res2.rows(); ++j) { //std::cout << " From reaction (new):" << res2.valueAsString(j, "author_id") << std::endl; options.insert(bepaald::toNumber(res2.valueAsString(j, "author_id"))); } } } // get thread ids of all group conversations if (!d_database.exec("SELECT _id FROM groups WHERE active IS NOT 0", &res)) return -1; //res.prettyPrint(); // for each group-thread for (unsigned int i = 0; i < res.rows(); ++i) { long long int gid = res.getValueAs(i, "_id"); SqliteDB::QueryResults res2; // skip groups without thread if (!d_database.exec("SELECT _id from thread WHERE " + d_thread_recipient_id + " IS (SELECT _id FROM recipient WHERE group_id IS (SELECT group_id from groups WHERE _id IS ?))", gid, &res2)) continue; if (res2.rows() == 0) { //std::cout << "Skipping group: " << gid << " (no thread)" << std::endl; continue; } long long int tid = res2.getValueAs(0, "_id"); //std::cout << "Dealing with group: " << gid << " (thread: " << tid << ")" << std::endl; SqliteDB::QueryResults res3; if (d_database.tableContainsColumn("groups", "members")) // old style { // this prints all group members that never appear as recipient in a message (in groups, the recipient ('address') is always the sender, except for self, who has the groups id as address) if (d_database.containsTable("sms")) { if (!d_database.exec("WITH split(word, str) AS (SELECT '',members||',' FROM groups WHERE _id IS ?1 UNION ALL SELECT substr(str, 0, instr(str, ',')), substr(str, instr(str, ',')+1) FROM split WHERE str!='') SELECT word FROM split WHERE word!='' AND word NOT IN (SELECT DISTINCT " + d_mms_recipient_id +" FROM " + d_mms_table + " WHERE thread_id IS (SELECT _id FROM thread WHERE " + d_thread_recipient_id + " IS (SELECT _id FROM recipient WHERE group_id IS (SELECT group_id FROM groups WHERE _id IS ?1)))) AND word NOT IN (SELECT DISTINCT " + d_sms_recipient_id + " FROM sms WHERE thread_id IS (SELECT _id FROM thread WHERE " + d_thread_recipient_id + " IS (SELECT _id FROM recipient WHERE group_id IS (SELECT group_id FROM groups WHERE _id IS ?1))))", gid, &res3)) continue; } else if (!d_database.exec("WITH split(word, str) AS (SELECT '',members||',' FROM groups WHERE _id IS ?1 UNION ALL SELECT substr(str, 0, instr(str, ',')), substr(str, instr(str, ',')+1) FROM split WHERE str!='') SELECT word FROM split WHERE word!='' AND word NOT IN (SELECT DISTINCT " + d_mms_recipient_id +" FROM " + d_mms_table + " WHERE thread_id IS (SELECT _id FROM thread WHERE " + d_thread_recipient_id + " IS (SELECT _id FROM recipient WHERE group_id IS (SELECT group_id FROM groups WHERE _id IS ?1))))", gid, &res3)) continue; for (unsigned int j = 0; j < res3.rows(); ++j) { //std::cout << " From group membership:" << res3.valueAsString(j, "word") << std::endl; options.insert(bepaald::toNumber(res3.valueAsString(j, "word"))); } } else if (d_database.containsTable("group_membership")) // modern style { if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) // < dbv185 { // this prints all group members that never appear as recipient in a message (in groups, the recipient ('address') is always the sender, except for self, who has the groups id as address) if (!d_database.exec("SELECT DISTINCT recipient_id FROM group_membership WHERE group_id IN (SELECT group_id FROM groups WHERE _id = ?) AND " "recipient_id NOT IN (SELECT DISTINCT " + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE thread_id IS ? AND type IS NOT ?)", {gid, tid, Types::GROUP_CALL_TYPE}, &res3)) continue; else { for (unsigned int j = 0; j < res3.rows(); ++j) { //std::cout << " From group membership (<185):" << res3.valueAsString(j, "recipient_id") << std::endl; options.insert(bepaald::toNumber(res3.valueAsString(j, "recipient_id"))); } } } else { // in the newer style, ([from/to]_recipient_id), self CAN appear as recipient (to_ when incoming, from_ when outgoing). // for incoming messages 'self' will never appear in from_, but could (for msgs arriving since 185) appear in to_. // for outgoing messages 'self' will never appear in to_ (= always rec._id of group), but always (if migration succesfull) in from_. // // outgoing // if (!d_database.exec("SELECT DISTINCT recipient_id FROM group_membership WHERE group_id IN (SELECT group_id FROM groups WHERE _id = ?) AND " // "recipient_id NOT IN (" // "SELECT DISTINCT to_recipient_id FROM " + d_mms_table + " WHERE thread_id IS ? AND type IN (?, ?, ?, ?, ?, ?, ?, ?)" // ")", // {gid, tid, // Types::BASE_OUTBOX_TYPE, Types::BASE_SENT_TYPE, Types::BASE_SENDING_TYPE, Types::BASE_SENT_FAILED_TYPE, // Types::BASE_PENDING_SECURE_SMS_FALLBACK,Types:: BASE_PENDING_INSECURE_SMS_FALLBACK , Types::OUTGOING_CALL_TYPE, Types::OUTGOING_VIDEO_CALL_TYPE}, &res3)) // for (unsigned int j = 0; j < res3.rows(); ++j) // { // std::cout << " From group membership (NEW):" << res3(j, "recipeint_id") << std::endl; // options.insert(bepaald::toNumber(res3(j, "recipient_id"))); // } // incoming if (!d_database.exec("SELECT DISTINCT recipient_id FROM group_membership WHERE group_id IN (SELECT group_id FROM groups WHERE _id = ?) AND " "recipient_id NOT IN (" "SELECT DISTINCT from_recipient_id FROM " + d_mms_table + " WHERE thread_id IS ? AND type IS NOT ? AND type NOT IN (?, ?, ?, ?, ?, ?, ?, ?)" ")", {gid, tid, Types::GROUP_CALL_TYPE, Types::BASE_OUTBOX_TYPE, Types::BASE_SENT_TYPE, Types::BASE_SENDING_TYPE, Types::BASE_SENT_FAILED_TYPE, Types::BASE_PENDING_SECURE_SMS_FALLBACK,Types:: BASE_PENDING_INSECURE_SMS_FALLBACK , Types::OUTGOING_CALL_TYPE, Types::OUTGOING_VIDEO_CALL_TYPE}, &res3)) continue; else { for (unsigned int j = 0; j < res3.rows(); ++j) { //std::cout << " From group membership (NEW):" << res3(j, "recipient_id") << std::endl; options.insert(res3.valueAsInt(j, "recipient_id")); } } } } } // std::cout << "OPTIONS:" << std::endl; // for (auto const &o: options) // std::cout << "Option: " << o << std::endl; if (options.size() == 1) return *options.begin(); return -1; } /* Another possible option, maybe also for older (but not oldest) databases: SELECT address FROM identites WHERE first_use = 1 AND verified = 1 AND nonblocking_approval = 1; This seems to return 1 or two 'address'es, which seem to all point to the same recipient (='self'); dbv10 : not working dbv23 : address == e164 of self dbv27 - dbv99 : address == recipient._id of self dbv123 - dbv198 : address == recipient.uuid (/aci) of self dbv201 - dbv231 : address (result 1) == recipient.uuid (/aci) of self address (result 2) == recipient.pni of self In my databases it only has 1 questionable result for DEVDBV23 where it returns 2 distinct e164s, One is self, the other is unknown (does not appear in recipient_preferences or elsewhere). Possible number change, or backup restore after new SIM card placed? */ signalbackup-tools-20250313-1/signalbackup/scramble.cc000066400000000000000000000052151476450434500224730ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::scrambleHelper(std::string const &table, std::vector const &columns) const { Logger::message("Scrambling ", table); std::string selectquery = "SELECT _id"; for (unsigned int i = 0; i < columns.size(); ++i) selectquery += "," + columns[i]; selectquery += " FROM " + table; SqliteDB::QueryResults res; d_database.exec(selectquery, &res); for (unsigned int i = 0; i < res.rows(); ++i) { std::vector str; for (unsigned int j = 0; j < columns.size(); ++j) { str.push_back(res.valueAsString(i, columns[j])); std::replace_if(str.back().begin(), str.back().end(), [](char c) { return c != ' ' && std::islower(c); }, 'x'); std::replace_if(str.back().begin(), str.back().end(), [](char c) { return c != ' ' && !std::islower(c); }, 'X'); } std::string updatequery = "UPDATE " + table + " SET "; for (unsigned int j = 0; j < columns.size(); ++j) updatequery += columns[j] + " = ?" + ((j == columns.size() - 1) ? " WHERE _id = ?" : ", "); std::vector values; for (unsigned int j = 0; j < str.size(); ++j) values.push_back(str[j]); values.push_back(res.getValueAs(i, "_id")); if (!d_database.exec(updatequery, values)) return false; } return true; } bool SignalBackup::scramble() const { if (d_database.containsTable("sms")) if (!scrambleHelper("sms", {"body"})) return false; if (!scrambleHelper(d_mms_table, {"body", "quote_body"})) return false; if (!scrambleHelper("recipient", {d_recipient_system_joined_name, "profile_joined_name", d_recipient_profile_given_name, "profile_family_name", "system_family_name", "system_given_name"})) return false; if (!scrambleHelper("thread", {"snippet"})) return false; if (!scrambleHelper("groups", {"title"})) return false; return true; } signalbackup-tools-20250313-1/signalbackup/setchatcolor.cc000066400000000000000000000070431476450434500233760ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::setChatColors(std::vector> const &colorlist) { if (!d_database.tableContainsColumn("recipient", "chat_colors")) { Logger::error("Recipient table does not appear to contain chat_colors-column"); return false; } /* message chatcolor/wallpaper { message SingleColor { int32 color = 1; } message LinearGradient { float rotation = 1; repeated int32 colors = 2; repeated float positions = 3; } message File { string uri = 1; } oneof wallpaper { SingleColor singleColor = 1; LinearGradient linearGradient = 2; File file = 3; // ONLY IN WALLPAPER } float dimLevelInDarkTheme = 4; // ONLY IN WALLPAPER } */ bool ok = false; // will be true if _any_ color setting succeeds for (auto [rid, colorstr] : colorlist) { // colorstring to number: if ((!(colorstr.size() == 7 && colorstr[0] == '#') && !(colorstr.size() == 9 && colorstr[0] == '#') && colorstr.size() != 6 && colorstr.size() != 8) || colorstr.find_first_not_of("#0123456789ABCDEFabcdef") != std::string::npos) { Logger::warning("Skipping [", rid, " -> ", colorstr, "]. Illegal colorstring format. Use (#)(AA)RRGGBB in hexadecimal notation."); continue; } // chop off leading # if (colorstr[0] == '#') colorstr = colorstr.substr(1); // append opacity if (colorstr.size() == 6) colorstr = "FF" + colorstr; //Logger::message("GOT COLOR STRING: ", colorstr); std::unique_ptr color_data(new unsigned char[4]); bepaald::hexStringToBytes(colorstr, color_data.get(), 4); uint32_t color_value = 0; std::memcpy(&color_value, color_data.get(), 4); color_value = bepaald::swap_endian(color_value); //Logger::message("GOT COLOR NUMBER VALUE: ", color_value); ProtoBufParser single_color; single_color.addField<1>(color_value); ProtoBufParser< ProtoBufParser, ProtoBufParser, ProtoBufParser, protobuffer::optional::FLOAT> color_proto; color_proto.addField<1>(single_color); //Logger::message(color_proto.getDataString()); //color_proto.print(); std::pair color_bindata(color_proto.data(), color_proto.size()); if (!d_database.exec("UPDATE recipient SET chat_colors = ? WHERE _id = ?", {color_bindata, rid}) || d_database.changed() != 1) Logger::warning("Failed to set custom chat color for recipient ", rid); else ok |= true; } return ok; } signalbackup-tools-20250313-1/signalbackup/setcolumnnames.cc000066400000000000000000000222751476450434500237450ustar00rootroot00000000000000/* Copyright (C) 2021-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::setColumnNames() { // started at dbv 215 d_part_table = "attachment"; if (d_database.containsTable("part") && !d_database.containsTable("attachment")) d_part_table = "part"; // started at dbv 174 d_mms_table = "message"; if (d_database.containsTable("mms") && !d_database.containsTable("message")) d_mms_table = "mms"; d_recipient_aci = "aci"; if (d_database.tableContainsColumn("recipient", "uuid")) // before dbv200 d_recipient_aci = "uuid"; d_recipient_e164 = "e164"; if (d_database.tableContainsColumn("recipient", "phone")) // before dbv201 d_recipient_e164 = "phone"; d_recipient_avatar_color = "avatar_color"; if (d_database.tableContainsColumn("recipient", "color")) // before dbv201 d_recipient_avatar_color = "color"; d_recipient_system_joined_name = "system_joined_name"; if (d_database.tableContainsColumn("recipient", "system_display_name")) // before dbv201 d_recipient_system_joined_name = "system_display_name"; d_recipient_profile_given_name = "profile_given_name"; if (d_database.tableContainsColumn("recipient", "signal_profile_name")) // before dbv201 d_recipient_profile_given_name = "signal_profile_name"; d_recipient_storage_service = "storage_service_id"; if (!d_database.tableContainsColumn("recipient", "storage_service_id") && d_database.tableContainsColumn("recipient", "storage_service_key")) d_recipient_storage_service = "storage_service_key"; d_recipient_type = "type"; if (!d_database.tableContainsColumn("recipient", "type") && // before dbv201 d_database.tableContainsColumn("recipient", "group_type")) d_recipient_type = "group_type"; d_recipient_profile_avatar = "profile_avatar"; if (!d_database.tableContainsColumn("recipient", "profile_avatar") && // before dbv201 d_database.tableContainsColumn("recipient", "signal_profile_avatar")) d_recipient_profile_avatar = "signal_profile_avatar"; d_recipient_sealed_sender = "sealed_sender_mode"; if (!d_database.tableContainsColumn("recipient", "sealed_sender_mode") && // before dbv201 d_database.tableContainsColumn("recipient", "unidentified_access_mode")) d_recipient_sealed_sender = "unidentified_access_mode"; // started at dbv166 d_thread_recipient_id = "recipient_id"; // from dbv 108 if (!d_database.tableContainsColumn("thread", "recipient_id") && d_database.tableContainsColumn("thread", "thread_recipient_id")) d_thread_recipient_id = "thread_recipient_id"; //earliest if (!d_database.tableContainsColumn("thread", "recipient_id") && !d_database.tableContainsColumn("thread", "thread_recipient_id") && d_database.tableContainsColumn("thread", "recipient_ids")) // before dbv108 d_thread_recipient_id = "recipient_ids"; // started at dbv166 d_thread_message_count = "meaningful_messages"; // before 166 if (!d_database.tableContainsColumn("thread", "meaningful_messages") && d_database.tableContainsColumn("thread", "message_count")) d_thread_message_count = "message_count"; // from dbv211 d_thread_delivery_receipts = "has_delivery_receipt"; // before 211 if (!d_database.tableContainsColumn("thread", "has_delivery_receipt") && d_database.tableContainsColumn("thread", "delivery_receipt_count")) d_thread_delivery_receipts = "delivery_receipt_count"; // from dbv211 d_thread_read_receipts = "has_read_receipt"; // before 211 if (!d_database.tableContainsColumn("thread", "has_read_receipt") && d_database.tableContainsColumn("thread", "read_receipt_count")) d_thread_read_receipts = "read_receipt_count"; // from dbv266 d_thread_pinned = "pinned_order"; // before 211 if (!d_database.tableContainsColumn("thread", "pinned_order") && d_database.tableContainsColumn("thread", "pinned")) d_thread_pinned = "pinned"; // started at dbv166 d_sms_date_received = "date_received"; // before 166 if (!d_database.tableContainsColumn("sms", "date_received") && d_database.tableContainsColumn("sms", "date")) d_sms_date_received = "date"; // started at dbv166 d_sms_recipient_id = "recipient_id"; // before 166 if (!d_database.tableContainsColumn("sms", "recipient_id") && d_database.tableContainsColumn("sms", "address")) d_sms_recipient_id = "address"; // started at dbv166 d_sms_recipient_device_id = "recipient_device_id"; // before 166 if (!d_database.tableContainsColumn("sms", "recipient_device_id") && d_database.tableContainsColumn("sms", "address_device_id")) d_sms_recipient_device_id = "address_device_id"; // from dbv211 d_mms_delivery_receipts = "has_delivery_receipt"; // before 211 if (!d_database.tableContainsColumn(d_mms_table, "has_delivery_receipt") && d_database.tableContainsColumn(d_mms_table, "delivery_receipt_count")) d_mms_delivery_receipts = "delivery_receipt_count"; // from dbv211 d_mms_read_receipts = "has_read_receipt"; // before 211 if (!d_database.tableContainsColumn(d_mms_table, "has_read_receipt") && d_database.tableContainsColumn(d_mms_table, "read_receipt_count")) d_mms_read_receipts = "read_receipt_count"; // from dbv211 d_mms_viewed_receipts = "viewed"; // before 211 if (!d_database.tableContainsColumn(d_mms_table, "viewed") && d_database.tableContainsColumn(d_mms_table, "viewed_receipt_count")) d_mms_viewed_receipts = "viewed_receipt_count"; // started at dbv166 d_mms_date_sent = "date_sent"; // before 166 if (!d_database.tableContainsColumn(d_mms_table, "date_sent") && d_database.tableContainsColumn(d_mms_table, "date")) d_mms_date_sent = "date"; // started at dbv166 d_mms_ranges = "message_ranges"; // before 166 if (!d_database.tableContainsColumn(d_mms_table, "message_ranges") && d_database.tableContainsColumn(d_mms_table, "ranges")) d_mms_ranges = "ranges"; // started at dbv185 d_mms_recipient_id = "from_recipient_id"; // before 185 if (!d_database.tableContainsColumn(d_mms_table, "from_recipient_id") && d_database.tableContainsColumn(d_mms_table, "recipient_id")) d_mms_recipient_id = "recipient_id"; // before 166 if (!d_database.tableContainsColumn(d_mms_table, "recipient_id") && d_database.tableContainsColumn(d_mms_table, "address")) d_mms_recipient_id = "address"; // started at dbv185 d_mms_recipient_device_id = "from_device_id"; // before 185 if (!d_database.tableContainsColumn(d_mms_table, "from_device_id") && d_database.tableContainsColumn(d_mms_table, "recipient_device_id")) d_mms_recipient_device_id = "recipient_device_id"; // before 166 if (!d_database.tableContainsColumn(d_mms_table, "recipient_device_id") && d_database.tableContainsColumn(d_mms_table, "address_device_id")) d_mms_recipient_device_id = "address_device_id"; // started at dbv166 d_mms_type = "type"; // before 166 if (!d_database.tableContainsColumn(d_mms_table, "type") && d_database.tableContainsColumn(d_mms_table, "msg_box")) d_mms_type = "msg_box"; // started at dbv166 d_mms_previews = "link_previews"; // before 166 if (!d_database.tableContainsColumn(d_mms_table, "link_previews") && d_database.tableContainsColumn(d_mms_table, "previews")) d_mms_previews = "previews"; d_groups_v1_members = "unmigrated_v1_members"; if (!d_database.tableContainsColumn("groups", "unmigrated_v1_members") && d_database.tableContainsColumn("groups", "former_v1_members")) d_groups_v1_members = "former_v1_members"; d_part_mid = "message_id"; // dbv 215 if (!d_database.tableContainsColumn(d_part_table, "message_id") && d_database.tableContainsColumn(d_part_table, "mid")) d_part_mid = "mid"; d_part_ct = "content_type"; // dbv 215 if (!d_database.tableContainsColumn(d_part_table, "content_type") && d_database.tableContainsColumn(d_part_table, "ct")) d_part_ct = "ct"; d_part_pending = "transfer_state"; // dbv 215 if (!d_database.tableContainsColumn(d_part_table, "transfer_state") && d_database.tableContainsColumn(d_part_table, "pending_push")) d_part_pending = "pending_push"; d_part_cd = "remote_key"; // dbv 215 if (!d_database.tableContainsColumn(d_part_table, "remote_key") && d_database.tableContainsColumn(d_part_table, "cd")) d_part_cd = "cd"; d_part_cl = "remote_location"; // dbv 215 if (!d_database.tableContainsColumn(d_part_table, "remote_location") && d_database.tableContainsColumn(d_part_table, "cl")) d_part_cl = "cl"; return true; } signalbackup-tools-20250313-1/signalbackup/setfiletimestamp.cc000066400000000000000000000046221476450434500242630ustar00rootroot00000000000000/* Copyright (C) 2021-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #if !defined(_WIN32) && !defined(__MINGW64__) #include #include bool SignalBackup::setFileTimeStamp(std::string const &file, long long int time_usec) const { struct timespec ntimes[] = { { // ntimes[0] = // last access time time_usec / 1000, // tv_sec, seconds (time_usec % 1000) * 1000 // tv_usec, nanoseconds }, { // ntimes[1] = // last modification time time_usec / 1000, // tv_sec, seconds (time_usec % 1000) * 1000 // tv_usec, nanoseconds } }; return (utimensat(AT_FDCWD, file.c_str(), ntimes, 0) == 0); } #else // this is poorly tested, I don't have windows #include #include bool SignalBackup::setFileTimeStamp(std::string const &file, long long int time_usec) const { // get file handle HANDLE hFile = CreateFile(file.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) return false; // windows epoch starts at 1601-01-01T00:00:00Z, 11644473600 seconds before the unix epoch. // then windows counts in 100 nsec chunks. long long unsigned int wintime = time_usec * 10000 + 116444736000000000; FILE_BASIC_INFO b; b.CreationTime.QuadPart = wintime; b.LastAccessTime.QuadPart = wintime; b.LastWriteTime.QuadPart = wintime; b.ChangeTime.QuadPart = wintime; b.FileAttributes = 0; // leave unchanged if (SetFileInformationByHandle(hFile, FileBasicInfo, &b, sizeof(b)) != 0) { // ignore for now... } return (CloseHandle(hFile)); } #endif signalbackup-tools-20250313-1/signalbackup/setlongmessagebody.cc000066400000000000000000000033711476450434500246020ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../common_be.h" void SignalBackup::setLongMessageBody(std::string *body, SqliteDB::QueryResults *attachment_results) const { for (unsigned int ai = 0; ai < attachment_results->rows(); ++ai) { if (attachment_results->valueAsString(ai, d_part_ct) == "text/x-signal-plain") [[unlikely]] { //std::cout << "Got long message!" << std::endl; SqliteDB::QueryResults longmessage = attachment_results->getRow(ai); attachment_results->removeRow(ai); // get message: long long int rowid = longmessage.valueAsInt(0, "_id"); long long int uniqueid = longmessage.valueAsInt(0, "unique_id"); // get attachment: auto ait = d_attachments.find({rowid, uniqueid}); if (ait == d_attachments.end()) [[unlikely]] continue; AttachmentFrame *a = ait->second.get(); // set body *body = std::string(reinterpret_cast(a->attachmentData()), a->attachmentSize()); a->clearData(); break; // always max 1? } } } signalbackup-tools-20250313-1/signalbackup/setminimumid.cc000066400000000000000000000041211476450434500234020ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::setMinimumId(std::string const &table, long long int offset, std::string const &col) const { Logger::message(__FUNCTION__, " ", table); if (offset == 0) // no changes requested return; // change sign on all values: d_database.exec("UPDATE " + table + " SET " + col + " = " + col + " * -1"); // change sign back && apply offset d_database.exec("UPDATE " + table + " SET " + col + " = " + col + " * -1 + ?", offset); /* // OLD VERSION // move everything to max + offset, the subtract max again. This works, but only if the id's are handled in order. if (offset < 0) { d_database.exec("UPDATE " + table + " SET " + col + " = " + col + " + (SELECT MAX(" + col + ") from " + table + ") - (SELECT MIN(" + col + ") from " + table + ") + ?", 1ll); d_database.exec("UPDATE " + table + " SET " + col + " = " + col + " - (SELECT MAX(" + col + ") from " + table + ") + (SELECT MIN(" + col + ") from " + table + ") + ?", (offset - 1)); } else { d_database.exec("UPDATE " + table + " SET " + col + " = " + col + " + (SELECT MAX(" + col + ") from " + table + ") - (SELECT MIN(" + col + ") from " + table + ") + ?", offset); d_database.exec("UPDATE " + table + " SET " + col + " = " + col + " - (SELECT MAX(" + col + ") from " + table + ") + (SELECT MIN(" + col + ") from " + table + ")"); } */ } signalbackup-tools-20250313-1/signalbackup/setrecipientinfo.cc000066400000000000000000000300471476450434500242560ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::setRecipientInfo(std::set const &recipients, std::map *recipientinfo) const { // get info from all recipients: for (long long int rid : recipients) { if (bepaald::contains(recipientinfo, rid)) // already present continue; // d_database.printLineMode("SELECT " + (d_database.tableContainsColumn("recipient", "nickname_joined_name") ? "NULLIF(recipient.nickname_joined_name, ''),"s : ""s) + // "NULLIF(recipient." + d_recipient_system_joined_name + ", ''), " + // (d_database.tableContainsColumn("recipient", "profile_joined_name") ? "NULLIF(recipient.profile_joined_name, ''),"s : ""s) + // "NULLIF(recipient." + d_recipient_profile_given_name + ", ''), NULLIF(groups.title, ''), " + // "NULLIF(recipient." + d_recipient_e164 + ", ''), NULLIF(recipient." + d_recipient_aci + ", ''), " // " recipient._id, recipient." + d_recipient_e164 + ", recipient.username, recipient." + d_recipient_aci + // " FROM recipient " // "LEFT JOIN groups ON recipient.group_id = groups.group_id " + // "WHERE recipient._id = ?", // rid); // get info SqliteDB::QueryResults results; d_database.exec("SELECT COALESCE(" + (d_database.tableContainsColumn("recipient", "nickname_joined_name") ? "NULLIF(recipient.nickname_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_system_joined_name + ", ''), " + (d_database.tableContainsColumn("recipient", "profile_joined_name") ? "NULLIF(recipient.profile_joined_name, ''),"s : ""s) + "NULLIF(recipient." + d_recipient_profile_given_name + ", ''), NULLIF(groups.title, ''), " + (d_database.containsTable("distribution_list") ? "NULLIF(distribution_list.name, ''), " : "") + "NULLIF(recipient." + d_recipient_e164 + ", ''), NULLIF(recipient." + d_recipient_aci + ", ''), " " recipient._id) AS 'display_name', recipient." + d_recipient_e164 + ", recipient.username, recipient." + d_recipient_aci + ", " + (d_database.tableContainsColumn("recipient", "chat_colors") ? "NULLIF(recipient.chat_colors, '') AS chat_colors,"s : ""s) + //wallpaper_file, custom_chat_colors_id "recipient.group_id, recipient." + d_recipient_avatar_color + ", " + (d_database.tableContainsColumn("recipient", "notification_channel") ? "notification_channel, " : "") + (d_database.tableContainsColumn("recipient", "mute_until") ? "mute_until, " : "") + (d_database.tableContainsColumn("recipient", "blocked") ? "blocked, " : "") + (d_database.tableContainsColumn("recipient", "mention_setting") ? "mention_setting, " : "") + (d_database.tableContainsColumn("recipient", "message_expiration_time") ? "message_expiration_time, " : "") + "identities.verified, " "recipient.wallpaper " "FROM recipient " "LEFT JOIN groups ON recipient.group_id = groups.group_id " + "LEFT JOIN identities ON recipient." + d_recipient_aci + " = identities.address " + (d_database.containsTable("distribution_list") ? "LEFT JOIN distribution_list ON recipient._id = distribution_list.recipient_id " : "") + "WHERE recipient._id = ?", rid, &results); std::string display_name = results.valueAsString(0, "display_name"); if (display_name.empty()) display_name = "?"; std::string initial; bool initial_is_emoji = false; if (bepaald::contains(s_emoji_first_bytes, display_name[0])) { for (char const *const emoji_string : s_emoji_unicode_list) { unsigned int emoji_size = std::strlen(emoji_string); if ((display_name.size() >= emoji_size) && std::strncmp(display_name.data(), emoji_string, emoji_size) == 0) { initial = emoji_string; initial_is_emoji = true; break; } } } if (initial.empty()) { int charsize = bytesToUtf8CharSize(display_name, 0); if (charsize == 1) initial = std::toupper(display_name[0]); else initial = display_name.substr(0, charsize); } if (display_name[0] != '?' && (std::ispunct(display_name[0]) || std::isdigit(display_name[0]))) initial = "#"; std::string color = s_html_colormap.at("group_color"); if (results.isNull(0, "group_id") && bepaald::contains(s_html_colormap, results.valueAsString(0, d_recipient_avatar_color))) color = s_html_colormap.at(results.valueAsString(0, d_recipient_avatar_color)); // custom color? if (!results.isNull(0, "chat_colors")) { auto [lightcolor, darkcolor] = getCustomColor(results.getValueAs, size_t>>(0, "chat_colors")); //std::cout << "CUSTOM CHAT COLOR (" << display_name << ")" << std::endl; //std::cout << lightcolor << " " << darkcolor << std::endl; if (!lightcolor.empty()) color = lightcolor; else if (!darkcolor.empty()) color = darkcolor; } // custom wallpaper? std::string wall_light; std::string wall_dark; if (!results.isNull(0, "wallpaper")) { auto [lightcolor, darkcolor] = getCustomColor(results.getValueAs, size_t>>(0, "wallpaper")); if (!lightcolor.empty()) { wall_light = lightcolor; wall_dark = (darkcolor.empty() ? lightcolor : darkcolor); } } long long int custom_notifications = -1; if (d_database.tableContainsColumn("recipient", "notification_channel")) custom_notifications = (results.valueAsString(0, "notification_channel").empty() ? 0 : 1); long long int mention_setting = -1; if (d_database.tableContainsColumn("recipient", "mention_setting")) mention_setting = results.valueAsInt(0, "mention_setting"); long long int mute_until = -1; if (d_database.tableContainsColumn("recipient", "mute_until")) mute_until = results.valueAsInt(0, "mute_until"); bool blocked = false; if (d_database.tableContainsColumn("recipient", "blocked")) blocked = (results.valueAsInt(0, "blocked") != 0); long long int message_expiration_time = 0; if (d_database.tableContainsColumn("recipient", "message_expiration_time")) message_expiration_time = results.valueAsInt(0, "message_expiration_time"); bool hasavatar = (std::find_if(d_avatars.begin(), d_avatars.end(), [rid](auto const &p) { return p.first == bepaald::toString(rid); }) != d_avatars.end()); bool verified = (results.valueAsInt(0, "verified") == 1); recipientinfo->emplace(rid, RecipientInfo{display_name, initial, initial_is_emoji, results.valueAsString(0, d_recipient_aci), results.valueAsString(0, d_recipient_e164), results.valueAsString(0, "username"), mute_until, blocked, mention_setting, message_expiration_time, custom_notifications, color, wall_light, wall_dark, hasavatar, verified}); } } /* app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java color: 'pink' -> A150 'C300' -> A150 'A150' -> 0xFFF5D7D7 app/src/main/java/org/thoughtcrime/securesms/color/MaterialColor.java private static final Map COLOR_MATCHES = new HashMap() {{ put("red", CRIMSON); put("deep_orange", CRIMSON); put("orange", VERMILLION); put("amber", VERMILLION); put("brown", BURLAP); put("yellow", BURLAP); put("pink", PLUM); put("purple", VIOLET); put("deep_purple", VIOLET); put("indigo", INDIGO); put("blue", BLUE); put("light_blue", BLUE); put("cyan", TEAL); put("teal", TEAL); put("green", FOREST); put("light_green", WINTERGREEN); put("lime", WINTERGREEN); put("blue_grey", TAUPE); put("grey", STEEL); put("ultramarine", ULTRAMARINE); put("group_color", GROUP); app/src/main/java/org/thoughtcrime/securesms/conversation/colors/ChatColorsPalette.kt val CRIMSON = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFFCF163E.toInt()) val VERMILION = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFFC73F0A.toInt()) val BURLAP = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF6F6A58.toInt()) val FOREST = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF3B7845.toInt()) val WINTERGREEN = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF1D8663.toInt()) val TEAL = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF077D92.toInt()) val BLUE = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF336BA3.toInt()) val INDIGO = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF6058CA.toInt()) val VIOLET = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF9932CB.toInt()) val PLUM = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFFAA377A.toInt()) val TAUPE = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF8F616A.toInt()) val STEEL = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF71717F.toInt()) // from app/src/main/res/values/material3_colors_dark.xml: screen background: #1B1C1F / #1B1C1F chat bubble (other party): #303133 // from app/src/main/java/org/thoughtcrime/securesms/color/MaterialColor.java quote background: outgoing ? int alpha = isDarkTheme(context) ? (int) (0.2 * 255) : (int) (0.4 * 255) : isDarkTheme(context) ? R.color.transparent_black_40 : R.color.transparent_white_60 quote footer: outgoing ? int alpha = isDarkTheme(context) ? (int) (0.4 * 255) : (int) (0.6 * 255) : isDarkTheme(context) ? R.color.transparent_black_60 : R.color.transparent_white_80 // from app/src/main/res/values/colors.xml #66000000 #80000000 #99000000 #99ffffff #b2ffffff #ccffffff */ signalbackup-tools-20250313-1/signalbackup/signalbackup.cc000066400000000000000000000035171476450434500233510ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../common_filesystem.h" SignalBackup::SignalBackup(std::string const &filename, std::string const &passphrase, bool verbose, bool truncate, bool showprogress, bool replaceattachments, bool assumebadframesizeonbadmac, std::vector const &editattachments, bool stoponerror, bool fulldecode) : d_filename(filename), d_passphrase(passphrase), d_found_sqlite_sequence_in_backup(false), d_ok(false), d_databaseversion(-1), d_backupfileversion(-1), d_showprogress(showprogress), d_stoponerror(stoponerror), d_verbose(verbose), d_truncate(truncate), d_fulldecode(fulldecode), d_selfid(-1) { if (bepaald::isDir(filename)) initFromDir(filename, replaceattachments); else // not directory { d_fd.reset(new FileDecryptor(d_filename, d_passphrase, d_verbose, d_stoponerror, assumebadframesizeonbadmac, editattachments)); if (!d_fd->ok()) return; initFromFile(); } if (!d_ok) return; Logger::message("Database version: ", d_databaseversion); checkDbIntegrity(true); } signalbackup-tools-20250313-1/signalbackup/signalbackup.h000066400000000000000000001521131476450434500232100ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef SIGNALBACKUP_H_ #define SIGNALBACKUP_H_ #include "../memsqlitedb/memsqlitedb.h" #include "../filedecryptor/filedecryptor.h" #include "../fileencryptor/fileencryptor.h" #include "../backupframe/backupframe.h" #include "../headerframe/headerframe.h" #include "../databaseversionframe/databaseversionframe.h" #include "../attachmentframe/attachmentframe.h" #include "../avatarframe/avatarframe.h" #include "../sharedprefframe/sharedprefframe.h" #include "../keyvalueframe/keyvalueframe.h" #include "../stickerframe/stickerframe.h" #include "../endframe/endframe.h" #include "../sqlstatementframe/sqlstatementframe.h" #include "../logger/logger.h" #include "../deepcopyinguniqueptr/deepcopyinguniqueptr.h" #include "../groupv2statusmessageproto/groupv2statusmessageproto.h" #include "../attachmentmetadata/attachmentmetadata.h" #include "../common_bytes.h" #include #include #include #include #include #include #include #if defined WIN32 || MINGW // Windows has all sorts of issues with 'long' paths. While this tool can (probably) // work around that (see WIN_LONGPATH in common_filesystem), it does not help much // as many Windows programs can then still not open the resulting files with their // long paths. Instead we limit the file length of files written to some max length // // In Signal currently group titles are limited to 32 characters, while contact names // are max 53 (26 first name + space + 26 last name) (historically, longer group // titles have been possible). // maybe this needs to be done smarter, making sure to only truncate at character // boundaries (not in the middle of multibyte ones... #define MAXFILELENGTH 54 //#define WIN_LIMIT_FILENAME_LENGTH(str) if (str.size() > MAXFILELENGTH) [[unlikely]] str.resize(MAXFILELENGTH); #define WIN_LIMIT_FILENAME_LENGTH(str) #else #define WIN_LIMIT_FILENAME_LENGTH(str) #endif #if defined WIN32 || MINGW #define WIN_CHECK_PATH_LENGTH(str) if (int pl = bepaald::abs_path_length(str); pl >= 260) Logger::warnOnce("Path length is " + bepaald::toString(pl) + ". This may be more than the allowed maximum path length on your platform. If you run into problems, use the option `--compactfilenames' to shorten the filenames this tool writes.", false, STRLEN("Path length is ")); #else #define WIN_CHECK_PATH_LENGTH(str) #endif struct HTMLMessageInfo; struct Range; struct GroupInfo; enum class IconType; class JsonDatabase; class DesktopDatabase; class SignalPlaintextBackupDatabase; class SignalBackup { public: static bool constexpr DROPATTACHMENTDATA = false; protected: MemSqliteDB d_database; DeepCopyingUniquePtr d_fd; FileEncryptor d_fe; std::string d_filename; std::string d_passphrase; // only used in testing bool d_found_sqlite_sequence_in_backup; // table/column names std::string d_mms_table; std::string d_part_table; std::string d_thread_recipient_id; std::string d_thread_message_count; std::string d_thread_delivery_receipts; std::string d_thread_read_receipts; std::string d_thread_pinned; std::string d_sms_date_received; std::string d_sms_recipient_id; std::string d_sms_recipient_device_id; std::string d_mms_date_sent; std::string d_mms_ranges; std::string d_mms_recipient_id; std::string d_mms_recipient_device_id; std::string d_mms_type; std::string d_mms_previews; std::string d_mms_delivery_receipts; std::string d_mms_read_receipts; std::string d_mms_viewed_receipts; std::string d_recipient_aci; std::string d_recipient_e164; std::string d_recipient_avatar_color; std::string d_recipient_system_joined_name; std::string d_recipient_profile_given_name; std::string d_recipient_storage_service; std::string d_recipient_type; std::string d_recipient_profile_avatar; std::string d_recipient_sealed_sender; std::string d_groups_v1_members; std::string d_part_mid; std::string d_part_ct; std::string d_part_pending; std::string d_part_cd; std::string d_part_cl; // table/column names for desktop db std::string d_dt_c_uuid; std::string d_dt_m_sourceuuid; std::string d_dt_s_uuid; std::vector>> d_avatars; std::map, DeepCopyingUniquePtr> d_attachments; //maps to attachment std::map> d_stickers; //maps to sticker DeepCopyingUniquePtr d_headerframe; DeepCopyingUniquePtr d_databaseversionframe; std::vector> d_sharedpreferenceframes; std::vector> d_keyvalueframes; DeepCopyingUniquePtr d_endframe; std::vector> d_badattachments; bool d_ok; unsigned int d_databaseversion; unsigned int d_backupfileversion; bool d_showprogress; bool d_stoponerror; bool d_verbose; bool d_truncate; bool d_fulldecode; long long int d_selfid; std::string d_selfuuid; enum DBLinkFlag : int { NO_COMPACT = 0b01, // don't run compactids on this table SKIP = 0b10, // ignore this table, but don't warn about it not being handled WARN = 0b100, // this is a somewhat unknown table, warn about it }; enum LinkFlag : int { SET_UNIQUELY = (1 << 0), //NEW_FLAG = (1 << 1), //ANOTHER_FLAG = (1 << 2), }; struct RecipientIdentification { std::string uuid; std::string phone; std::string group_id; std::string distribution_id; //long long int group_type; // NONE(0), MMS(1), SIGNAL_GV1(2), SIGNAL_GV2(3), DISTRIBUTION_LIST(4), CALL_LINK(5); std::string storage_service; }; struct TableConnection { std::string table; std::string column; std::string whereclause = std::string(); std::string json_path = std::string(); int flags = 0; unsigned int mindbvversion = 0; unsigned int maxdbvversion = std::numeric_limits::max(); }; struct DatabaseLink { std::string table; std::string column; std::vector const connections; int flags; }; struct RecipientInfo { std::string display_name; std::string initial; bool initial_is_emoji; std::string uuid; std::string phone; std::string username; long long int mute_until; // -1 : n/a bool blocked; long long int mention_setting; // -1 : n/a long long int message_expiration_time; long long int custom_notifications; // -1 : n/a std::string color; // "RRGGBB" std::string wall_light; std::string wall_dark; bool hasavatar; bool verified; }; static std::vector const s_databaselinks; static std::map>> const s_columnaliases; static char const *const s_emoji_unicode_list[3781]; static std::unordered_set const s_emoji_first_bytes; static unsigned int constexpr s_emoji_min_size = 2; // smallest emoji_unicode_size - 1 static std::map const s_html_colormap; static std::array, 12> const s_html_random_colors; static std::regex const s_linkify_pattern; protected: inline SignalBackup(bool verbose, bool truncate, bool showprogress); public: inline SignalBackup(std::string const &filename, std::string const &passphrase, bool verbose, bool truncate, bool showprogress, bool replaceattachments); SignalBackup(std::string const &filename, std::string const &passphrase, bool verbose, bool truncate, bool showprogress, bool replaceattachment, bool assumebadframesizeonbadmac, std::vector const &editattachments, bool stoponerror, bool fulldecode); inline SignalBackup(SignalBackup const &other) = default; inline SignalBackup &operator=(SignalBackup const &other) = default; inline SignalBackup(SignalBackup &&other) = default; inline SignalBackup &operator=(SignalBackup &&other) = default; [[nodiscard]] bool exportBackup(std::string const &filename, std::string const &passphrase, bool overwrite, bool keepattachmentdatainmemory, bool onlydb = false); bool exportXml(std::string const &filename, bool overwrite, std::string self, bool includemms = false, bool keepattachmentdatainmemory = true); bool exportCsv(std::string const &filename, std::string const &table, bool overwrite) const; void listThreads() const; void listRecipients() const; void cropToThread(long long int threadid); void cropToThread(std::vector const &threadid); void cropToDates(std::vector> const &dateranges); inline void addSMSMessage(std::string const &body, std::string const &address, std::string const ×tamp, long long int thread, bool incoming); void addSMSMessage(std::string const &body, std::string const &address, long long int timestamp, long long int thread, bool incoming); bool importThread(SignalBackup *source, long long int thread); //bool importThread(SignalBackup *source, std::vector const &threads); inline bool ok() const; bool dropBadFrames(); //void fillThreadTableFromMessages(); inline void addEndFrame(); bool mergeRecipients(std::vector const &addresses); void mergeGroups(std::vector const &groups); inline void runQuery(std::string const &q, std::string const &mode = std::string()) const; void removeDoubles(long long int milliseconds = 0); inline std::vector threadIds() const; bool importCSV(std::string const &file, std::map const &fieldmap); //bool importWAChat(std::string const &file, std::string const &fmt, std::string const &self = std::string()); bool summarize() const; bool reorderMmsSmsIds() const; bool dumpMedia(std::string const &dir, std::vector const &dateranges, std::vector const &threads, bool excludestickers, bool overwrite) const; bool dumpAvatars(std::string const &dir, std::vector const &contacts, bool overwrite) const; bool deleteAttachments(std::vector const &threadids, std::string const &before, std::string const &after, long long int filesize, std::vector const &mimetypes, std::string const &append, std::string const &prepend, std::vector> replace); inline void showDBInfo() const; bool scramble() const; //std::pair getDesktopDir() const; bool importFromDesktop(std::unique_ptr const &dtdb, bool skipmessagereorder, std::vector const &dateranges, bool createmissingcontacts, bool createcontacts_nowarn, bool autodates, bool importstickers, std::string const &selfphone, bool targetisdummy); bool importFromPlaintextBackup(std::unique_ptr const &ptdb, bool skipmessagereorder, std::vector> const &initial_contactmap, std::vector const &daterangelist, std::vector const &chats, bool createmissingcontacts, bool markdelivered, bool markread, bool autodates, std::string const &selfphone, bool targetisdummy); long long int ptCreateRecipient(std::unique_ptr const &ptdb, std::map *contactmap, bool *warned_createcontacts, std::string const &contact_name, std::string const &address, bool isgroup) const; bool checkDbIntegrity(bool warn = false) const; bool exportHtml(std::string const &directory, std::vector const &threads, std::vector const &dateranges, std::string const &splitby, long long int split, std::string const &selfid, bool calllog, bool searchpage, bool stickerpacks, bool migrate, bool overwrite, bool append, bool theme, bool themeswitching, bool addexportdetails, bool blocked, bool fullcontacts, bool settings, bool receipts, bool use_original_filenames, bool linkify, bool chatfolders, bool compact, bool pagemenu, std::vector const &ignoremediatypes); bool exportTxt(std::string const &directory, std::vector const &threads, std::vector const &dateranges, std::string const &selfid, bool migrate, bool overwrite); bool findRecipient(long long int id) const; long long int getRecipientIdFromName(std::string const &name, bool withthread) const; long long int getRecipientIdFromPhone(std::string const &phone, bool withthread) const; long long int getRecipientIdFromUsername(std::string const &phone, bool withthread) const; long long int getThreadIdFromRecipient(std::string const &recipient) const; long long int getThreadIdFromRecipient(long long int recipientid) const; bool importTelegramJson(std::string const &file, std::vector const &chatselection, std::vector> contactmap, std::vector const &inhibitmapping, bool prependforwarded, bool skipmessagereorder, bool markdelivered, bool markread, std::string const &selfphone); bool setChatColors(std::vector> const &colorlist); /* CUSTOMS */ //bool hhenkel(std::string const &); //bool sleepyh34d(std::string const &truncatedbackup, std::string const &pwd); bool hiperfall(uint64_t t_id, std::string const &selfid); void scanMissingAttachments() const; //void devCustom() const; //bool carowit(std::string const &sourcefile, std::string const &sourcepw) const; bool custom_hugogithubs(); bool arc(long long int tid, std::string const &selfphone); // for bug 233/13034 bool migrate_to_191(std::string const &selfphone); /* CUSTOMS */ //bool migrate_to_191_CUSTOM(std::string const &selfphone); protected: [[nodiscard]] bool exportBackupToFile(std::string const &filename, std::string const &passphrase, bool overwrite, bool keepattachmentdatainmemory); [[nodiscard]] bool exportBackupToDir(std::string const &directory, bool overwrite, bool keepattachmentdatainmemory, bool onlydb); void initFromFile(); void initFromDir(std::string const &inputdir, bool replaceattachments); void updateThreadsEntries(long long int thread = -1); long long int getMaxUsedId(std::string const &table, std::string const &col = "_id") const; long long int getMinUsedId(std::string const &table, std::string const &col = "_id") const; template [[nodiscard]] inline bool writeRawFrameDataToFile(std::string const &outputfile, T *frame) const; template [[nodiscard]] inline bool writeRawFrameDataToFile(std::string const &outputfile, std::unique_ptr const &frame) const; [[nodiscard]] inline bool writeFrameDataToFile(std::ofstream &outputfile, std::pair const &data) const; [[nodiscard]] bool writeEncryptedFrame(std::ofstream &outputfile, BackupFrame *frame); [[nodiscard]] bool writeEncryptedFrameWithoutAttachment(std::ofstream &outputfile, std::pair, uint64_t> framedata); SqlStatementFrame buildSqlStatementFrame(std::string const &table, std::vector const &headers, std::vector const &result) const; SqlStatementFrame buildSqlStatementFrame(std::string const &table, std::vector const &result) const; template inline bool setFrameFromFile(DeepCopyingUniquePtr *frame, std::string const &file, bool quiet = false) const; template inline bool setFrameFromStrings(DeepCopyingUniquePtr *frame, std::vector const &lines) const; template inline std::pair numToData(T num) const; void setMinimumId(std::string const &table, long long int offset, std::string const &col = "_id") const; void cleanDatabaseByMessages(); void getGroupV1MigrationRecipients(std::set *referenced_recipients, long long int = -1) const; void remapRecipients(); void compactIds(std::string const &table, std::string const &col = "_id"); // void makeIdsUnique(long long int minthread, long long int minsms, long long int minmms, // long long int minpart, long long int minrecipient, long long int mingroups, // long long int minidentities, long long int mingroup_receipts, long long int mindrafts, // long long int minsticker, long long int minmegaphone, long long int minremapped_recipients, // long long int minremapped_threads, long long int minmention, // long long int minmsl_payload, long long int minmsl_message, long long int minmsl_recipient, // long long int minreaction, long long int mingroup_call_ring, // long long int minnotification_profile, long long int minnotification_profile_allowed_members, // long long int minnotification_profile_schedule); void makeIdsUnique(SignalBackup *source); void updateRecipientId(long long int targetid, RecipientIdentification const &ident); //void updateRecipientId(long long int targetid, std::string const &ident); void updateRecipientId(long long int targetid, long long int sourceid); void updateGroupMembers(long long int id1, long long int id2 = -1) const; // id2 == -1 -> id1 = offset, else transform 1 into 2 void updateReactionAuthors(long long int id1, long long int id2 = -1) const; // idem. void updateGV1MigrationMessage(long long int id1, long long int id2 = -1) const; // idem. void updateAvatars(long long int id1, long long int id2 = -1); // idem. void updateSnippetExtrasRecipient(long long int id1, long long int id2 = -1) const; // idem. long long int dateToMSecsSinceEpoch(std::string const &date, bool *fromdatestring = nullptr) const; void dumpInfoOnBadFrame(std::unique_ptr *frame); void dumpInfoOnBadFrames() const; void duplicateQuotes(std::string *s) const; std::string decodeStatusMessage(std::string const &body, long long int expiration, long long int type, std::string const &contactname, IconType *icon = nullptr) const; std::string decodeStatusMessage(std::pair, size_t> const &body, long long int expiration, long long int type, std::string const &contactname, IconType *icon = nullptr) const; void escapeXmlString(std::string *s) const; bool unescapeXmlString(std::string *s) const; void handleSms(SqliteDB::QueryResults const &results, std::ofstream &outputfile, std::string const &self [[maybe_unused]], int i) const; void handleMms(SqliteDB::QueryResults const &results, std::ofstream &outputfile, std::string const &self, int i, bool keepattachmentdatainmemory) const; inline std::string getStringOr(SqliteDB::QueryResults const &results, int i, std::string const &columnname, std::string const &def = std::string()) const; inline long long int getIntOr(SqliteDB::QueryResults const &results, int i, std::string const &columnname, long long int def) const; // bool handleWAMessage(long long int thread_id, long long int time, std::string const &chatname, std::string const &author, // std::string const &message, std::string const &selfid, bool isgroup, // std::map const &name_to_recipientid); bool setFileTimeStamp(std::string const &file, long long int time_usec) const; std::string sanitizeFilename(std::string const &filename) const; bool setColumnNames(); void dtSetColumnNames(SqliteDB *ddb); long long int scanSelf() const; bool cleanAttachments(); inline bool updatePartTableForReplace(AttachmentMetadata const &data, long long int id); bool scrambleHelper(std::string const &table, std::vector const &columns) const; std::vector getGroupUpdateRecipients(int thread = -1) const; void getGroupUpdateRecipientsFromGV2Context(DecryptedGroupV2Context const &sts2, std::set *uuids) const; bool getGroupMembersModern(std::vector *members, std::string const &group_id) const; bool getGroupMembersOld(std::vector *members, std::string const &group_id, std::string const &column = "members") const; bool missingAttachmentExpected(uint64_t rowid, int64_t unique_id) const; template inline bool setFrameFromLine(DeepCopyingUniquePtr *newframe, std::string const &line) const; bool insertRow(std::string const &table, std::vector> data, std::string const &returnfield = std::string(), std::any *returnvalue = nullptr) const; bool updateRows(std::string const &table, std::vector> data, std::vector> whereclause, std::string const &returnfield = std::string(), std::any *returnvalue = nullptr) const; bool dtInsertAttachments(long long int mms_id, long long int unique_id, int numattachments, long long int haspreviews, long long int rowid, SqliteDB const &ddb, std::string const &where, std::string const &databasedir, bool isquote, bool issticker, bool targetisdummy); bool handleDTCallTypeMessage(SqliteDB const &ddb, std::string const &callid, long long int rowid, long long int ttid, long long int address, bool insertincompletedataforexport) const; bool handleDTGroupChangeMessage(SqliteDB const &ddb, long long int rowid, long long int thread_id, long long int address, long long int date, std::map *adjusted_timestamps, std::map *savedmap, std::string const &databasedir, bool istimermessage, bool createcontacts, bool create_valid_contacts, bool *warn); bool handleDTExpirationChangeMessage(SqliteDB const &ddb, long long int rowid, long long int ttid, long long int sent_at, long long int address) const; bool handleDTGroupV1Migration(SqliteDB const &ddb, long long int rowid, long long int thread_id, long long int timestamp, long long int address, std::map *savedmap, bool createcontacts, std::string const &databasedir, bool create_valid_contacts, bool *warn); void getDTReactions(SqliteDB const &ddb, long long int rowid, long long int numreactions, std::vector> *reactions) const; void dtInsertReactions(SqliteDB const &ddb, long long int message_id, std::vector> const &reactions, bool mms, std::map *savedmap, std::string const &databasedir, bool createcontacts, bool create_valid_contacts); long long int getRecipientIdFromUuidMapped(std::string const &uuid, std::map *savedmap, bool suppresswarning = false) const; long long int getRecipientIdFromPhoneMapped(std::string const &phone, std::map *savedmap, bool suppresswarning = false) const; inline std::string getNameFromUuid(std::string const &uuid) const; std::string getNameFromRecipientId(long long int id) const; void dtSetMessageDeliveryReceipts(SqliteDB const &ddb, long long int rowid, std::map *savedmap, std::string const &databasedir, bool createcontacts, long long int msg_id, bool is_mms, bool isgroup, bool create_valid_contacts, bool *warn); bool HTMLwriteStart(std::ofstream &file, long long int thread_recipient_id, std::string const &directory, std::string const &threaddir, bool isgroup, bool isnotetoself, bool isreleasechannel, std::set const &recipients, std::map *recipientinfo, std::map *written_avatars, bool overwrite, bool append, bool light, bool themeswitching, bool searchpage, bool exportdetails, bool pagemenu) const; void HTMLwriteAttachmentDiv(std::ofstream &htmloutput, SqliteDB::QueryResults const &attachment_results, int indent, std::string const &directory, std::string const &threaddir, bool use_original_filenames, bool is_image_preview, bool overwrite, bool append, std::vector const &ignoremediatypes) const; void HTMLwriteCallLinkDiv(std::ofstream &htmloutput, int indent, std::string const &url, std::string const &title, std::string const &description/*, std::string const &directory, std::string const &threaddir, bool overwrite, bool append*/) const; void HTMLwriteSharedContactDiv(std::ofstream &htmloutput, std::string const &shared_contact, int indent, std::string const &directory, std::string const &threaddir, bool overwrite, bool append) const; bool HTMLwriteAttachment(std::string const &directory, std::string const &threaddir, long long int rowid, long long int uniqueid, std::string const &attachment_filename, long long int timestamp, bool overwrite, bool append) const; bool HTMLprepMsgBody(std::string *body, std::vector> const &mentions, std::map *recipients_info, bool incoming, std::pair, size_t> const &brdata, bool linkify, bool isquote) const; std::string HTMLwriteAvatar(long long int recipient_id, std::string const &directory, std::string const &threaddir, bool overwrite, bool append) const; void HTMLwriteMessage(std::ofstream &filt, HTMLMessageInfo const &msginfo, std::map *recipientinfo, bool searchpage, bool writereceipts, std::vector const &ignoremediatypes) const; void HTMLwriteRevision(long long int msg_id, std::ofstream &filt, HTMLMessageInfo const &parent_info, std::map *recipientinfo, bool linkify, std::vector const &ignoremediatypes) const; void HTMLwriteMsgReceiptInfo(std::ofstream &htmloutput, std::map *recipientinfo, long long int message_id, bool isgroup, long long int read_count, long long int delivered_count, long long int timestamp, int indent) const; inline bool HTMLwriteIndex(std::vector const &threads, long long int maxtimestamp, std::string const &directory, std::map *recipient_info, long long int note_to_self_tid, bool calllog, bool searchpage, bool stickerpacks, bool blocked, bool fullcontacts, bool settings, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails, std::vector> const &chatfolders, bool compact) const; inline bool HTMLwriteChatFolder(std::vector const &threads, long long int maxtimestamp, std::string const &directory, std::string const &basename, std::map *recipient_info, long long int note_to_self_tid, bool calllog, bool searchpage, bool stickerpacks, bool blocked, bool fullcontacts, bool settings, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails, long long int chatfolderidx, std::vector> const &chatfolders, bool compact) const; bool HTMLwriteIndexImpl(std::vector const &threads, long long int maxtimestamp, std::string const &directory, std::string const &basename, std::map *recipient_info, long long int note_to_self_tid, bool calllog, bool searchpage, bool stickerpacks, bool blocked, bool fullcontacts, bool settings, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails, long long int chatfolderidx, std::vector> const &chatfolders, bool compact) const; void HTMLwriteSearchpage(std::string const &dir, bool light, bool themeswitching, bool compact) const; void HTMLwriteCallLog(std::vector const &threads, std::string const &directory, std::string const &datewhereclause, std::map *recipientinfo, long long int notetoself_tid, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails, bool compact) const; bool HTMLwriteStickerpacks(std::string const &dir, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails) const; bool HTMLwriteBlockedlist(std::string const &dir, std::map *recipientinfo, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails, bool compact) const; bool HTMLwriteFullContacts(std::string const &dir, std::map *recipientinfo, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails, bool compact) const; bool HTMLwriteSettings(std::string const &dir, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails) const; void HTMLescapeString(std::string *in, std::set const *const positions_excluded_from_escape = nullptr) const; std::string HTMLescapeString(std::string const &in) const; void HTMLescapeUrl(std::string *in) const; std::string HTMLescapeUrl(std::string const &in) const; void HTMLLinkify(std::string const &body, std::vector *ranges) const; std::set getAllThreadRecipients(long long int t) const; void setRecipientInfo(std::set const &recipients, std::map *recipientinfo) const; std::string getAvatarExtension(long long int recipient_id) const; void prepRanges(std::vector *ranges) const; void applyRanges(std::string *body, std::vector *ranges, std::set *positions_excluded_from_escape) const; std::vector> HTMLgetEmojiPos(std::string const &line) const; bool makeFilenameUnique(std::string const &path, std::string *file_or_dir) const; std::string decodeProfileChangeMessage(std::string const &body, std::string const &name) const; inline int numBytesInUtf16Substring(std::string const &text, unsigned int idx, int length) const; inline int utf16CharSize(std::string const &body, int idx) const; inline int utf8Chars(std::string const &body) const; inline void resizeToNUtf8Chars(std::string &body, unsigned long size) const; inline int bytesToUtf8CharSize(std::string const &body, int idx) const; std::string utf8BytesToHexString(unsigned char const *const data, size_t data_size) const; inline std::string utf8BytesToHexString(std::shared_ptr const &data, size_t data_size) const; inline std::string utf8BytesToHexString(std::string const &data) const; inline RecipientInfo const &getRecipientInfoFromMap(std::map *recipient_info, long long int rid) const; bool migrateDatabase(int from, int to) const; long long int dtCreateRecipient(SqliteDB const &ddb, std::string const &id, std::string const &phone, std::string const &gidb64, std::string const &databasedir, std::map *recipient_info, bool create_valid_contacts, bool *warn); bool dtUpdateProfile(SqliteDB const &ddb, std::string const &dtid, long long int aid, std::string const &databasedir); bool dtSetAvatar(std::string const &avatarpath, std::string const &key, int64_t size, int version, long long int rid, std::string const &databasedir); std::string dtSetSharedContactsJsonString(SqliteDB const &ddb, long long int rowid) const; void getGroupInfo(long long int rid, GroupInfo *groupinfo) const; std::pair getCustomColor(std::pair, size_t> const &colorproto) const; std::string HTMLprepLinkPreviewDescription(std::string const &in) const; long long int getFreeDateForMessage(long long int targetdate, long long int thread_id, long long int from_recipient_id) const; inline void TXTaddReactions(SqliteDB::QueryResults const *const reaction_results, std::ofstream *out) const; void setLongMessageBody(std::string *body, SqliteDB::QueryResults *attachment_results) const; bool tgImportMessages(SqliteDB const &db, std::vector, long long int>> const &contactmap, std::string const &datapath, std::string const &threadname, long long int chat_idx, bool prependforwarded, bool markdelivered, bool markread, bool isgroup); bool tgMapContacts(JsonDatabase const &jdb, std::string const &chatselection, std::vector, long long int>> *contactmap, std::vector const &inhibitmappping) const; std::string tgBuildBody(std::string const &bodyjson) const; bool tgSetBodyRanges(std::string const &bodyjson, long long int message_id); bool tgSetAttachment(SqliteDB::QueryResults const &message_data, std::string const &datapath, long long int r, long long int new_msg_id); bool tgSetQuote(long long int quoted_message_id, long long int new_msg_id); bool dtImportStickerPacks(SqliteDB const &ddb, std::string const &databasedir); void dtImportLongText(std::string const &msgbody_full, long long int new_mms_id, long long int uniqueid); bool prepareOutputDirectory(std::string const &dir, bool overwrite, bool allowappend = false, bool append = false) const; std::string getTranslatedName(std::string const &table, std::string const &old_column_name) const; bool writeStickerToDisk(long long int id, std::string const &packid, std::string const &directory, bool overwrite, bool append, std::string *extension) const; long long int getRecipientIdFromField(std::string const &field, std::string const &value, bool withthread) const; std::string unicodeToUtf8(uint32_t unicode) const; int utf16ToUnicodeCodepoint(uint16_t utf16, uint32_t *codepoint) const; std::string makePrintable(std::string const &in) const; }; // ONLY FOR DUMMYBACKUP inline SignalBackup::SignalBackup(bool verbose, bool truncate, bool showprogress) : d_found_sqlite_sequence_in_backup(false), d_ok(false), d_databaseversion(-1), d_backupfileversion(-1), d_showprogress(showprogress), d_stoponerror(false), d_verbose(verbose), d_truncate(truncate), d_fulldecode(false), d_selfid(-1) {} inline SignalBackup::SignalBackup(std::string const &filename, std::string const &passphrase, bool verbose, bool truncate, bool showprogress, bool replaceattachments) : SignalBackup(filename, passphrase, verbose, truncate, showprogress, replaceattachments, false, std::vector(), false, false) {} inline bool SignalBackup::ok() const { return d_ok; } template inline bool SignalBackup::writeRawFrameDataToFile(std::string const &outputfile, T *frame) const { if (!frame) { Logger::error("Asked to write nullptr frame to disk"); return false; } std::ofstream rawframefile(outputfile, std::ios_base::binary); if (!rawframefile.is_open()) { Logger::error("Opening file for writing: ", outputfile); return false; } if (frame->frameType() == BackupFrame::FRAMETYPE::END) rawframefile << "END" << std::endl; else { std::string d = frame->getHumanData(); rawframefile << d; } return rawframefile.good(); } template inline bool SignalBackup::writeRawFrameDataToFile(std::string const &outputfile, std::unique_ptr const &frame) const { return writeRawFrameDataToFile(outputfile, frame.get()); } bool SignalBackup::writeFrameDataToFile(std::ofstream &outputfile, std::pair const &data) const { uint32_t besize = bepaald::swap_endian(static_cast(data.second)); // write 4 byte size header if (!outputfile.write(reinterpret_cast(&besize), sizeof(uint32_t))) return false; // write data if (!outputfile.write(reinterpret_cast(data.first), data.second)) return false; return true; } template inline std::pair SignalBackup::numToData(T num) const { unsigned char *data = new unsigned char[sizeof(T)]; std::memcpy(data, reinterpret_cast(&num), sizeof(T)); return {data, sizeof(T)}; } template inline bool SignalBackup::setFrameFromLine(DeepCopyingUniquePtr *newframe, std::string const &line) const { if (line.empty()) return true; std::string::size_type pos = line.find(":", 0); if (pos == std::string::npos) [[unlikely]] { Logger::error("Failed to read frame data line '", line, "'"); return false; } unsigned int field = (*newframe)->getField(std::string_view(line.data(), pos)); if (!field) [[unlikely]] { Logger::error("Failed to get field number"); return false; } ++pos; std::string::size_type pos2 = line.find(":", pos); if (pos2 == std::string::npos) [[unlikely]] { Logger::error("Failed to read frame data from line '", line, "'"); return false; } std::string_view type(line.data() + pos, pos2 - pos); std::string_view datastr(line.data() + pos2 + 1); if (type == "uint64" || type == "uint32") // Note stoul and stoull are the same on linux. Internally 8 byte int are needed anyway. { // (on windows stoul would be four bytes and the above if-clause would cause bad data std::pair decdata = numToData(bepaald::swap_endian(bepaald::toNumber(datastr))); if (!decdata.first) [[unlikely]] return false; (*newframe)->setNewData(field, decdata.first, decdata.second); } else if (type == "string") { unsigned char *data = new unsigned char[datastr.size()]; std::memcpy(data, datastr.data(), datastr.size()); (*newframe)->setNewData(field, data, datastr.size()); } else if (type == "bool") // since booleans are stored as varints, this is identical to uint64/32 code { std::string val = (datastr == "true") ? "1" : "0"; std::pair decdata = numToData(bepaald::swap_endian(std::stoull(val))); if (!decdata.first) [[unlikely]] return false; (*newframe)->setNewData(field, decdata.first, decdata.second); } else if (type == "bytes") { std::pair decdata = Base64::base64StringToBytes(datastr); if (!decdata.first) [[unlikely]] return false; (*newframe)->setNewData(field, decdata.first, decdata.second); } else if (type == "int64" || type == "int32") // Note stol and stoll are the same on linux. Internally 8 byte int are needed anyway. { // (on windows stol would be four bytes and the above if-clause would cause bad data std::pair decdata = numToData(bepaald::swap_endian(bepaald::toNumber(datastr))); if (!decdata.first) [[unlikely]] return false; (*newframe)->setNewData(field, decdata.first, decdata.second); } else if (type == "float") // due to possible precision problems, the 4 bytes of float are saved in binary format (base64 encoded) { // WARNING untested std::pair decfloat = Base64::base64StringToBytes(datastr); if (!decfloat.first) [[unlikely]] return false; if (decfloat.second != 4) [[unlikely]] { delete[] decfloat.first; return false; } (*newframe)->setNewData(field, decfloat.first, decfloat.second); } else return false; return true; } template <> inline bool SignalBackup::setFrameFromFile(DeepCopyingUniquePtr *frame, std::string const &file, bool quiet) const { std::ifstream datastream(file, std::ios_base::binary | std::ios::in); if (!datastream.is_open()) { if (!quiet) Logger::error("Failed to open '", file, "' for reading"); return false; } frame->reset(new EndFrame(nullptr, 1ull)); return true; } template inline bool SignalBackup::setFrameFromFile(DeepCopyingUniquePtr *frame, std::string const &file, bool quiet) const { std::ifstream datastream(file, std::ios_base::binary | std::ios::in); if (!datastream.is_open()) { if (!quiet) Logger::error("Failed to open '", file, "' for reading"); return false; } DeepCopyingUniquePtr newframe(new T); std::string line; while (std::getline(datastream, line)) if (!setFrameFromLine(&newframe, line)) return false; frame->reset(newframe.release()); return true; } template inline bool SignalBackup::setFrameFromStrings(DeepCopyingUniquePtr *frame, std::vector const &lines) const { DeepCopyingUniquePtr newframe(new T); for (auto const &l : lines) if (!setFrameFromLine(&newframe, l)) return false; frame->reset(newframe.release()); return true; } inline void SignalBackup::addEndFrame() { d_endframe.reset(new EndFrame(nullptr, 1ull)); } inline void SignalBackup::runQuery(std::string const &q, std::string const &mode) const { Logger::message(" * Executing query: ", q); SqliteDB::QueryResults res; if (!d_database.exec(q, &res)) return; std::string q_comm(q, 0, STRLEN("DELETE")); // delete, insert and update are same length... std::for_each(q_comm.begin(), q_comm.end(), [] (char &ch) { ch = std::toupper(ch); }); if (q_comm == "DELETE" || q_comm == "INSERT" || q_comm == "UPDATE") { Logger::message("Modified ", d_database.changed(), " rows"); if (res.rows() == 0 && res.columns() == 0) return; } if (mode == "pretty") res.prettyPrint(d_truncate); else if (mode == "line") res.printLineMode(); else if (mode == "single") res.printSingleLine(); else res.print(); } inline void SignalBackup::addSMSMessage(std::string const &body, std::string const &address, std::string const ×tamp, long long int thread, bool incoming) { addSMSMessage(body, address, dateToMSecsSinceEpoch(timestamp), thread, incoming); } inline std::vector SignalBackup::threadIds() const { std::vector res; SqliteDB::QueryResults results; d_database.exec("SELECT DISTINCT _id FROM thread ORDER BY _id ASC", &results); if (results.columns() == 1) for (unsigned int i = 0; i < results.rows(); ++i) if (results.valueHasType(i, 0)) res.push_back(results.getValueAs(i, 0)); return res; } inline void SignalBackup::showDBInfo() const { Logger::message("Database version: ", d_databaseversion); d_database.print("SELECT m.name as TABLE_NAME, p.name as COLUMN_NAME FROM sqlite_master m LEFT OUTER JOIN pragma_table_info((m.name)) p ON m.name <> p.name ORDER BY TABLE_NAME, COLUMN_NAME"); } inline std::string SignalBackup::getStringOr(SqliteDB::QueryResults const &results, int i, std::string const &columnname, std::string const &def) const { std::string tmp(def); if (results.hasColumn(columnname)) // to prevent warning in this case, we check the if (results.valueHasType(i, columnname)) // column name. This function expect it may fail { tmp = results.getValueAs(i, columnname); escapeXmlString(&tmp); } return tmp; } inline long long int SignalBackup::getIntOr(SqliteDB::QueryResults const &results, int i, std::string const &columnname, long long int def) const { long long int tmp = def; if (results.hasColumn(columnname)) // to prevent warning in this case, we check the if (results.valueHasType(i, columnname)) // column name. This function expect it may fail tmp = results.getValueAs(i, columnname); return tmp; } inline bool SignalBackup::updatePartTableForReplace(AttachmentMetadata const &data, long long int id) { if (!updateRows(d_part_table, {{d_part_ct, data.filetype}, {"data_size", data.filesize}, {"width", data.width}, {"height", data.height}, {(d_database.tableContainsColumn(d_part_table, "data_hash") ? "data_hash" : ""), data.hash}, {(d_database.tableContainsColumn(d_part_table, "data_hash_start") ? "data_hash_start" : ""), data.hash}, {(d_database.tableContainsColumn(d_part_table, "data_hash_end") ? "data_hash_end" : ""), data.hash}}, {{"_id", id}}) || d_database.changed() != 1) return false; return true; } inline std::string SignalBackup::getNameFromUuid(std::string const &uuid) const { return getNameFromRecipientId(getRecipientIdFromUuidMapped(uuid, nullptr)); } inline bool SignalBackup::HTMLwriteIndex(std::vector const &threads, long long int maxtimestamp, std::string const &directory, std::map *recipient_info, long long int note_to_self_tid, bool calllog, bool searchpage, bool stickerpacks, bool blocked, bool fullcontacts, bool settings, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails, std::vector> const &chatfolders, bool compact) const { return HTMLwriteIndexImpl(threads, maxtimestamp, directory, "index", recipient_info, note_to_self_tid, calllog, searchpage, stickerpacks, blocked, fullcontacts, settings, overwrite, append, light, themeswitching, exportdetails, -1, chatfolders, compact); } inline bool SignalBackup::HTMLwriteChatFolder(std::vector const &threads, long long int maxtimestamp, std::string const &directory, std::string const &basename, std::map *recipient_info, long long int note_to_self_tid, bool calllog, bool searchpage, bool stickerpacks, bool blocked, bool fullcontacts, bool settings, bool overwrite, bool append, bool light, bool themeswitching, std::string const &exportdetails, long long int chatfolderidx, std::vector> const &chatfolders, bool compact) const { return HTMLwriteIndexImpl(threads, maxtimestamp, directory, basename, recipient_info, note_to_self_tid, calllog, searchpage, stickerpacks, blocked, fullcontacts, settings, overwrite, append, light, themeswitching, exportdetails, chatfolderidx, chatfolders, compact); } inline int SignalBackup::utf16CharSize(std::string const &body, int idx) const { // get code point uint32_t codepoint = 0; if ((static_cast(body[idx]) & 0b11111000) == 0b11110000) // 4 byte char /* codepoint = (static_cast(body[idx]) & 0b00000111) << 18 | (static_cast(body[idx + 1]) & 0b00111111) << 12 | (static_cast(body[idx + 2]) & 0b00111111) << 6 | (static_cast(body[idx + 3]) & 0b00111111); */ return 2; // all 4 byte utf8 chars are 2 bytes in utf16 else if ((static_cast(body[idx]) & 0b11110000) == 0b11100000) // 3 byte char codepoint = (static_cast(body[idx]) & 0b00001111) << 12 | (static_cast(body[idx + 1]) & 0b00111111) << 6 | (static_cast(body[idx + 2]) & 0b00111111); /* else if ((static_cast(body[idx]) & 0b11100000) == 0b11000000) // 2 byte char codepoint = (static_cast(body[idx]) & 0b00011111) << 6 | (static_cast(body[idx + 1]) & 0b00111111); else codepoint = static_cast(body[idx]); */ else // all 1 and two byte utf-8 chars are 1 utf-16 char (max is 0b11111111111 which < 0x10000) return 1; return codepoint >= 0x10000 ? 2 : 1; } inline int SignalBackup::numBytesInUtf16Substring(std::string const &text, unsigned int idx, int length) const { int utf16count = 0; int bytecount = 0; while (utf16count < length && idx < text.size()) { utf16count += utf16CharSize(text, idx); int utf8size = bytesToUtf8CharSize(text, idx); bytecount += utf8size; idx += utf8size; } return bytecount; } inline int SignalBackup::utf8Chars(std::string const &body) const { int res = 0; for (unsigned int i = 0; i < body.size(); ) { ++res; i += bytesToUtf8CharSize(body, i); } return res; } inline void SignalBackup::resizeToNUtf8Chars(std::string &body, unsigned long size) const { unsigned long res = 0; unsigned int idx = 0; while (idx < body.size()) { ++res; idx += bytesToUtf8CharSize(body, idx); if (res == size) break; } if (idx < body.size()) body.resize(idx); } inline int SignalBackup::bytesToUtf8CharSize(std::string const &body, int idx) const { if ((static_cast(body[idx]) & 0b10000000) == 0b00000000) return 1; else if ((static_cast(body[idx]) & 0b11100000) == 0b11000000) // 2 byte char return 2; else if ((static_cast(body[idx]) & 0b11110000) == 0b11100000) // 3 byte char return 3; else if ((static_cast(body[idx]) & 0b11111000) == 0b11110000) // 4 byte char return 4; else return 1; } inline std::string SignalBackup::utf8BytesToHexString(std::shared_ptr const &data, size_t data_size) const { return utf8BytesToHexString(data.get(), data_size); } inline std::string SignalBackup::utf8BytesToHexString(std::string const &data) const { return utf8BytesToHexString(reinterpret_cast(data.data()), data.size()); } inline SignalBackup::RecipientInfo const &SignalBackup::getRecipientInfoFromMap(std::map *recipient_info, long long int rid) const { if (auto found = recipient_info->find(rid); found != recipient_info->end()) [[likely]] return found->second; // if (bepaald::contains(recipient_info, rid)) // return (*recipient_info)[rid]; setRecipientInfo({rid}, recipient_info); return (*recipient_info)[rid]; } inline void SignalBackup::TXTaddReactions(SqliteDB::QueryResults const *const reaction_results, std::ofstream *out) const { if (reaction_results->rows() == 0) [[likely]] return; *out << " ("; for (unsigned int r = 0; r < reaction_results->rows(); ++r) { std::string emojireaction = reaction_results->valueAsString(r, "emoji"); std::string authordisplayname = getNameFromRecipientId(reaction_results->getValueAs(r, "author_id")); *out << authordisplayname << ": " << emojireaction; if (r < reaction_results->rows() - 1) *out << "; "; } *out << ")"; } #endif signalbackup-tools-20250313-1/signalbackup/signalbackup.ih000066400000000000000000000031421476450434500233560ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.h" #include #include #include #include #include #include #include #include #include #include "../androidattachmentreader/androidattachmentreader.h" #include "../rawfileattachmentreader/rawfileattachmentreader.h" #include "../desktopattachmentreader/desktopattachmentreader.h" #include "../msgtypes/msgtypes.h" #include "../protobufparser/protobufparser.h" #include "../reactionlist/reactionlist.h" #include "../groupstatusmessageproto/groupstatusmessageproto.h" #include "../groupv2statusmessageproto/groupv2statusmessageproto.h" #include "../csvreader/csvreader.h" #include "../mimetypes/mimetypes.h" #include "../messagerangeproto/messagerangeproto.h" #include "../autoversion.h" #include "../filesqlitedb/filesqlitedb.h" #include "htmlmessageinfo.h" #include "groupinfo.h" signalbackup-tools-20250313-1/signalbackup/statics.cc000066400000000000000000000353031476450434500223560ustar00rootroot00000000000000/* Copyright (C) 2022-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" /* struct DatabaseLink { std::string table; std::string column; std::vector const connections; { std::string table; std::string column; std::string whereclause = std::string(); std::string json_path = std::string(); int flags = 0; // SET_UNIQUELY unsigned int mindbvversion = 0; unsigned int maxdbvversion = std::numeric_limits::max(); } int flags; // NO_COMPACT, SKIP, WARN }; */ std::vector const SignalBackup::s_databaselinks // static { { "thread", "_id", { {"sms", "thread_id"}, {"mms", "thread_id"}, // \ These are the same {"message", "thread_id"},// / {"drafts", "thread_id"}, {"mention", "thread_id"}, {"name_collision", "thread_id"}, {"chat_folder_membership", "thread_id"} }, NO_COMPACT }, { "sms", "_id", { {"msl_message", "message_id", "is_mms IS NOT 1", "", 0, 0, 167}, // is_mms is 'removed' from table (dbv 168?) {"msl_message", "message_id", "", "", 0, 168}, {"reaction", "message_id", "is_mms IS NOT 1", "", 0, 0, 167}, {"reaction", "message_id", "", "", 0, 168} }, 0 }, { "message", "_id", { {"part", "mid"}, // \ The same {"attachment", "message_id"}, // / {"group_receipts", "mms_id"}, {"mention", "message_id"}, {"msl_message", "message_id", "is_mms IS 1", "", 0, 0, 167}, // is_mms is 'removed' from table (dbv 168?) {"msl_message", "message_id", "", "", 0, 168}, {"reaction", "message_id", "is_mms IS 1", "", 0, 0, 167}, {"reaction", "message_id", "", "", 0, 168}, {"story_sends", "message_id"}, {"call", "message_id"}, {"message", "latest_revision_id"}, {"message", "original_message_id"} }, 0 }, { "mms", "_id", { {"part", "mid"}, // \ The same {"attachment", "message_id"}, // / {"group_receipts", "mms_id"}, {"mention", "message_id"}, {"msl_message", "message_id", "is_mms IS 1", "", 0, 0, 167}, // is_mms is 'removed' from table (dbv 168?) {"msl_message", "message_id", "", "", 0, 168}, {"reaction", "message_id", "is_mms IS 1", "", 0, 0, 167}, {"reaction", "message_id", "", "", 0, 168}, {"story_sends", "message_id"}, {"call", "message_id"} }, 0 }, { "part", "_id", { {"message", "previews", "", "'$[0].attachmentId.rowId'"}, // \ These are the same {"message", "link_previews", "", "'$[0].attachmentId.rowId'"}, // / {"mms", "previews", "", "'$[0].attachmentId.rowId'"}, // \ These are the same {"mms", "link_previews", "", "'$[0].attachmentId.rowId'"} // / }, 0 }, { "attachment", "_id", { {"message", "previews", "", "'$[0].attachmentId.rowId'"}, // \ These are the same {"message", "link_previews", "", "'$[0].attachmentId.rowId'"}, // / {"mms", "previews", "", "'$[0].attachmentId.rowId'"}, // \ These are the same {"mms", "link_previews", "", "'$[0].attachmentId.rowId'"} // / }, 0 }, { "recipient_preferences", // for (very) old databases "_id", {}, NO_COMPACT }, { "recipient", // for (very) old databases "_id", { {"sms", "address"}, // \ These are one {"sms", "recipient_id"}, // / {"message", "address"}, // \ These are one {"message", "recipient_id"}, // / {"message", "from_recipient_id"}, // | Also sort of {"message", "to_recipient_id"}, // / {"message", "quote_author"}, {"mms", "address"}, // \ These are one {"mms", "recipient_id"}, // / {"mms", "quote_author"}, {"sessions", "address"}, {"group_receipts", "address"}, {"thread", "recipient_ids"}, //---\ Only one of these will exist {"thread", "thread_recipient_id"}, // / {"thread", "recipient_id"}, //__/ {"groups", "recipient_id"}, {"remapped_recipients", "old_id"}, // should actually be cleared, but ... {"remapped_recipients", "new_id"}, // this can't hurt {"mention", "recipient_id"}, {"msl_recipient", "recipient_id"}, {"reaction", "author_id"}, {"notification_profile_allowed_members", "recipient_id"}, {"payments", "recipient"}, {"identities", "address", "", "", SET_UNIQUELY}, // identities.address has UNIQUE constraint // when I can assume c++20, sometime in the future, change this to // {.table = "identities", .column = "address', .flags = SET_UNIQUELY} // this is much more explicit and looks cleaner without the empty // fields. (give missing fields default init in header) {"distribution_list", "recipient_id"}, {"distribution_list_member", "recipient_id"}, {"story_sends", "recipient_id"}, {"pending_pni_signature_message", "recipient_id"}, {"call", "peer"}, {"group_membership", "recipient_id", "", "", SET_UNIQUELY}, {"name_collision_membership", "recipient_id"} }, NO_COMPACT }, { "groups", "_id", {}, 0 }, { "identities", "_id", {}, 0 }, { "group_receipts", "_id", {}, 0 }, { "drafts", "_id", {}, 0 }, { "sticker", "_id", {}, 0 }, { "msl_payload", "_id", { {"msl_recipient", "payload_id"}, {"msl_message", "payload_id"} }, 0 }, { "msl_recipient", "_id", {}, 0 }, { "msl_message", "_id", {}, 0 }, { "group_call_ring", "_id", {}, 0 }, { "megaphone", "_id", {}, 0 }, { "remapped_recipients", "_id", {}, 0 }, { "remapped_threads", "_id", {}, 0 }, { "mention", "_id", {}, 0 }, { "reaction", "_id", {}, 0 }, { "notification_profile", "_id", { {"notification_profile_allowed_members", "notification_profile_id"}, {"notification_profile_schedule", "notification_profile_id"} }, 0 }, { "notification_profile_allowed_members", "_id", {}, 0 }, { "notification_profile_schedule", "_id", {}, 0 }, { "payments", "_id", {}, 0 }, { "chat_colors", "", {}, SKIP // deleted in importThread() }, { "push", // this table was dropped around dbv205 "_id", {}, SKIP // cleared in importThread() }, { "storage_key", "_id", {}, 0 //WARN // I have never seen this table not-empty, this link definition may be incomplete (has 'key TEXT UNIQUE' field) // see #76, should be fixed (existing 'key' entries are deleted in importThread() }, { "sender_key_shared", "_id", {}, WARN // I have never seen this table not-empty, this link definition may be incomplete (has 'address' field + multiple UNIQUE) }, { "sender_keys", "_id", {}, WARN // I have never seen this table not-empty, this link definition may be incomplete (has UNIQUE 'address' field) }, { "pending_retry_receipts", "_id", {}, WARN // I have never seen this table not-empty, this link definition may be incomplete (has UNIQUE 'author' field + more) }, { "avatar_picker", "_id", {}, WARN // I have never seen this table not-emptyy, this link definition may be incomplete (has 'group_id' field) }, { "emoji_search", "", {}, SKIP // not sure, but i think this is skipped anyway, also does not seem to have any unique fields }, { "job_spec", "", {}, SKIP // cleared in importthread }, { "constraint_spec", "", {}, SKIP // cleared in importthread }, { "dependency_spec", "", {}, SKIP // cleared in importthread }, { "distribution_list", /// WORK IN PROGRESS? other fields of distr_list are also unique: distibution_id, recipient_id "_id", { {"recipient", "distribution_list_id"}, {"distribution_list_member", "list_id"} //{d_mms_table,"parent_story_id"}??? //distribution_id TEXT UNIQUE NOT NULL }, 0 }, { "distribution_list_member", "_id", {}, 0 }, { "donation_receipt", "_id", {}, 0 }, { "story_sends", "_id", //distribution_id TEXT NOT NULL REFERENCES distribution_list (distribution_id) ON DELETE CASCADE {}, 0 }, { "key_value", "_id", {}, 0 }, { // NOTE e164 is also UNIQUE, remove doubles (oldeest?) beforehand "cds", // 'Caontact Discovery Service (v2)??' "_id", {}, 0 }, { // Remove double (UNIQUE) uuid beforehand "remote_megaphone", "_id", {}, 0 }, { "pending_pni_signature_message", "_id", {}, 0 }, { "call", "_id", {}, 0 }, { "group_membership", "_id", {}, 0 }, { "call_link", "_id", { {"call", "call_link"} }, 0 }, { "kyber_prekey", "_id", {}, 0 }, // { // "kyber_prekey", // "key_id", // {}, // 0 // }, { "name_collision", "_id", { {"name_collision_membership", "collision_id"} }, 0 }, { "name_collision_membership", "_id", {}, 0 }, { "in_app_payment", "_id", {}, 0 }, { "in_app_payment_subscriber", "_id", {}, 0 }, { "chat_folder", "_id", { {"chat_folder_membership", "chat_folder_id"} }, 0 }, { "chat_folder_membership", "_id", {}, 0 }, { "backup_media_snapshot", "_id", {}, 0 } }; // in table FIRST, SECOND[n] used to be known as SECOND[n+1] std::map>> const SignalBackup::s_columnaliases //static { std::make_pair("thread", std::vector>{{"recipient_id", "thread_recipient_id", "recipient_ids"}, {"meaningful_messages", "message_count"}, {"has_delivery_receipt", "delivery_receipt_count"}, {"has_read_receipt", "read_receipt_count"}, {"pinned_order", "pinned"}}), std::make_pair("recipient", std::vector>{{"aci", "uuid"}, {"e164", "phone"}, {"avatar_color", "color"}, {"system_joined_name", "system_display_name"}, {"profile_given_name", "signal_profile_name"}, {"storage_service_id", "storage_service_key"}, {"type", "group_type"}, {"sealed_sender_mode", "unidentified_access_mode"}, {"profile_avatar", "signal_profile_avatar"}}), std::make_pair("sms", std::vector>{{"date_received", "date"}, {"recipient_id", "address"}, {"recipient_device_id", "address_device_id"}}), std::make_pair("message", std::vector>{{"has_delivery_receipt", "delivery_receipt_count"}, {"has_read_receipt", "read_receipt_count"}, {"viewed", "viewed_receipt_count"}, {"date_sent", "date"}, {"message_ranges", "ranges"}, {"from_recipient_id", "recipient_id", "address"}, {"from_device_id", "recipient_device_id", "address_device_id"}, {"type", "msg_box"}, {"link_previews", "previews"}}), std::make_pair("mms", std::vector>{{"has_delivery_receipt", "delivery_receipt_count"}, {"has_read_receipt", "read_receipt_count"}, {"viewed", "viewed_receipt_count"}, {"date_sent", "date"}, {"from_recipient_id", "recipient_id", "address"}, {"from_device_id", "recipient_device_id", "address_device_id"}, {"type", "msg_box"}, {"link_previews", "previews"}}), std::make_pair("groups", std::vector>{{"unmigrated_v1_members", "former_v1_members"}, {"display_as_story", "show_as_story_state"}}), std::make_pair("attachment", std::vector>{{"message_id", "mid"}, {"content_type", "ct"}, {"transfer_state", "pending_push"}, {"remote_key", "cd"}, {"remote_location", "cl"}}) }; signalbackup-tools-20250313-1/signalbackup/statics_emoji.cc000066400000000000000000014527341476450434500235550ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" // this list is generated from https://unicode.org/Public/emoji/latest/emoji-test.txt char const *const SignalBackup::s_emoji_unicode_list[3781] = {"\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\x8f\xb4\xf3\xa0\x81\xa7\xf3\xa0\x81\xa2\xf3\xa0\x81\xb7\xf3\xa0\x81\xac\xf3\xa0\x81\xb3\xf3\xa0\x81\xbf", "\xf0\x9f\x8f\xb4\xf3\xa0\x81\xa7\xf3\xa0\x81\xa2\xf3\xa0\x81\xb3\xf3\xa0\x81\xa3\xf3\xa0\x81\xb4\xf3\xa0\x81\xbf", "\xf0\x9f\x8f\xb4\xf3\xa0\x81\xa7\xf3\xa0\x81\xa2\xf3\xa0\x81\xa5\xf3\xa0\x81\xae\xf3\xa0\x81\xa7\xf3\xa0\x81\xbf", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa9", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\x8b\xe2\x80\x8d\xf0\x9f\x91\xa8", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa7\x92\xe2\x80\x8d\xf0\x9f\xa7\x92", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa9", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x91\xa8", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xaf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbe", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbe", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbe", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa7\x92\xe2\x80\x8d\xf0\x9f\xa7\x92", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa4\x9d\xe2\x80\x8d\xf0\x9f\xa7\x91", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa7\x92", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\x81\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x97\xa8\xef\xb8\x8f", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\xb3\xef\xb8\x8f\xe2\x80\x8d\xe2\x9a\xa7\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8c\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8c\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xe2\x9b\xb9\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\x84", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\x84", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\x84", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\x84", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\x84", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xe2\x9b\xb9\xef\xb8\x8f\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x8f\xb3\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x8c\x88", "\xf0\x9f\x98\xb6\xe2\x80\x8d\xf0\x9f\x8c\xab\xef\xb8\x8f", "\xf0\x9f\xa4\xb8\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8f\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\xb4\xe2\x80\x8d\xe2\x98\xa0\xef\xb8\x8f", "\xf0\x9f\xa7\x96\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x87\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb5\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\xa7\x9a\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8d\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x90\xbb\xe2\x80\x8d\xe2\x9d\x84\xef\xb8\x8f", "\xf0\x9f\x91\xaf\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xe2\x9b\x93\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x92\xa5", "\xf0\x9f\xa4\xb7\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x99\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x99\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb4\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9f\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9a\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9f\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xa6\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x97\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x94\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\x99\x8b\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\xa7\x97\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb8\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8b\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb4\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xe2\x9c\x88\xef\xb8\x8f", "\xf0\x9f\xa6\xb9\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x87\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xe2\x9a\x95\xef\xb8\x8f", "\xf0\x9f\xa4\xa6\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x94\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb8\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa6\xb9\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9d\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9d\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9c\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9c\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9e\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x96\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb7\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9b\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x9b\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x9e\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb5\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xbd\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8d\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb5\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8d\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8d\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb5\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x85\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xa3\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x8e\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\xa4\xbe\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\xa9\xb9", "\xf0\x9f\xa4\xbe\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x83\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xe2\x9d\xa4\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x94\xa5", "\xf0\x9f\xa7\x98\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xa3\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x82\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb1\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb3\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x85\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb3\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x82\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa4\xb9\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb1\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x99\x8e\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x8e\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb9\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\xa7\x98\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xae\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb0\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xbc\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xb7\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xbc\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x86\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\xa4\xb8\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x81\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb7\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x86\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x81\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x84\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\x8f\x84\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xaf\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xe2\x9a\x96\xef\xb8\x8f", "\xf0\x9f\xa7\x8f\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xe2\x80\x8d\xe2\x9e\xa1\xef\xb8\x8f", "\xf0\x9f\x99\x86\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x91\xb0\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x9a\xb6\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x82\xe2\x80\x8d\xe2\x86\x95\xef\xb8\x8f", "\xf0\x9f\x99\x86\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x8f\x8a\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x91\xae\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x99\x82\xe2\x80\x8d\xe2\x86\x94\xef\xb8\x8f", "\xf0\x9f\xa4\xbd\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8f\x8a\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x92\x87\xe2\x80\x8d\xe2\x99\x80\xef\xb8\x8f", "\xf0\x9f\x92\x87\xe2\x80\x8d\xe2\x99\x82\xef\xb8\x8f", "\xf0\x9f\x8d\x8b\xe2\x80\x8d\xf0\x9f\x9f\xa9", "\xf0\x9f\x90\xa6\xe2\x80\x8d\xf0\x9f\x94\xa5", "\xf0\x9f\x8d\x84\xe2\x80\x8d\xf0\x9f\x9f\xab", "\xf0\x9f\x90\x95\xe2\x80\x8d\xf0\x9f\xa6\xba", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xaf", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\x98\xb5\xe2\x80\x8d\xf0\x9f\x92\xab", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xbd", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\x98\xae\xe2\x80\x8d\xf0\x9f\x92\xa8", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xbc", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8d\xbc", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb2", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8e\xa8", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8e\x84", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xb3", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x9a\x80", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8f\xab", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8c\xbe", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x9a\x92", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa7\x92", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8f\xad", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xb0", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa7", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x92\xbc", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\xa6\xb1", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x94\xac", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x94\xa7", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x92\xbb", "\xf0\x9f\xa7\x91\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8d\xb3", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x8e\x93", "\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x8e\xa4", "\xf0\x9f\x90\x88\xe2\x80\x8d\xe2\xac\x9b", "\xf0\x9f\x90\xa6\xe2\x80\x8d\xe2\xac\x9b", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xab", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xb0", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xb1", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xab\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xab\xf0\x9f\x8f\xbd", "\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbb", "\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbc", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xad", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xa6", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xb2", "\xf0\x9f\x87\xb4\xf0\x9f\x87\xb2", "\xf0\x9f\x87\xb3\xf0\x9f\x87\xbf", "\xf0\x9f\x87\xb3\xf0\x9f\x87\xba", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xb3", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xb4", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xac", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xad", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xac", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xab", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xb0", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xb1", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xb2", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xb3", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xaa", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x98\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xab\xf0\x9f\x8f\xbf", "\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xac\xf0\x9f\x8f\xbb", "\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbf", "\xf0\x9f\x87\xb3\xf0\x9f\x87\xb1", "\xf0\x9f\x87\xb3\xf0\x9f\x87\xae", "\xf0\x9f\x87\xb3\xf0\x9f\x87\xac", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xbc", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xbd", "\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbd", "\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xab\xf0\x9f\x8f\xbb", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xbb", "\xf0\x9f\x91\xad\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xad\xf0\x9f\x8f\xbe", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xbe", "\xf0\x9f\x87\xb3\xf0\x9f\x87\xab", "\xf0\x9f\x87\xb3\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xb3\xf0\x9f\x87\xa8", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xbf", "\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbb", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xb5", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xb6", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xb7", "\xf0\x9f\x91\xad\xf0\x9f\x8f\xbd", "\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xab\xf0\x9f\x8f\xbc", "\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbe", "\xf0\x9f\x9b\x80\xf0\x9f\x8f\xbf", "\xf0\x9f\x87\xb3\xf0\x9f\x87\xa6", "\xf0\x9f\x87\xb3\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xb3\xf0\x9f\x87\xb5", "\xf0\x9f\x9b\x8c\xf0\x9f\x8f\xbc", "\xf0\x9f\x87\xb3\xf0\x9f\x87\xb4", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xb8", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xb9", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xba", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xb9", "\xf0\x9f\x87\xbb\xf0\x9f\x87\xa6", "\xf0\x9f\x87\xba\xf0\x9f\x87\xbf", "\xf0\x9f\x87\xba\xf0\x9f\x87\xbe", "\xf0\x9f\x87\xba\xf0\x9f\x87\xb8", "\xf0\x9f\x87\xba\xf0\x9f\x87\xb3", "\xf0\x9f\x87\xba\xf0\x9f\x87\xb2", "\xf0\x9f\x87\xba\xf0\x9f\x87\xac", "\xf0\x9f\x87\xba\xf0\x9f\x87\xa6", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xbf", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xbc", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xbb", "\xf0\x9f\x87\xbb\xf0\x9f\x87\xa8", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xb4", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xb3", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xb2", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xad\xf0\x9f\x8f\xbc", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbc", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbd", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbe", "\xf0\x9f\x8f\x84\xf0\x9f\x8f\xbf", "\xf0\x9f\x87\xbf\xf0\x9f\x87\xa6", "\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbd", "\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbe", "\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbf", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbb", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbc", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbd", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbe", "\xf0\x9f\x8f\x8c\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xad\xf0\x9f\x8f\xbb", "\xf0\x9f\x87\xbf\xf0\x9f\x87\xbc", "\xf0\x9f\x87\xbf\xf0\x9f\x87\xb2", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbd", "\xf0\x9f\x87\xbe\xf0\x9f\x87\xb9", "\xf0\x9f\x87\xbe\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xbd\xf0\x9f\x87\xb0", "\xf0\x9f\x87\xbc\xf0\x9f\x87\xb8", "\xf0\x9f\x87\xbc\xf0\x9f\x87\xab", "\xf0\x9f\x87\xbb\xf0\x9f\x87\xba", "\xf0\x9f\x87\xbb\xf0\x9f\x87\xb3", "\xf0\x9f\x87\xbb\xf0\x9f\x87\xae", "\xf0\x9f\x87\xbb\xf0\x9f\x87\xac", "\xf0\x9f\x87\xbb\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xb7\xf0\x9f\x87\xba", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xb0", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xaf", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xae", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xad", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xac", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xa9", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xa8", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xa7", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xa6", "\xf0\x9f\x87\xb7\xf0\x9f\x87\xbc", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xb1", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbe", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbf", "\xf0\x9f\x87\xb7\xf0\x9f\x87\xb8", "\xf0\x9f\x87\xb7\xf0\x9f\x87\xb4", "\xf0\x9f\x87\xb7\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xb6\xf0\x9f\x87\xa6", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xbe", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xbc", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xb9", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xb8", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbd", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xb1", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xb0", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xaf", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xad", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xac", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xab", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xa9", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xa8", "\xf0\x9f\x87\xb9\xf0\x9f\x87\xa6", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbb", "\xf0\x9f\x9a\xa3\xf0\x9f\x8f\xbc", "\xf0\x9f\x87\xb5\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xbf", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xbe", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xbd", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xbb", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xb9", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xb8", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xb4", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xb3", "\xf0\x9f\x87\xb8\xf0\x9f\x87\xb2", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xbf", "\xf0\x9f\x91\xac\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb8\xf0\x9f\x8f\xbb", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbf", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbe", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xab", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xa9", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xa8", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xa6", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xac", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbd", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xbe", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xbc", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xbb", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xb9", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xb8", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbc", "\xf0\x9f\x9a\xb5\xf0\x9f\x8f\xbb", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbb", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbc", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbd", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xba", "\xf0\x9f\x87\xa9\xf0\x9f\x87\xb0", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbe", "\xf0\x9f\x87\xa9\xf0\x9f\x87\xaf", "\xf0\x9f\x87\xa9\xf0\x9f\x87\xac", "\xf0\x9f\x87\xa9\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xbf", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xbe", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbc", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xbd", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xbc", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xbb", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xb6", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xb5", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xb4", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xb3", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xb2", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbb", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xb1", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xb0", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xae", "\xf0\x9f\x87\xa8\xf0\x9f\x87\xad", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xa9", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xb8", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xb6", "\xf0\x9f\x92\x8f\xf0\x9f\x8f\xbe", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xb4", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xb2", "\xf0\x9f\x92\x8f\xf0\x9f\x8f\xbf", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xb1", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xae", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xac", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xab", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xb9", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xa8", "\xf0\x9f\x92\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\x92\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\x92\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbf", "\xf0\x9f\x92\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\x92\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbb", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbc", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbd", "\xf0\x9f\x9a\xb4\xf0\x9f\x8f\xbe", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbf", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xb6", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xb4", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xb3", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xb2", "\xf0\x9f\x91\xac\xf0\x9f\x8f\xbf", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xb1", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xaf", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xae", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xad", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xac", "\xf0\x9f\x92\x8f\xf0\x9f\x8f\xbb", "\xf0\x9f\x8f\x8b\xf0\x9f\x8f\xbe", "\xf0\x9f\x87\xa9\xf0\x9f\x87\xb2", "\xf0\x9f\x92\x8f\xf0\x9f\x8f\xbc", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xab", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xa9", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xa7", "\xf0\x9f\x87\xa7\xf0\x9f\x87\xa6", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xbf", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xbd", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xbc", "\xf0\x9f\x92\x8f\xf0\x9f\x8f\xbd", "\xf0\x9f\x87\xa6\xf0\x9f\x87\xba", "\xf0\x9f\x87\xae\xf0\x9f\x87\xb9", "\xf0\x9f\x87\xb0\xf0\x9f\x87\xb3", "\xf0\x9f\x87\xb0\xf0\x9f\x87\xb2", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbf", "\xf0\x9f\x87\xb0\xf0\x9f\x87\xae", "\xf0\x9f\x87\xb0\xf0\x9f\x87\xad", "\xf0\x9f\x87\xb0\xf0\x9f\x87\xac", "\xf0\x9f\x87\xb0\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xaf\xf0\x9f\x87\xb5", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbe", "\xf0\x9f\x87\xaf\xf0\x9f\x87\xb4", "\xf0\x9f\x87\xaf\xf0\x9f\x87\xb2", "\xf0\x9f\x87\xaf\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xb0\xf0\x9f\x87\xb5", "\xf0\x9f\x87\xae\xf0\x9f\x87\xb8", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbd", "\xf0\x9f\x87\xae\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xae\xf0\x9f\x87\xb6", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbc", "\xf0\x9f\x87\xae\xf0\x9f\x87\xb4", "\xf0\x9f\x87\xae\xf0\x9f\x87\xb3", "\xf0\x9f\x87\xae\xf0\x9f\x87\xb2", "\xf0\x9f\x87\xae\xf0\x9f\x87\xb1", "\xf0\x9f\x87\xae\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xae\xf0\x9f\x87\xa9", "\xf0\x9f\x87\xb1\xf0\x9f\x87\xb9", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xa9", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xa8", "\xf0\x9f\x87\xb2\xf0\x9f\x87\xa6", "\xf0\x9f\x87\xb1\xf0\x9f\x87\xbe", "\xf0\x9f\x87\xb1\xf0\x9f\x87\xbb", "\xf0\x9f\x87\xb1\xf0\x9f\x87\xba", "\xf0\x9f\x91\xac\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb9\xf0\x9f\x8f\xbb", "\xf0\x9f\x87\xae\xf0\x9f\x87\xa8", "\xf0\x9f\x87\xb1\xf0\x9f\x87\xb8", "\xf0\x9f\x87\xb1\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xb1\xf0\x9f\x87\xb0", "\xf0\x9f\x87\xb1\xf0\x9f\x87\xae", "\xf0\x9f\x87\xb1\xf0\x9f\x87\xa8", "\xf0\x9f\x87\xb1\xf0\x9f\x87\xa7", "\xf0\x9f\x87\xb1\xf0\x9f\x87\xa6", "\xf0\x9f\x87\xb0\xf0\x9f\x87\xbf", "\xf0\x9f\x87\xb0\xf0\x9f\x87\xbe", "\xf0\x9f\x87\xb0\xf0\x9f\x87\xbc", "\xf0\x9f\x87\xb0\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xaa\xf0\x9f\x87\xba", "\xf0\x9f\x87\xac\xf0\x9f\x87\xac", "\xf0\x9f\x87\xac\xf0\x9f\x87\xab", "\xf0\x9f\x87\xac\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xac\xf0\x9f\x87\xa9", "\xf0\x9f\x87\xac\xf0\x9f\x87\xa7", "\xf0\x9f\x87\xac\xf0\x9f\x87\xa6", "\xf0\x9f\x87\xab\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xab\xf0\x9f\x87\xb4", "\xf0\x9f\x87\xab\xf0\x9f\x87\xb2", "\xf0\x9f\x87\xab\xf0\x9f\x87\xb0", "\xf0\x9f\x87\xab\xf0\x9f\x87\xaf", "\xf0\x9f\x87\xab\xf0\x9f\x87\xae", "\xf0\x9f\x87\xac\xf0\x9f\x87\xad", "\xf0\x9f\x87\xaa\xf0\x9f\x87\xb9", "\xf0\x9f\x87\xaa\xf0\x9f\x87\xb8", "\xf0\x9f\xa4\xbd\xf0\x9f\x8f\xbf", "\xf0\x9f\x87\xaa\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xaa\xf0\x9f\x87\xad", "\xf0\x9f\x87\xaa\xf0\x9f\x87\xac", "\xf0\x9f\x87\xaa\xf0\x9f\x87\xaa", "\xf0\x9f\x87\xaa\xf0\x9f\x87\xa8", "\xf0\x9f\x87\xaa\xf0\x9f\x87\xa6", "\xf0\x9f\x87\xa9\xf0\x9f\x87\xbf", "\xf0\x9f\x87\xa9\xf0\x9f\x87\xb4", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\xbe\xf0\x9f\x8f\xbb", "\xf0\x9f\x87\xad\xf0\x9f\x87\xba", "\xf0\x9f\x87\xad\xf0\x9f\x87\xb9", "\xf0\x9f\x87\xad\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xad\xf0\x9f\x87\xb3", "\xf0\x9f\x87\xad\xf0\x9f\x87\xb2", "\xf0\x9f\x87\xad\xf0\x9f\x87\xb0", "\xf0\x9f\x87\xac\xf0\x9f\x87\xbe", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbb", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbc", "\xf0\x9f\x8f\x8a\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xac\xf0\x9f\x8f\xbc", "\xf0\x9f\x87\xac\xf0\x9f\x87\xbc", "\xf0\x9f\x87\xac\xf0\x9f\x87\xba", "\xf0\x9f\x87\xac\xf0\x9f\x87\xb9", "\xf0\x9f\x87\xac\xf0\x9f\x87\xb8", "\xf0\x9f\x87\xac\xf0\x9f\x87\xb7", "\xf0\x9f\x87\xac\xf0\x9f\x87\xb6", "\xf0\x9f\x87\xac\xf0\x9f\x87\xb5", "\xf0\x9f\x87\xac\xf0\x9f\x87\xb3", "\xf0\x9f\x87\xac\xf0\x9f\x87\xb2", "\xf0\x9f\x87\xac\xf0\x9f\x87\xb1", "\xf0\x9f\x87\xac\xf0\x9f\x87\xae", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbb", "\xf0\x9f\x96\x96\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xb1\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x91\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa7\xf0\x9f\x8f\xbb", "\xf0\x9f\x96\x96\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x94\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb", "\xf0\x9f\x96\x96\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x83\xf0\x9f\x8f\xbb", "\xf0\x9f\x96\x90\xf0\x9f\x8f\xbf", "\xf0\x9f\x96\x90\xf0\x9f\x8f\xbe", "\xf0\x9f\x96\x90\xf0\x9f\x8f\xbd", "\xf0\x9f\x96\x90\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x83\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x83\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\x83\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x83\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbc", "\xf0\x9f\x96\x90\xf0\x9f\x8f\xbb", "\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbf", "\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbe", "\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbd", "\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbc", "\xf0\x9f\xa6\xbb\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x82\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xa6\xf0\x9f\x8f\xbb", "\xf0\x9f\x96\x96\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x92\xf0\x9f\x8f\xbb", "\xf0\x9f\x96\x96\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xb6\xf0\x9f\x8f\xbb", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbe", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb3\xf0\x9f\x8f\xbb", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbf", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbe", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbd", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbc", "\xf0\x9f\x92\x81\xf0\x9f\x8f\xbb", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbf", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbd", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbd", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbf", "\xf0\x9f\x99\x86\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbe", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbf", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbe", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbd", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbf", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbe", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb3\xf0\x9f\x8f\xbe", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbc", "\xf0\x9f\x99\x87\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb3\xf0\x9f\x8f\xbd", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x8f\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb3\xf0\x9f\x8f\xbc", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbf", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xb4\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x93\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb1\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xa9\xf0\x9f\x8f\xbe", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbe", "\xf0\x9f\x99\x85\xf0\x9f\x8f\xbb", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbf", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbe", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbd", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbc", "\xf0\x9f\x99\x8e\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbd", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x82\xf0\x9f\x8f\xbe", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbd", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbc", "\xf0\x9f\x99\x8d\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb2\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xb5\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x8e\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x8d\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x8f\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb5\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x9c\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\x9b\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x8a\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x89\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x86\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x86\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\x86\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x86\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x86\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x89\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x89\xf0\x9f\x8f\xbe", "\xf0\x9f\x96\x95\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x89\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x89\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x88\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x88\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x88\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\x88\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x88\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x87\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb5\xf0\x9f\x8f\xbe", "\xf0\x9f\xab\xb5\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb5\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\xb5\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x87\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x87\xf0\x9f\x8f\xbe", "\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x87\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x87\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x99\xf0\x9f\x8f\xbe", "\xf0\x9f\x96\x95\xf0\x9f\x8f\xbf", "\xf0\x9f\x96\x95\xf0\x9f\x8f\xbe", "\xf0\x9f\x96\x95\xf0\x9f\x8f\xbd", "\xf0\x9f\x96\x95\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbb", "\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbf", "\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbe", "\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbd", "\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbc", "\xf0\x9f\x92\xaa\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb3\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbe", "\xf0\x9f\x92\x85\xf0\x9f\x8f\xbf", "\xf0\x9f\x92\x85\xf0\x9f\x8f\xbe", "\xf0\x9f\x92\x85\xf0\x9f\x8f\xbd", "\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x82\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x82\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x82\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbf", "\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbf", "\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbe", "\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbd", "\xf0\x9f\xa6\xb6\xf0\x9f\x8f\xbc", "\xf0\x9f\x92\x85\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbe", "\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbf", "\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbe", "\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbd", "\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbc", "\xf0\x9f\xa6\xb5\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x9a\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb6\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x90\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x90\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\x90\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x90\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x90\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb6\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb6\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb6\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\xb6\xf0\x9f\x8f\xbb", "\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbf", "\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbe", "\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbd", "\xf0\x9f\x99\x8c\xf0\x9f\x8f\xbc", "\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbb", "\xf0\x9f\x92\x85\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\x8b\xf0\x9f\x8f\xbb", "\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbf", "\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbe", "\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbd", "\xf0\x9f\x99\x8f\xf0\x9f\x8f\xbc", "\xf0\x9f\x99\x8b\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\x9d\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x9d\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\x9d\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\x9d\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\x9d\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\xb2\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x9b\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x9a\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x9c\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x9d\xf0\x9f\x8f\xbf", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbb", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbc", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbd", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbe", "\xf0\x9f\x92\x86\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbc", "\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb6\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbd", "\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbf", "\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbe", "\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbc", "\xf0\x9f\x8e\x85\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbd", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbb", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbc", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbd", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\x8c\xf0\x9f\x8f\xbf", "\xf0\x9f\xa6\xb8\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbb", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbb", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbc", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbd", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbe", "\xf0\x9f\xa6\xb9\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x8f\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x99\xf0\x9f\x8f\xbc", "\xf0\x9f\x95\xba\xf0\x9f\x8f\xbc", "\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbf", "\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbe", "\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbd", "\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbc", "\xf0\x9f\x95\xb4\xf0\x9f\x8f\xbb", "\xf0\x9f\x95\xba\xf0\x9f\x8f\xbf", "\xf0\x9f\x95\xba\xf0\x9f\x8f\xbe", "\xf0\x9f\x95\xba\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbf", "\xf0\x9f\x95\xba\xf0\x9f\x8f\xbb", "\xf0\x9f\x92\x83\xf0\x9f\x8f\xbf", "\xf0\x9f\x92\x83\xf0\x9f\x8f\xbe", "\xf0\x9f\x92\x83\xf0\x9f\x8f\xbd", "\xf0\x9f\x92\x83\xf0\x9f\x8f\xbc", "\xf0\x9f\x92\x83\xf0\x9f\x8f\xbb", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbf", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x96\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x97\xf0\x9f\x8f\xbf", "\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbb", "\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbc", "\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbd", "\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbe", "\xf0\x9f\x8f\x87\xf0\x9f\x8f\xbf", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb0\xf0\x9f\x8f\xbd", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbf", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbe", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbd", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbc", "\xf0\x9f\x9a\xb6\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb0\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\xb0\xf0\x9f\x8f\xbe", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbe", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbd", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbc", "\xf0\x9f\x92\x87\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb0\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x9e\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb0\xf0\x9f\x8f\xbf", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x8d\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbe", "\xf0\x9f\xa7\x8e\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x9f\xf0\x9f\x8f\xbc", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbb", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbc", "\xf0\x9f\x8f\x83\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbd", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbc", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\xb7\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb4\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xb8\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\xb7\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb7\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xb3\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\xb7\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xae\xf0\x9f\x8f\xbb", "\xf0\x9f\x8f\x82\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbe", "\xf0\x9f\xa5\xb7\xf0\x9f\x8f\xbe", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbc", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb4\xf0\x9f\x8f\xbc", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbb", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbe", "\xf0\x9f\x92\x82\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\x98\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb4\xf0\x9f\x8f\xbd", "\xf0\x9f\xa5\xb7\xf0\x9f\x8f\xbb", "\xf0\x9f\xa5\xb7\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\xb4\xf0\x9f\x8f\xbe", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbf", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbe", "\xf0\x9f\xa5\xb7\xf0\x9f\x8f\xbd", "\xf0\x9f\x95\xb5\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbb", "\xf0\x9f\xa5\xb7\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\xb4\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xb7\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\x85\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\x85\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\x85\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\x85\xf0\x9f\x8f\xbe", "\xf0\x9f\xab\x85\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\xb7\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\xb4\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\x83\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\x84\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\x84\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\x83\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\x83\xf0\x9f\x8f\xbe", "\xf0\x9f\xab\x83\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\x84\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\x83\xf0\x9f\x8f\xbb", "\xf0\x9f\xab\xb3\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xb2\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\xb0\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\x84\xf0\x9f\x8f\xbe", "\xf0\x9f\xab\x84\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xb1\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\x8c\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\xa6\xf0\x9f\x8f\xbb", "\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbc", "\xf0\x9f\x91\xbc\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbf", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbe", "\xf0\x9f\xab\xb8\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbd", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb7\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbb", "\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbc", "\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbd", "\xf0\x9f\xa7\x95\xf0\x9f\x8f\xbe", "\xf0\x9f\xab\xb8\xf0\x9f\x8f\xbf", "\xf0\x9f\xab\xb8\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbb", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbc", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbe", "\xf0\x9f\xa4\xb5\xf0\x9f\x8f\xbf", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbd", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbc", "\xf0\x9f\xab\xb8\xf0\x9f\x8f\xbd", "\xf0\x9f\xab\xb8\xf0\x9f\x8f\xbe", "\xf0\x9f\x91\xb0\xf0\x9f\x8f\xbb", "\xf0\x9f\x97\xa3\xef\xb8\x8f", "\x2a\xef\xb8\x8f\xe2\x83\xa3", "\xf0\x9f\x97\x9d\xef\xb8\x8f", "\xf0\x9f\x97\x82\xef\xb8\x8f", "\x23\xef\xb8\x8f\xe2\x83\xa3", "\x30\xef\xb8\x8f\xe2\x83\xa3", "\x34\xef\xb8\x8f\xe2\x83\xa3", "\xf0\x9f\x9b\xa0\xef\xb8\x8f", "\xf0\x9f\x97\x84\xef\xb8\x8f", "\xf0\x9f\x97\x83\xef\xb8\x8f", "\xf0\x9f\x95\xb0\xef\xb8\x8f", "\xf0\x9f\x97\xa1\xef\xb8\x8f", "\xf0\x9f\x9b\xa3\xef\xb8\x8f", "\xf0\x9f\x97\x91\xef\xb8\x8f", "\xf0\x9f\x8c\xb6\xef\xb8\x8f", "\x33\xef\xb8\x8f\xe2\x83\xa3", "\x32\xef\xb8\x8f\xe2\x83\xa3", "\xf0\x9f\x9b\xa2\xef\xb8\x8f", "\xf0\x9f\x96\x8a\xef\xb8\x8f", "\xe2\x9c\x8c\xf0\x9f\x8f\xbd", "\xf0\x9f\x95\xb8\xef\xb8\x8f", "\xf0\x9f\x95\xb3\xef\xb8\x8f", "\xf0\x9f\x95\xb7\xef\xb8\x8f", "\xe2\x9c\x8c\xf0\x9f\x8f\xbc", "\xf0\x9f\x96\x8c\xef\xb8\x8f", "\xf0\x9f\x97\xa8\xef\xb8\x8f", "\xf0\x9f\x97\xaf\xef\xb8\x8f", "\xf0\x9f\x96\x8d\xef\xb8\x8f", "\xe2\x9c\x8c\xf0\x9f\x8f\xbe", "\xe2\x9c\x8c\xf0\x9f\x8f\xbb", "\xf0\x9f\x97\x93\xef\xb8\x8f", "\xf0\x9f\x96\x90\xef\xb8\x8f", "\xe2\x9c\x8b\xf0\x9f\x8f\xbf", "\xe2\x9c\x8b\xf0\x9f\x8f\xbe", "\xe2\x9c\x8b\xf0\x9f\x8f\xbd", "\xe2\x9c\x8b\xf0\x9f\x8f\xbc", "\xf0\x9f\x90\xbf\xef\xb8\x8f", "\xe2\x9c\x8b\xf0\x9f\x8f\xbb", "\xf0\x9f\x8c\xa8\xef\xb8\x8f", "\xf0\x9f\x9b\xa1\xef\xb8\x8f", "\xf0\x9f\x9b\xa4\xef\xb8\x8f", "\xf0\x9f\x8f\xb3\xef\xb8\x8f", "\xf0\x9f\x97\xb3\xef\xb8\x8f", "\xf0\x9f\x8c\xac\xef\xb8\x8f", "\xf0\x9f\x8c\xab\xef\xb8\x8f", "\xf0\x9f\x8c\xaa\xef\xb8\x8f", "\xf0\x9f\x95\x8a\xef\xb8\x8f", "\xf0\x9f\x8c\xa9\xef\xb8\x8f", "\x31\xef\xb8\x8f\xe2\x83\xa3", "\xf0\x9f\x96\x87\xef\xb8\x8f", "\xf0\x9f\x8c\xa7\xef\xb8\x8f", "\xf0\x9f\x8c\xa6\xef\xb8\x8f", "\xf0\x9f\x8c\xa1\xef\xb8\x8f", "\xf0\x9f\x8c\xa5\xef\xb8\x8f", "\xf0\x9f\x8f\xb5\xef\xb8\x8f", "\xf0\x9f\x8c\xa4\xef\xb8\x8f", "\xf0\x9f\x96\x8b\xef\xb8\x8f", "\xf0\x9f\x97\x92\xef\xb8\x8f", "\xe2\x9c\x8c\xf0\x9f\x8f\xbf", "\x35\xef\xb8\x8f\xe2\x83\xa3", "\xf0\x9f\x8e\x9e\xef\xb8\x8f", "\xf0\x9f\x93\xbd\xef\xb8\x8f", "\xf0\x9f\x8f\x9c\xef\xb8\x8f", "\xf0\x9f\x8f\x96\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbf", "\xe2\x9b\xb9\xf0\x9f\x8f\xbe", "\xf0\x9f\x8f\x95\xef\xb8\x8f", "\xf0\x9f\x88\x82\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbd", "\xf0\x9f\x8f\x94\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbc", "\xf0\x9f\x88\xb7\xef\xb8\x8f", "\xf0\x9f\x95\xaf\xef\xb8\x8f", "\xf0\x9f\x8f\x9d\xef\xb8\x8f", "\xf0\x9f\x97\xba\xef\xb8\x8f", "\xf0\x9f\x91\x81\xef\xb8\x8f", "\xe2\x9b\xb9\xf0\x9f\x8f\xbb", "\xf0\x9f\x95\x89\xef\xb8\x8f", "\xf0\x9f\x8f\x99\xef\xb8\x8f", "\xf0\x9f\x9b\x8d\xef\xb8\x8f", "\xf0\x9f\x8d\xbd\xef\xb8\x8f", "\xf0\x9f\x9b\xa5\xef\xb8\x8f", "\xf0\x9f\x9b\xb0\xef\xb8\x8f", "\xf0\x9f\x8f\x8e\xef\xb8\x8f", "\xf0\x9f\x8f\x8d\xef\xb8\x8f", "\xf0\x9f\x9b\x8b\xef\xb8\x8f", "\xf0\x9f\x8f\x9a\xef\xb8\x8f", "\xf0\x9f\x95\xb6\xef\xb8\x8f", "\x37\xef\xb8\x8f\xe2\x83\xa3", "\xf0\x9f\x85\xbf\xef\xb8\x8f", "\xf0\x9f\x96\xbc\xef\xb8\x8f", "\x38\xef\xb8\x8f\xe2\x83\xa3", "\xf0\x9f\x8e\x9b\xef\xb8\x8f", "\xf0\x9f\x8e\x9a\xef\xb8\x8f", "\xf0\x9f\x8e\x99\xef\xb8\x8f", "\xf0\x9f\x85\xbe\xef\xb8\x8f", "\x39\xef\xb8\x8f\xe2\x83\xa3", "\xf0\x9f\x9b\xa9\xef\xb8\x8f", "\xf0\x9f\x8f\x8b\xef\xb8\x8f", "\xf0\x9f\x9b\x8e\xef\xb8\x8f", "\xf0\x9f\x8f\x98\xef\xb8\x8f", "\xf0\x9f\x95\xb5\xef\xb8\x8f", "\xf0\x9f\x96\xa5\xef\xb8\x8f", "\xf0\x9f\x95\xb9\xef\xb8\x8f", "\xf0\x9f\x96\xa8\xef\xb8\x8f", "\xf0\x9f\x96\xb1\xef\xb8\x8f", "\xf0\x9f\x96\xb2\xef\xb8\x8f", "\x36\xef\xb8\x8f\xe2\x83\xa3", "\xf0\x9f\x8f\x97\xef\xb8\x8f", "\xf0\x9f\x8f\x9b\xef\xb8\x8f", "\xf0\x9f\x8f\x9f\xef\xb8\x8f", "\xf0\x9f\x8f\x9e\xef\xb8\x8f", "\xe2\x9c\x8a\xf0\x9f\x8f\xbf", "\xf0\x9f\x8e\x9f\xef\xb8\x8f", "\xf0\x9f\x85\xb1\xef\xb8\x8f", "\xf0\x9f\x8e\x97\xef\xb8\x8f", "\xe2\x98\x9d\xf0\x9f\x8f\xbb", "\xe2\x98\x9d\xf0\x9f\x8f\xbc", "\xe2\x98\x9d\xf0\x9f\x8f\xbd", "\xe2\x98\x9d\xf0\x9f\x8f\xbe", "\xf0\x9f\x95\xb4\xef\xb8\x8f", "\xf0\x9f\x8f\xb7\xef\xb8\x8f", "\xe2\x9c\x8a\xf0\x9f\x8f\xbe", "\xe2\x98\x9d\xf0\x9f\x8f\xbf", "\xe2\x9c\x8a\xf0\x9f\x8f\xbd", "\xe2\x9c\x8a\xf0\x9f\x8f\xbc", "\xe2\x9c\x8a\xf0\x9f\x8f\xbb", "\xf0\x9f\x97\x9c\xef\xb8\x8f", "\xf0\x9f\x9b\xb3\xef\xb8\x8f", "\xf0\x9f\x85\xb0\xef\xb8\x8f", "\xf0\x9f\x9b\x8f\xef\xb8\x8f", "\xe2\x9c\x8d\xf0\x9f\x8f\xbf", "\xf0\x9f\x97\x9e\xef\xb8\x8f", "\xf0\x9f\x8e\x96\xef\xb8\x8f", "\xe2\x9c\x8d\xf0\x9f\x8f\xbe", "\xe2\x9c\x8d\xf0\x9f\x8f\xbd", "\xe2\x9c\x8d\xf0\x9f\x8f\xbc", "\xe2\x9c\x8d\xf0\x9f\x8f\xbb", "\xf0\x9f\x8f\x8c\xef\xb8\x8f", "\xe2\x9b\x8f\xef\xb8\x8f", "\xe2\x9a\x94\xef\xb8\x8f", "\xe2\x9a\x95\xef\xb8\x8f", "\xe2\x98\xba\xef\xb8\x8f", "\xe2\x93\x82\xef\xb8\x8f", "\xe2\x84\xa2\xef\xb8\x8f", "\xe2\x8f\xb2\xef\xb8\x8f", "\xe2\x9b\xa9\xef\xb8\x8f", "\xe3\x8a\x97\xef\xb8\x8f", "\xe2\xac\x86\xef\xb8\x8f", "\xe2\x9a\x92\xef\xb8\x8f", "\xe2\x86\x97\xef\xb8\x8f", "\xe2\x9a\x97\xef\xb8\x8f", "\xe2\x9c\x8c\xef\xb8\x8f", "\xe2\x9b\xb7\xef\xb8\x8f", "\xe2\x9c\x89\xef\xb8\x8f", "\xe2\x98\xa2\xef\xb8\x8f", "\xe2\x98\xa3\xef\xb8\x8f", "\xe3\x80\xbd\xef\xb8\x8f", "\xe2\x9a\xb0\xef\xb8\x8f", "\xe2\x9b\xb4\xef\xb8\x8f", "\xe3\x8a\x99\xef\xb8\x8f", "\xe2\x9a\xa0\xef\xb8\x8f", "\xe2\x84\xb9\xef\xb8\x8f", "\xe2\x9a\x9c\xef\xb8\x8f", "\xe2\x9b\xb9\xef\xb8\x8f", "\xe2\x9a\xb1\xef\xb8\x8f", "\xe2\x8f\xb1\xef\xb8\x8f", "\xe2\x98\x91\xef\xb8\x8f", "\xe2\x9c\x94\xef\xb8\x8f", "\xe2\x98\x98\xef\xb8\x8f", "\xe2\x97\xbc\xef\xb8\x8f", "\xe2\x9c\x82\xef\xb8\x8f", "\xe2\x99\xbb\xef\xb8\x8f", "\xe2\x99\xa8\xef\xb8\x8f", "\xe2\x9b\x91\xef\xb8\x8f", "\xe2\x9c\xb3\xef\xb8\x8f", "\xe2\x9c\xb4\xef\xb8\x8f", "\xe2\x9d\x87\xef\xb8\x8f", "\xe2\x96\xab\xef\xb8\x8f", "\xe2\x96\xaa\xef\xb8\x8f", "\xe2\x97\xbb\xef\xb8\x8f", "\xe2\x9c\x88\xef\xb8\x8f", "\xe2\x9b\x88\xef\xb8\x8f", "\xe2\x98\x82\xef\xb8\x8f", "\xe2\x9c\x8f\xef\xb8\x8f", "\xe2\x9c\x92\xef\xb8\x8f", "\xe2\x8f\xae\xef\xb8\x8f", "\xe2\x97\x80\xef\xb8\x8f", "\xe2\x8f\xaf\xef\xb8\x8f", "\xe2\x8f\xad\xef\xb8\x8f", "\xe2\x96\xb6\xef\xb8\x8f", "\xe2\x9c\x96\xef\xb8\x8f", "\xe2\x98\x81\xef\xb8\x8f", "\xe2\x98\xa0\xef\xb8\x8f", "\xe2\x9c\x8d\xef\xb8\x8f", "\xe2\x99\xbe\xef\xb8\x8f", "\xe2\x80\xbc\xef\xb8\x8f", "\xe2\x9a\x96\xef\xb8\x8f", "\xe2\x98\xae\xef\xb8\x8f", "\xe2\x86\x98\xef\xb8\x8f", "\xe2\x9d\xa4\xef\xb8\x8f", "\xe2\x9b\xb1\xef\xb8\x8f", "\xe2\x9d\xa3\xef\xb8\x8f", "\xe2\x9a\xa7\xef\xb8\x8f", "\xe2\x9d\x84\xef\xb8\x8f", "\xe2\x98\x83\xef\xb8\x8f", "\xe2\x99\x82\xef\xb8\x8f", "\xe2\x98\x84\xef\xb8\x8f", "\xe2\x99\x80\xef\xb8\x8f", "\xe2\x9a\x99\xef\xb8\x8f", "\xe2\x8f\xb8\xef\xb8\x8f", "\xe2\x8f\xb9\xef\xb8\x8f", "\xe2\x98\x9d\xef\xb8\x8f", "\xe2\x8f\xba\xef\xb8\x8f", "\xe2\x8f\x8f\xef\xb8\x8f", "\xe2\xac\x87\xef\xb8\x8f", "\xe2\x99\xa0\xef\xb8\x8f", "\xe2\x99\xa5\xef\xb8\x8f", "\xe2\x98\xaa\xef\xb8\x8f", "\xe2\x99\xa6\xef\xb8\x8f", "\xe2\x99\xa3\xef\xb8\x8f", "\xe2\x99\x9f\xef\xb8\x8f", "\xe2\xa4\xb4\xef\xb8\x8f", "\xe2\x98\x8e\xef\xb8\x8f", "\xe2\xa4\xb5\xef\xb8\x8f", "\xe2\x86\xaa\xef\xb8\x8f", "\xe2\x86\xa9\xef\xb8\x8f", "\xe2\x86\x94\xef\xb8\x8f", "\xe2\x86\x95\xef\xb8\x8f", "\xe2\x86\x96\xef\xb8\x8f", "\xe2\x86\x99\xef\xb8\x8f", "\xe2\xac\x85\xef\xb8\x8f", "\xe3\x80\xb0\xef\xb8\x8f", "\xe2\x9b\x93\xef\xb8\x8f", "\xe2\x8c\xa8\xef\xb8\x8f", "\xe2\x98\xb9\xef\xb8\x8f", "\xe2\x98\x80\xef\xb8\x8f", "\xe2\x9b\xb8\xef\xb8\x8f", "\xe2\x9b\xb0\xef\xb8\x8f", "\xe2\x81\x89\xef\xb8\x8f", "\xe2\x9a\x9b\xef\xb8\x8f", "\xe2\x9c\xa1\xef\xb8\x8f", "\xe2\x98\xb8\xef\xb8\x8f", "\xe2\x98\xaf\xef\xb8\x8f", "\xe2\x9c\x9d\xef\xb8\x8f", "\xe2\x9e\xa1\xef\xb8\x8f", "\xe2\x98\xa6\xef\xb8\x8f", "\xc2\xa9\xef\xb8\x8f", "\xc2\xae\xef\xb8\x8f", "\xf0\x9f\xa7\xac", "\xf0\x9f\x94\xac", "\xf0\x9f\x93\x91", "\xf0\x9f\xa7\xab", "\xf0\x9f\xa7\xaf", "\xf0\x9f\xa7\xb7", "\xf0\x9f\xa7\xb9", "\xf0\x9f\xa7\xba", "\xf0\x9f\xa7\xbb", "\xf0\x9f\xaa\xa3", "\xf0\x9f\xa7\xbc", "\xf0\x9f\xab\xa7", "\xf0\x9f\xaa\xa5", "\xf0\x9f\xa7\xbd", "\xf0\x9f\xa7\xb4", "\xf0\x9f\x9b\x92", "\xf0\x9f\x9a\xac", "\xf0\x9f\xaa\xa6", "\xf0\x9f\xa7\xbf", "\xf0\x9f\xaa\xac", "\xf0\x9f\x97\xbf", "\xf0\x9f\xaa\xa7", "\xf0\x9f\xaa\xaa", "\xf0\x9f\x8f\xa7", "\xf0\x9f\x9b\x97", "\xf0\x9f\x93\xa1", "\xf0\x9f\x92\x89", "\xf0\x9f\xa9\xb8", "\xf0\x9f\x92\x8a", "\xf0\x9f\xa9\xb9", "\xf0\x9f\xa9\xbc", "\xf0\x9f\xa9\xba", "\xf0\x9f\xa9\xbb", "\xf0\x9f\x9a\xaa", "\xf0\x9f\x94\xad", "\xf0\x9f\xaa\x9e", "\xf0\x9f\xaa\x9f", "\xf0\x9f\xaa\x91", "\xf0\x9f\x9a\xbd", "\xf0\x9f\xaa\xa0", "\xf0\x9f\x9a\xbf", "\xf0\x9f\x9b\x81", "\xf0\x9f\xaa\xa4", "\xf0\x9f\xaa\x92", "\xf0\x9f\xaa\xab", "\xf0\x9f\x8e\xa5", "\xf0\x9f\xa7\xae", "\xf0\x9f\x93\x80", "\xf0\x9f\x92\xbf", "\xf0\x9f\x92\xbe", "\xf0\x9f\x92\xbd", "\xf0\x9f\x92\xbb", "\xf0\x9f\x94\x8c", "\xf0\x9f\x8e\xac", "\xf0\x9f\x94\x8b", "\xf0\x9f\x93\xa0", "\xf0\x9f\x93\x9f", "\xf0\x9f\x93\x9e", "\xf0\x9f\x93\xb2", "\xf0\x9f\x93\xb1", "\xf0\x9f\xaa\x89", "\xf0\x9f\xaa\x88", "\xf0\x9f\x92\xa1", "\xf0\x9f\x93\x98", "\xf0\x9f\x93\x97", "\xf0\x9f\x93\x96", "\xf0\x9f\x93\x95", "\xf0\x9f\x93\x94", "\xf0\x9f\xaa\x94", "\xf0\x9f\x8f\xae", "\xf0\x9f\x94\xa6", "\xf0\x9f\xaa\x87", "\xf0\x9f\x94\x8e", "\xf0\x9f\x94\x8d", "\xf0\x9f\x93\xbc", "\xf0\x9f\x93\xb9", "\xf0\x9f\x93\xb8", "\xf0\x9f\x93\xb7", "\xf0\x9f\x93\xba", "\xf0\x9f\x92\x8d", "\xf0\x9f\x93\xaf", "\xf0\x9f\x93\xa3", "\xf0\x9f\x93\xa2", "\xf0\x9f\x94\x8a", "\xf0\x9f\x94\x89", "\xf0\x9f\x94\x88", "\xf0\x9f\x94\x87", "\xf0\x9f\x92\x8e", "\xf0\x9f\x94\x94", "\xf0\x9f\x92\x84", "\xf0\x9f\x93\xbf", "\xf0\x9f\xaa\x96", "\xf0\x9f\xa7\xa2", "\xf0\x9f\x8e\x93", "\xf0\x9f\x8e\xa9", "\xf0\x9f\x9a\x94", "\xf0\x9f\x8e\xb7", "\xf0\x9f\xaa\x98", "\xf0\x9f\xa5\x81", "\xf0\x9f\xaa\x95", "\xf0\x9f\x8e\xbb", "\xf0\x9f\x8e\xba", "\xf0\x9f\x8e\xb9", "\xf0\x9f\x8e\xb8", "\xf0\x9f\xaa\x97", "\xf0\x9f\x93\x99", "\xf0\x9f\x93\xbb", "\xf0\x9f\x8e\xa7", "\xf0\x9f\x8e\xa4", "\xf0\x9f\x8e\xb6", "\xf0\x9f\x8e\xb5", "\xf0\x9f\x8e\xbc", "\xf0\x9f\x94\x95", "\xf0\x9f\x93\x8c", "\xf0\x9f\x94\x90", "\xf0\x9f\x94\x8f", "\xf0\x9f\x94\x93", "\xf0\x9f\x94\x92", "\xf0\x9f\x93\x90", "\xf0\x9f\x93\x8f", "\xf0\x9f\x93\x8e", "\xf0\x9f\x93\x8d", "\xf0\x9f\x94\x91", "\xf0\x9f\x93\x8b", "\xf0\x9f\x93\x8a", "\xf0\x9f\x93\x89", "\xf0\x9f\x93\x88", "\xf0\x9f\x93\x87", "\xf0\x9f\x93\x86", "\xf0\x9f\x93\x85", "\xf0\x9f\xaa\x9b", "\xf0\x9f\xaa\x8f", "\xf0\x9f\xaa\x9c", "\xf0\x9f\xa7\xb2", "\xf0\x9f\xa7\xb0", "\xf0\x9f\xaa\x9d", "\xf0\x9f\x94\x97", "\xf0\x9f\xa6\xaf", "\xf0\x9f\x94\xa9", "\xf0\x9f\x93\x82", "\xf0\x9f\x94\xa7", "\xf0\x9f\xaa\x9a", "\xf0\x9f\x8f\xb9", "\xf0\x9f\xaa\x83", "\xf0\x9f\x92\xa3", "\xf0\x9f\xaa\x93", "\xf0\x9f\x94\xa8", "\xf0\x9f\x91\x92", "\xf0\x9f\x92\xb8", "\xf0\x9f\x92\xb7", "\xf0\x9f\x92\xb6", "\xf0\x9f\x92\xb5", "\xf0\x9f\x92\xb4", "\xf0\x9f\xaa\x99", "\xf0\x9f\x92\xb0", "\xf0\x9f\x94\x96", "\xf0\x9f\x92\xb3", "\xf0\x9f\x93\xb0", "\xf0\x9f\x93\x84", "\xf0\x9f\x93\x9c", "\xf0\x9f\x93\x83", "\xf0\x9f\x93\x92", "\xf0\x9f\x93\x93", "\xf0\x9f\x93\x9a", "\xf0\x9f\x93\xa6", "\xf0\x9f\x93\x81", "\xf0\x9f\x92\xbc", "\xf0\x9f\x93\x9d", "\xf0\x9f\x93\xae", "\xf0\x9f\x93\xad", "\xf0\x9f\x93\xac", "\xf0\x9f\x93\xaa", "\xf0\x9f\x93\xab", "\xf0\x9f\xa7\xaa", "\xf0\x9f\x93\xa5", "\xf0\x9f\x93\xa4", "\xf0\x9f\x93\xa9", "\xf0\x9f\x93\xa8", "\xf0\x9f\x93\xa7", "\xf0\x9f\x92\xb9", "\xf0\x9f\xa7\xbe", "\xf0\x9f\x98\xa4", "\xf0\x9f\x98\xb1", "\xf0\x9f\x98\x96", "\xf0\x9f\x98\xa3", "\xf0\x9f\x98\x9e", "\xf0\x9f\x98\x93", "\xf0\x9f\x98\xa9", "\xf0\x9f\x98\xab", "\xf0\x9f\xa5\xb1", "\xf0\x9f\x98\xad", "\xf0\x9f\x98\xa1", "\xf0\x9f\x98\xa0", "\xf0\x9f\xa4\xac", "\xf0\x9f\x98\x88", "\xf0\x9f\x91\xbf", "\xf0\x9f\x92\x80", "\xf0\x9f\x92\xa9", "\xf0\x9f\xa4\xa1", "\xf0\x9f\xa5\xba", "\xf0\x9f\x98\x95", "\xf0\x9f\xab\xa4", "\xf0\x9f\x98\x9f", "\xf0\x9f\x99\x81", "\xf0\x9f\x98\xae", "\xf0\x9f\x98\xaf", "\xf0\x9f\x98\xb2", "\xf0\x9f\x98\xb3", "\xf0\x9f\x91\xb9", "\xf0\x9f\xa5\xb9", "\xf0\x9f\x98\xa6", "\xf0\x9f\x98\xa7", "\xf0\x9f\x98\xa8", "\xf0\x9f\x98\xb0", "\xf0\x9f\x98\xa5", "\xf0\x9f\x98\xa2", "\xf0\x9f\x92\x9f", "\xf0\x9f\x92\x8c", "\xf0\x9f\x92\x98", "\xf0\x9f\x92\x9d", "\xf0\x9f\x92\x96", "\xf0\x9f\x92\x97", "\xf0\x9f\x92\x93", "\xf0\x9f\x92\x9e", "\xf0\x9f\x92\x95", "\xf0\x9f\x99\x8a", "\xf0\x9f\x92\x94", "\xf0\x9f\xa9\xb7", "\xf0\x9f\xa7\xa1", "\xf0\x9f\x92\x9b", "\xf0\x9f\x92\x9a", "\xf0\x9f\x92\x99", "\xf0\x9f\xa9\xb5", "\xf0\x9f\x98\xbb", "\xf0\x9f\x91\xba", "\xf0\x9f\x91\xbb", "\xf0\x9f\x91\xbd", "\xf0\x9f\x91\xbe", "\xf0\x9f\xa4\x96", "\xf0\x9f\x98\xba", "\xf0\x9f\x98\xb8", "\xf0\x9f\x98\xb9", "\xf0\x9f\xa7\x90", "\xf0\x9f\x98\xbc", "\xf0\x9f\x98\xbd", "\xf0\x9f\x99\x80", "\xf0\x9f\x98\xbf", "\xf0\x9f\x98\xbe", "\xf0\x9f\x99\x88", "\xf0\x9f\x99\x89", "\xf0\x9f\x98\x9d", "\xf0\x9f\x98\x97", "\xf0\x9f\x98\x9a", "\xf0\x9f\x98\x99", "\xf0\x9f\xa5\xb2", "\xf0\x9f\x98\x8b", "\xf0\x9f\x98\x9b", "\xf0\x9f\x98\x9c", "\xf0\x9f\xa4\xaa", "\xf0\x9f\x98\x98", "\xf0\x9f\xa4\x91", "\xf0\x9f\xa4\x97", "\xf0\x9f\xa4\xad", "\xf0\x9f\xab\xa2", "\xf0\x9f\xab\xa3", "\xf0\x9f\xa4\xab", "\xf0\x9f\xa4\x94", "\xf0\x9f\xab\xa1", "\xf0\x9f\x99\x83", "\xf0\x9f\x98\x83", "\xf0\x9f\x98\x84", "\xf0\x9f\x98\x81", "\xf0\x9f\x98\x86", "\xf0\x9f\x98\x85", "\xf0\x9f\xa4\xa3", "\xf0\x9f\x98\x82", "\xf0\x9f\x99\x82", "\xf0\x9f\xa4\x90", "\xf0\x9f\xab\xa0", "\xf0\x9f\x98\x89", "\xf0\x9f\x98\x8a", "\xf0\x9f\x98\x87", "\xf0\x9f\xa5\xb0", "\xf0\x9f\x98\x8d", "\xf0\x9f\xa4\xa9", "\xf0\x9f\xa5\xb4", "\xf0\x9f\x98\xb7", "\xf0\x9f\xa4\x92", "\xf0\x9f\xa4\x95", "\xf0\x9f\xa4\xa2", "\xf0\x9f\xa4\xae", "\xf0\x9f\xa4\xa7", "\xf0\x9f\xa5\xb5", "\xf0\x9f\xa5\xb6", "\xf0\x9f\xab\xa9", "\xf0\x9f\x98\xb5", "\xf0\x9f\xa4\xaf", "\xf0\x9f\xa4\xa0", "\xf0\x9f\xa5\xb3", "\xf0\x9f\xa5\xb8", "\xf0\x9f\x98\x8e", "\xf0\x9f\xa4\x93", "\xf0\x9f\x98\xac", "\xf0\x9f\xa4\xa8", "\xf0\x9f\x98\x90", "\xf0\x9f\x98\x91", "\xf0\x9f\x98\xb6", "\xf0\x9f\xab\xa5", "\xf0\x9f\x98\x8f", "\xf0\x9f\x98\x92", "\xf0\x9f\x99\x84", "\xf0\x9f\x92\x9c", "\xf0\x9f\xa4\xa5", "\xf0\x9f\xab\xa8", "\xf0\x9f\x98\x8c", "\xf0\x9f\x98\x94", "\xf0\x9f\x98\xaa", "\xf0\x9f\xa4\xa4", "\xf0\x9f\x98\xb4", "\xf0\x9f\x9f\xb0", "\xf0\x9f\x94\xa0", "\xf0\x9f\x94\x9f", "\xf0\x9f\xab\x9f", "\xf0\x9f\x94\xb0", "\xf0\x9f\x93\x9b", "\xf0\x9f\x94\xb1", "\xf0\x9f\x92\xb2", "\xf0\x9f\x92\xb1", "\xf0\x9f\x94\xa1", "\xf0\x9f\x93\xb4", "\xf0\x9f\x93\xb3", "\xf0\x9f\x9b\x9c", "\xf0\x9f\x93\xb6", "\xf0\x9f\x94\x86", "\xf0\x9f\x94\x85", "\xf0\x9f\x8e\xa6", "\xf0\x9f\x94\xbd", "\xf0\x9f\x86\x94", "\xf0\x9f\x88\xb6", "\xf0\x9f\x88\x81", "\xf0\x9f\x86\x9a", "\xf0\x9f\x86\x99", "\xf0\x9f\x86\x98", "\xf0\x9f\x86\x97", "\xf0\x9f\x86\x96", "\xf0\x9f\x86\x95", "\xf0\x9f\x94\xbc", "\xf0\x9f\x86\x93", "\xf0\x9f\x86\x92", "\xf0\x9f\x86\x91", "\xf0\x9f\x86\x8e", "\xf0\x9f\x94\xa4", "\xf0\x9f\x94\xa3", "\xf0\x9f\x94\xa2", "\xf0\x9f\x9b\x83", "\xf0\x9f\x9a\xb1", "\xf0\x9f\x9a\xaf", "\xf0\x9f\x9a\xad", "\xf0\x9f\x9a\xb3", "\xf0\x9f\x9a\xab", "\xf0\x9f\x9a\xb8", "\xf0\x9f\x9b\x85", "\xf0\x9f\x9b\x84", "\xf0\x9f\x9a\xb7", "\xf0\x9f\x9b\x82", "\xf0\x9f\x9a\xbe", "\xf0\x9f\x9a\xbc", "\xf0\x9f\x9a\xbb", "\xf0\x9f\x9a\xba", "\xf0\x9f\x9a\xb9", "\xf0\x9f\x9a\xb0", "\xf0\x9f\x94\x9c", "\xf0\x9f\x94\x82", "\xf0\x9f\x94\x81", "\xf0\x9f\x94\x80", "\xf0\x9f\xaa\xaf", "\xf0\x9f\x94\xaf", "\xf0\x9f\x95\x8e", "\xf0\x9f\x9b\x90", "\xf0\x9f\x94\x9d", "\xf0\x9f\x88\xaf", "\xf0\x9f\x94\x9b", "\xf0\x9f\x94\x9a", "\xf0\x9f\x94\x99", "\xf0\x9f\x94\x84", "\xf0\x9f\x94\x83", "\xf0\x9f\x94\x9e", "\xf0\x9f\x93\xb5", "\xf0\x9f\xa4\x8f", "\xf0\x9f\xab\xb1", "\xf0\x9f\xab\xb2", "\xf0\x9f\xab\xb3", "\xf0\x9f\xab\xb4", "\xf0\x9f\xab\xb7", "\xf0\x9f\xab\xb8", "\xf0\x9f\x91\x8c", "\xf0\x9f\xa4\x8c", "\xf0\x9f\x96\x96", "\xf0\x9f\xa4\x9e", "\xf0\x9f\xab\xb0", "\xf0\x9f\xa4\x9f", "\xf0\x9f\x8f\xb4", "\xf0\x9f\x8e\x8c", "\xf0\x9f\x9a\xa9", "\xf0\x9f\x8f\x81", "\xf0\x9f\x92\xab", "\xf0\x9f\xa4\x8e", "\xf0\x9f\x96\xa4", "\xf0\x9f\xa9\xb6", "\xf0\x9f\xa4\x8d", "\xf0\x9f\x92\x8b", "\xf0\x9f\x92\xaf", "\xf0\x9f\x92\xa2", "\xf0\x9f\x92\xa5", "\xf0\x9f\x94\xb2", "\xf0\x9f\x92\xa6", "\xf0\x9f\x92\xa8", "\xf0\x9f\x92\xac", "\xf0\x9f\x92\xad", "\xf0\x9f\x92\xa4", "\xf0\x9f\x91\x8b", "\xf0\x9f\xa4\x9a", "\xf0\x9f\x88\xb3", "\xf0\x9f\x9f\xa3", "\xf0\x9f\x94\xb5", "\xf0\x9f\x9f\xa2", "\xf0\x9f\x9f\xa1", "\xf0\x9f\x9f\xa0", "\xf0\x9f\x94\xb4", "\xf0\x9f\x88\xb5", "\xf0\x9f\x88\xba", "\xf0\x9f\x9f\xa4", "\xf0\x9f\x88\xb4", "\xf0\x9f\x88\xb8", "\xf0\x9f\x89\x91", "\xf0\x9f\x88\xb2", "\xf0\x9f\x88\x9a", "\xf0\x9f\x88\xb9", "\xf0\x9f\x89\x90", "\xf0\x9f\x94\xb6", "\xf0\x9f\x94\xb3", "\xf0\x9f\x94\x98", "\xf0\x9f\x92\xa0", "\xf0\x9f\x94\xbb", "\xf0\x9f\x94\xba", "\xf0\x9f\x94\xb9", "\xf0\x9f\x94\xb8", "\xf0\x9f\x94\xb7", "\xf0\x9f\x9a\xae", "\xf0\x9f\x9f\xab", "\xf0\x9f\x9f\xaa", "\xf0\x9f\x9f\xa6", "\xf0\x9f\x9f\xa9", "\xf0\x9f\x9f\xa8", "\xf0\x9f\x9f\xa7", "\xf0\x9f\x9f\xa5", "\xf0\x9f\xa6\x90", "\xf0\x9f\xaa\xb3", "\xf0\x9f\xa6\x97", "\xf0\x9f\x90\x9e", "\xf0\x9f\xaa\xb2", "\xf0\x9f\x90\x9d", "\xf0\x9f\x90\x9c", "\xf0\x9f\x90\x9b", "\xf0\x9f\xa6\x8b", "\xf0\x9f\x90\x8c", "\xf0\x9f\xa6\xaa", "\xf0\x9f\xa6\x91", "\xf0\x9f\xa6\x82", "\xf0\x9f\xa6\x9e", "\xf0\x9f\xa6\x80", "\xf0\x9f\xaa\xbc", "\xf0\x9f\xaa\xb8", "\xf0\x9f\x90\x9a", "\xf0\x9f\x90\x99", "\xf0\x9f\xa6\x88", "\xf0\x9f\x90\xa1", "\xf0\x9f\x90\xa0", "\xf0\x9f\x90\x9f", "\xf0\x9f\x8c\xba", "\xf0\x9f\x8c\xb5", "\xf0\x9f\x8c\xb4", "\xf0\x9f\x8c\xb3", "\xf0\x9f\x8c\xb2", "\xf0\x9f\xaa\xb4", "\xf0\x9f\x8c\xb1", "\xf0\x9f\xaa\xbb", "\xf0\x9f\x8c\xb7", "\xf0\x9f\x8c\xbc", "\xf0\x9f\x8c\xbb", "\xf0\x9f\xa6\xad", "\xf0\x9f\xa5\x80", "\xf0\x9f\x8c\xb9", "\xf0\x9f\xaa\xb7", "\xf0\x9f\x92\xae", "\xf0\x9f\x8c\xb8", "\xf0\x9f\x92\x90", "\xf0\x9f\xa6\xa0", "\xf0\x9f\xaa\xb1", "\xf0\x9f\xaa\xb0", "\xf0\x9f\xa6\x9f", "\xf0\x9f\xa6\xa1", "\xf0\x9f\xa6\x86", "\xf0\x9f\xa6\x85", "\xf0\x9f\x90\xa7", "\xf0\x9f\x90\xa6", "\xf0\x9f\x90\xa5", "\xf0\x9f\x90\xa4", "\xf0\x9f\x90\xa3", "\xf0\x9f\x90\x93", "\xf0\x9f\x90\x94", "\xf0\x9f\xa6\x83", "\xf0\x9f\x90\xbe", "\xf0\x9f\xa6\xa2", "\xf0\x9f\xa6\x98", "\xf0\x9f\xa6\xa8", "\xf0\x9f\xa6\xa6", "\xf0\x9f\xa6\xa5", "\xf0\x9f\x90\xbc", "\xf0\x9f\x90\xa8", "\xf0\x9f\x90\xbb", "\xf0\x9f\xa6\x87", "\xf0\x9f\xa6\x94", "\xf0\x9f\xa6\xab", "\xf0\x9f\x90\x8a", "\xf0\x9f\x90\xac", "\xf0\x9f\x90\x8b", "\xf0\x9f\x90\xb3", "\xf0\x9f\xa6\x96", "\xf0\x9f\xa6\x95", "\xf0\x9f\x90\x89", "\xf0\x9f\x90\xb2", "\xf0\x9f\x90\x8d", "\xf0\x9f\xa6\x8e", "\xf0\x9f\x90\xa2", "\xf0\x9f\x8c\xbe", "\xf0\x9f\x90\xb8", "\xf0\x9f\xa4\x98", "\xf0\x9f\xaa\xbf", "\xf0\x9f\xaa\xbd", "\xf0\x9f\xa6\x9c", "\xf0\x9f\xa6\x9a", "\xf0\x9f\xa6\xa9", "\xf0\x9f\xaa\xb6", "\xf0\x9f\xa6\xa4", "\xf0\x9f\xa6\x89", "\xf0\x9f\x8d\x96", "\xf0\x9f\xab\x94", "\xf0\x9f\x8c\xaf", "\xf0\x9f\x8c\xae", "\xf0\x9f\xa5\xaa", "\xf0\x9f\x8c\xad", "\xf0\x9f\x8d\x95", "\xf0\x9f\x8d\x9f", "\xf0\x9f\x8d\x94", "\xf0\x9f\xa5\x93", "\xf0\x9f\xa5\xa9", "\xf0\x9f\x8d\x97", "\xf0\x9f\xa5\x99", "\xf0\x9f\xa7\x80", "\xf0\x9f\xa7\x87", "\xf0\x9f\xa5\x9e", "\xf0\x9f\xa5\xaf", "\xf0\x9f\xa5\xa8", "\xf0\x9f\xab\x93", "\xf0\x9f\xa5\x96", "\xf0\x9f\xa5\x90", "\xf0\x9f\x8d\x9e", "\xf0\x9f\xab\x9c", "\xf0\x9f\xa7\x82", "\xf0\x9f\x8d\xa2", "\xf0\x9f\x8d\xa0", "\xf0\x9f\x8d\x9d", "\xf0\x9f\x91\x91", "\xf0\x9f\x8d\x9b", "\xf0\x9f\x8d\x9a", "\xf0\x9f\x8d\x99", "\xf0\x9f\x8d\x98", "\xf0\x9f\x8d\xb1", "\xf0\x9f\xa5\xab", "\xf0\x9f\xab\x9b", "\xf0\x9f\xa7\x88", "\xf0\x9f\x8d\xbf", "\xf0\x9f\xa5\x97", "\xf0\x9f\xa5\xa3", "\xf0\x9f\xab\x95", "\xf0\x9f\x8d\xb2", "\xf0\x9f\xa5\x98", "\xf0\x9f\x8d\xb3", "\xf0\x9f\xa5\x9a", "\xf0\x9f\xa7\x86", "\xf0\x9f\x8d\x88", "\xf0\x9f\x8d\x91", "\xf0\x9f\x8d\x90", "\xf0\x9f\x8d\x8f", "\xf0\x9f\x8d\x8e", "\xf0\x9f\xa5\xad", "\xf0\x9f\x8d\x8d", "\xf0\x9f\x8d\x8c", "\xf0\x9f\x8d\x8b", "\xf0\x9f\x8d\x8a", "\xf0\x9f\x8d\x89", "\xf0\x9f\x8d\x92", "\xf0\x9f\x8d\x87", "\xf0\x9f\xaa\xbe", "\xf0\x9f\x8d\x84", "\xf0\x9f\xaa\xba", "\xf0\x9f\xaa\xb9", "\xf0\x9f\x8d\x83", "\xf0\x9f\x8d\x82", "\xf0\x9f\x8d\x81", "\xf0\x9f\x8d\x80", "\xf0\x9f\x8c\xbf", "\xf0\x9f\x8c\xbd", "\xf0\x9f\xab\x9a", "\xf0\x9f\x8c\xb0", "\xf0\x9f\xab\x98", "\xf0\x9f\xa5\x9c", "\xf0\x9f\xa7\x85", "\xf0\x9f\xa7\x84", "\xf0\x9f\xa5\xa6", "\xf0\x9f\xa5\xac", "\xf0\x9f\xa5\x92", "\xf0\x9f\xab\x91", "\xf0\x9f\x90\x87", "\xf0\x9f\xa5\x95", "\xf0\x9f\xa5\x94", "\xf0\x9f\x8d\x86", "\xf0\x9f\xa5\x91", "\xf0\x9f\xa5\xa5", "\xf0\x9f\xab\x92", "\xf0\x9f\x8d\x85", "\xf0\x9f\xa5\x9d", "\xf0\x9f\xab\x90", "\xf0\x9f\x8d\x93", "\xf0\x9f\x9b\x8c", "\xf0\x9f\x99\x86", "\xf0\x9f\x91\xab", "\xf0\x9f\x92\x81", "\xf0\x9f\x99\x8b", "\xf0\x9f\xa7\x8f", "\xf0\x9f\x99\x87", "\xf0\x9f\x91\xad", "\xf0\x9f\xa4\xa6", "\xf0\x9f\xa4\xb7", "\xf0\x9f\x91\xae", "\xf0\x9f\x92\x82", "\xf0\x9f\x99\x85", "\xf0\x9f\xa5\xb7", "\xf0\x9f\x9b\x80", "\xf0\x9f\x91\xb7", "\xf0\x9f\xab\x85", "\xf0\x9f\xa4\xb4", "\xf0\x9f\xa7\x98", "\xf0\x9f\x91\xb8", "\xf0\x9f\x91\xb3", "\xf0\x9f\x91\xb2", "\xf0\x9f\xa4\xb9", "\xf0\x9f\x91\xb1", "\xf0\x9f\xa6\xb4", "\xf0\x9f\x91\x80", "\xf0\x9f\x91\x85", "\xf0\x9f\x91\x84", "\xf0\x9f\xab\xa6", "\xf0\x9f\x91\xb6", "\xf0\x9f\xa7\x92", "\xf0\x9f\x91\xa6", "\xf0\x9f\x91\xa7", "\xf0\x9f\xa7\x91", "\xf0\x9f\xa7\x95", "\xf0\x9f\x91\xa8", "\xf0\x9f\x92\x8f", "\xf0\x9f\xa7\x94", "\xf0\x9f\x91\xa9", "\xf0\x9f\xa7\x93", "\xf0\x9f\x91\xb4", "\xf0\x9f\x91\xb5", "\xf0\x9f\x91\xac", "\xf0\x9f\x99\x8d", "\xf0\x9f\x99\x8e", "\xf0\x9f\x8f\x83", "\xf0\x9f\xa7\x9e", "\xf0\x9f\xa7\x9f", "\xf0\x9f\xa7\x8c", "\xf0\x9f\x92\x86", "\xf0\x9f\x92\x87", "\xf0\x9f\x9a\xb6", "\xf0\x9f\xa7\x8d", "\xf0\x9f\x98\x80", "\xf0\x9f\x8f\x8a", "\xf0\x9f\xa7\x8e", "\xf0\x9f\xa7\x9d", "\xf0\x9f\x92\x83", "\xf0\x9f\x9a\xa3", "\xf0\x9f\x95\xba", "\xf0\x9f\x91\xaf", "\xf0\x9f\xa7\x96", "\xf0\x9f\x8f\x84", "\xf0\x9f\xa7\x97", "\xf0\x9f\xa4\xba", "\xf0\x9f\x8f\x87", "\xf0\x9f\x8f\x82", "\xf0\x9f\xa4\xb8", "\xf0\x9f\xa4\xb5", "\xf0\x9f\xa4\xbe", "\xf0\x9f\x91\xb0", "\xf0\x9f\xa4\xb0", "\xf0\x9f\xab\x83", "\xf0\x9f\xa4\xbd", "\xf0\x9f\xab\x84", "\xf0\x9f\xa4\xbc", "\xf0\x9f\xa4\xb1", "\xf0\x9f\x91\xbc", "\xf0\x9f\xa6\xb7", "\xf0\x9f\x8e\x85", "\xf0\x9f\xa4\xb6", "\xf0\x9f\xa6\xb8", "\xf0\x9f\x9a\xb5", "\xf0\x9f\xa6\xb9", "\xf0\x9f\xa7\x99", "\xf0\x9f\xa7\x9a", "\xf0\x9f\x9a\xb4", "\xf0\x9f\xa7\x9b", "\xf0\x9f\xa7\x9c", "\xf0\x9f\x90\x85", "\xf0\x9f\x90\x82", "\xf0\x9f\x90\xae", "\xf0\x9f\xa6\xac", "\xf0\x9f\xa6\x8c", "\xf0\x9f\xa6\x93", "\xf0\x9f\xa6\x84", "\xf0\x9f\x90\x8e", "\xf0\x9f\xab\x8f", "\xf0\x9f\xab\x8e", "\xf0\x9f\x90\xb4", "\xf0\x9f\x90\x86", "\xf0\x9f\x90\x83", "\xf0\x9f\x90\xaf", "\xf0\x9f\xa6\x81", "\xf0\x9f\x90\x88", "\xf0\x9f\x90\xb1", "\xf0\x9f\xa6\x9d", "\xf0\x9f\xa6\x8a", "\xf0\x9f\x90\xba", "\xf0\x9f\x90\xa9", "\xf0\x9f\xa6\xae", "\xf0\x9f\x90\x95", "\xf0\x9f\xa6\x99", "\xf0\x9f\x90\xb0", "\xf0\x9f\x90\xb9", "\xf0\x9f\x90\x80", "\xf0\x9f\x90\x81", "\xf0\x9f\x90\xad", "\xf0\x9f\xa6\x9b", "\xf0\x9f\xa6\x8f", "\xf0\x9f\xa6\xa3", "\xf0\x9f\x90\x98", "\xf0\x9f\xa6\x92", "\xf0\x9f\x90\xb6", "\xf0\x9f\x90\xab", "\xf0\x9f\x90\xaa", "\xf0\x9f\x90\x90", "\xf0\x9f\x90\x91", "\xf0\x9f\x90\x8f", "\xf0\x9f\x90\xbd", "\xf0\x9f\x90\x97", "\xf0\x9f\x90\x96", "\xf0\x9f\x90\xb7", "\xf0\x9f\x90\x84", "\xf0\x9f\x92\xaa", "\xf0\x9f\x91\x8f", "\xf0\x9f\x99\x8c", "\xf0\x9f\xab\xb6", "\xf0\x9f\x91\x90", "\xf0\x9f\xa4\xb2", "\xf0\x9f\xa4\x9d", "\xf0\x9f\x99\x8f", "\xf0\x9f\x92\x91", "\xf0\x9f\x92\x85", "\xf0\x9f\xa4\xb3", "\xf0\x9f\xa4\x9c", "\xf0\x9f\xa6\xbe", "\xf0\x9f\xa6\xbf", "\xf0\x9f\xa6\xb5", "\xf0\x9f\xa6\xb6", "\xf0\x9f\x91\x82", "\xf0\x9f\xa6\xbb", "\xf0\x9f\x91\x83", "\xf0\x9f\xa7\xa0", "\xf0\x9f\xab\x80", "\xf0\x9f\xab\x81", "\xf0\x9f\x91\xa4", "\xf0\x9f\xa6\xa7", "\xf0\x9f\xa6\x8d", "\xf0\x9f\x90\x92", "\xf0\x9f\x90\xb5", "\xf0\x9f\xab\x86", "\xf0\x9f\x91\xa3", "\xf0\x9f\xa4\x99", "\xf0\x9f\x91\xaa", "\xf0\x9f\xab\x82", "\xf0\x9f\x91\xa5", "\xf0\x9f\x8d\x9c", "\xf0\x9f\x91\x88", "\xf0\x9f\x91\x89", "\xf0\x9f\x91\x86", "\xf0\x9f\x96\x95", "\xf0\x9f\x91\x87", "\xf0\x9f\xab\xb5", "\xf0\x9f\x91\x8d", "\xf0\x9f\x91\x8e", "\xf0\x9f\x91\x8a", "\xf0\x9f\xa4\x9b", "\xf0\x9f\xa7\xa9", "\xf0\x9f\x95\x9e", "\xf0\x9f\x95\x92", "\xf0\x9f\x95\x9d", "\xf0\x9f\x95\x91", "\xf0\x9f\x95\x9c", "\xf0\x9f\x95\x90", "\xf0\x9f\x95\xa7", "\xf0\x9f\x95\x9b", "\xf0\x9f\x8e\xb2", "\xf0\x9f\x95\x93", "\xf0\x9f\xa7\xb8", "\xf0\x9f\xaa\x85", "\xf0\x9f\xa7\xb3", "\xf0\x9f\x9b\xb8", "\xf0\x9f\x9a\x80", "\xf0\x9f\x9a\xa1", "\xf0\x9f\x9a\xa0", "\xf0\x9f\x9a\x9f", "\xf0\x9f\x9a\x81", "\xf0\x9f\x95\xa3", "\xf0\x9f\x8c\x93", "\xf0\x9f\x8c\x92", "\xf0\x9f\x8c\x91", "\xf0\x9f\x95\xa6", "\xf0\x9f\x95\x9a", "\xf0\x9f\x95\xa5", "\xf0\x9f\x95\x99", "\xf0\x9f\x95\xa4", "\xf0\x9f\x95\x98", "\xf0\x9f\x92\xba", "\xf0\x9f\x95\x97", "\xf0\x9f\x95\xa2", "\xf0\x9f\x95\x96", "\xf0\x9f\x95\xa1", "\xf0\x9f\x95\x95", "\xf0\x9f\x95\xa0", "\xf0\x9f\x95\x94", "\xf0\x9f\x95\x9f", "\xf0\x9f\x9a\x9a", "\xf0\x9f\x9b\xb9", "\xf0\x9f\x9b\xb4", "\xf0\x9f\x9a\xb2", "\xf0\x9f\x9b\xba", "\xf0\x9f\xa6\xbc", "\xf0\x9f\xa6\xbd", "\xf0\x9f\x9b\xb5", "\xf0\x9f\x9a\x9c", "\xf0\x9f\x9a\x9b", "\xf0\x9f\x9b\xbc", "\xf0\x9f\x9b\xbb", "\xf0\x9f\x9a\x99", "\xf0\x9f\x9a\x98", "\xf0\x9f\x9a\x97", "\xf0\x9f\x9a\x96", "\xf0\x9f\x9a\x95", "\xf0\x9f\x9a\x93", "\xf0\x9f\x9a\x92", "\xf0\x9f\x83\x8f", "\xf0\x9f\xaa\x82", "\xf0\x9f\x9b\xac", "\xf0\x9f\x9b\xab", "\xf0\x9f\x9a\xa2", "\xf0\x9f\xaa\xa9", "\xf0\x9f\x9a\xa4", "\xf0\x9f\x9b\xb6", "\xf0\x9f\xaa\x86", "\xf0\x9f\x9b\x9f", "\xf0\x9f\x8c\x94", "\xf0\x9f\x9a\xa7", "\xf0\x9f\x9b\x91", "\xf0\x9f\x9a\xa6", "\xf0\x9f\x9a\xa5", "\xf0\x9f\x9a\xa8", "\xf0\x9f\x9b\x9e", "\xf0\x9f\x80\x84", "\xf0\x9f\x9a\x8f", "\xf0\x9f\x8f\x85", "\xf0\x9f\x8f\x88", "\xf0\x9f\x8f\x90", "\xf0\x9f\x8f\x80", "\xf0\x9f\xa5\x8e", "\xf0\x9f\x8e\xaf", "\xf0\x9f\xaa\x80", "\xf0\x9f\xa5\x89", "\xf0\x9f\xa5\x88", "\xf0\x9f\xa5\x87", "\xf0\x9f\x8f\x89", "\xf0\x9f\x8f\x86", "\xf0\x9f\x8e\xab", "\xf0\x9f\x8e\x81", "\xf0\x9f\x8e\x80", "\xf0\x9f\xa7\xa7", "\xf0\x9f\x8e\x91", "\xf0\x9f\x8e\x90", "\xf0\x9f\x8e\x8f", "\xf0\x9f\x8f\xb8", "\xf0\x9f\x9b\xb7", "\xf0\x9f\x8e\xbf", "\xf0\x9f\x8e\xbd", "\xf0\x9f\xa4\xbf", "\xf0\x9f\x8e\xa3", "\xf0\x9f\xa5\x8c", "\xf0\x9f\xa5\x85", "\xf0\x9f\xa5\x8b", "\xf0\x9f\xa5\x8a", "\xf0\x9f\x8e\x8e", "\xf0\x9f\x8f\x93", "\xf0\x9f\xa5\x8d", "\xf0\x9f\x8f\x92", "\xf0\x9f\x8f\x91", "\xf0\x9f\x8f\x8f", "\xf0\x9f\x8e\xb3", "\xf0\x9f\xa5\x8f", "\xf0\x9f\x8e\xbe", "\xf0\x9f\x8c\x9d", "\xf0\x9f\x8c\x88", "\xf0\x9f\x8c\x80", "\xf0\x9f\x8e\xae", "\xf0\x9f\x8c\x8c", "\xf0\x9f\x8c\xa0", "\xf0\x9f\x8c\x9f", "\xf0\x9f\x8e\xb0", "\xf0\x9f\xaa\x90", "\xf0\x9f\x8c\x9e", "\xf0\x9f\x8c\x82", "\xf0\x9f\x8c\x9c", "\xf0\x9f\x8c\x9b", "\xf0\x9f\x8c\x9a", "\xf0\x9f\x8c\x99", "\xf0\x9f\x8c\x98", "\xf0\x9f\x8c\x97", "\xf0\x9f\x8c\x96", "\xf0\x9f\x8c\x95", "\xf0\x9f\x8e\x84", "\xf0\x9f\x8e\x8d", "\xf0\x9f\x8e\x8b", "\xf0\x9f\x8e\x8a", "\xf0\x9f\x8e\x89", "\xf0\x9f\x8e\x88", "\xf0\x9f\xaa\x81", "\xf0\x9f\xa7\xa8", "\xf0\x9f\x8e\x87", "\xf0\x9f\x8e\x86", "\xf0\x9f\x9a\x91", "\xf0\x9f\x8e\x83", "\xf0\x9f\x8c\x8a", "\xf0\x9f\x92\xa7", "\xf0\x9f\x94\xa5", "\xf0\x9f\x94\xab", "\xf0\x9f\x8e\xb1", "\xf0\x9f\x94\xae", "\xf0\x9f\xaa\x84", "\xf0\x9f\x8d\xba", "\xf0\x9f\xa7\x83", "\xf0\x9f\xa7\x8b", "\xf0\x9f\xa5\xa4", "\xf0\x9f\xab\x97", "\xf0\x9f\x91\x99", "\xf0\x9f\xa5\x83", "\xf0\x9f\xa5\x82", "\xf0\x9f\x8d\xbb", "\xf0\x9f\x91\x9a", "\xf0\x9f\xa9\xb3", "\xf0\x9f\x8d\xb9", "\xf0\x9f\xaa\xad", "\xf0\x9f\x8d\xb8", "\xf0\x9f\x91\x9b", "\xf0\x9f\x8d\xb7", "\xf0\x9f\x8d\xbe", "\xf0\x9f\x8d\xb6", "\xf0\x9f\x8d\xb5", "\xf0\x9f\xa7\xa3", "\xf0\x9f\x8d\xb4", "\xf0\x9f\xa5\xa2", "\xf0\x9f\xa7\x8a", "\xf0\x9f\xa7\x89", "\xf0\x9f\xa5\xbc", "\xf0\x9f\xa6\xba", "\xf0\x9f\x91\x94", "\xf0\x9f\x91\x95", "\xf0\x9f\x91\x96", "\xf0\x9f\xab\x96", "\xf0\x9f\xa7\xa4", "\xf0\x9f\xa7\xa5", "\xf0\x9f\xa7\xa6", "\xf0\x9f\x91\x97", "\xf0\x9f\x91\x98", "\xf0\x9f\xa5\xbb", "\xf0\x9f\xa9\xb1", "\xf0\x9f\xa9\xb2", "\xf0\x9f\xa5\xae", "\xf0\x9f\xa5\xbe", "\xf0\x9f\xa5\xbf", "\xf0\x9f\x8d\xa8", "\xf0\x9f\x8d\xa7", "\xf0\x9f\x8d\xa6", "\xf0\x9f\xa5\xa1", "\xf0\x9f\xa5\xa0", "\xf0\x9f\xa5\x9f", "\xf0\x9f\x8d\xa1", "\xf0\x9f\x91\x9f", "\xf0\x9f\x8d\xa5", "\xf0\x9f\x91\xa0", "\xf0\x9f\x91\xa1", "\xf0\x9f\x8d\xa4", "\xf0\x9f\x8d\xa3", "\xf0\x9f\xa9\xb0", "\xf0\x9f\x91\xa2", "\xf0\x9f\xaa\xae", "\xf0\x9f\xa7\x81", "\xf0\x9f\x91\x9c", "\xf0\x9f\xa5\x9b", "\xf0\x9f\x8d\xbc", "\xf0\x9f\x8d\xaf", "\xf0\x9f\x8d\xae", "\xf0\x9f\x8d\xad", "\xf0\x9f\x8d\xac", "\xf0\x9f\x8d\xab", "\xf0\x9f\xa5\xa7", "\xf0\x9f\xa5\xbd", "\xf0\x9f\x91\x9d", "\xf0\x9f\x8e\x92", "\xf0\x9f\xa9\xb4", "\xf0\x9f\x8d\xb0", "\xf0\x9f\x8e\x82", "\xf0\x9f\x8d\xaa", "\xf0\x9f\x8d\xa9", "\xf0\x9f\x91\x9e", "\xf0\x9f\x8c\x83", "\xf0\x9f\x8e\xb4", "\xf0\x9f\x8e\xa1", "\xf0\x9f\x9b\x9d", "\xf0\x9f\x8e\xa0", "\xf0\x9f\x8c\x89", "\xf0\x9f\x8c\x87", "\xf0\x9f\x8c\x86", "\xf0\x9f\x8c\x85", "\xf0\x9f\x8c\x84", "\xf0\x9f\x8e\xa2", "\xf0\x9f\x8c\x81", "\xf0\x9f\x8e\xad", "\xf0\x9f\x8e\xa8", "\xf0\x9f\xa7\xb5", "\xf0\x9f\x95\x8b", "\xf0\x9f\x95\x8d", "\xf0\x9f\x9b\x95", "\xf0\x9f\x95\x8c", "\xf0\x9f\x9a\x88", "\xf0\x9f\x9a\x90", "\xf0\x9f\x9a\x8e", "\xf0\x9f\x9a\x8d", "\xf0\x9f\x9a\x8c", "\xf0\x9f\x9a\x8b", "\xf0\x9f\x9a\x9e", "\xf0\x9f\x9a\x9d", "\xf0\x9f\x9a\x8a", "\xf0\x9f\x9a\x89", "\xf0\x9f\xaa\xa1", "\xf0\x9f\x9a\x87", "\xf0\x9f\x9a\x86", "\xf0\x9f\x9a\x85", "\xf0\x9f\x9a\x84", "\xf0\x9f\x9a\x83", "\xf0\x9f\x9a\x82", "\xf0\x9f\x8e\xaa", "\xf0\x9f\x92\x88", "\xf0\x9f\x8c\x8e", "\xf0\x9f\xaa\xb5", "\xf0\x9f\xaa\xa8", "\xf0\x9f\xa7\xb1", "\xf0\x9f\x97\xbb", "\xf0\x9f\x8c\x8b", "\xf0\x9f\xa7\xad", "\xf0\x9f\x97\xbe", "\xf0\x9f\x8c\x90", "\xf0\x9f\x8c\x8f", "\xf0\x9f\x9b\x96", "\xf0\x9f\x8c\x8d", "\xf0\x9f\x8f\xba", "\xf0\x9f\xab\x99", "\xf0\x9f\xa7\xb6", "\xf0\x9f\xaa\xa2", "\xf0\x9f\x94\xaa", "\xf0\x9f\xa5\x84", "\xf0\x9f\x91\x93", "\xf0\x9f\x8f\xa9", "\xf0\x9f\x97\xbd", "\xf0\x9f\x97\xbc", "\xf0\x9f\x92\x92", "\xf0\x9f\x8f\xb0", "\xf0\x9f\x8f\xaf", "\xf0\x9f\x8f\xad", "\xf0\x9f\x8f\xac", "\xf0\x9f\x8f\xab", "\xf0\x9f\x8f\xaa", "\xf0\x9f\x8f\xa8", "\xf0\x9f\x8f\xa6", "\xf0\x9f\x8f\xa5", "\xf0\x9f\x8f\xa4", "\xf0\x9f\x8f\xa3", "\xf0\x9f\x8f\xa2", "\xf0\x9f\x8f\xa1", "\xf0\x9f\x8f\xa0", "\xe2\x9b\xb5", "\xe2\x8f\xab", "\xe2\x9a\xaa", "\xe2\x9a\xab", "\xe2\x99\x8b", "\xe2\x99\x8c", "\xe2\x99\x8d", "\xe2\x99\x8e", "\xe2\x99\x8f", "\xe2\x99\x90", "\xe2\x99\x91", "\xe2\x99\x92", "\xe2\x99\x93", "\xe2\x9b\x8e", "\xe2\x8f\xa9", "\xe2\x8f\xaa", "\xe2\x99\x8a", "\xe2\x8f\xac", "\xe2\x98\x95", "\xe2\x9e\x95", "\xe2\x9e\x96", "\xe2\x9e\x97", "\xe2\x9d\x93", "\xe2\x9d\x94", "\xe2\x9d\x95", "\xe2\x9d\x97", "\xe2\xad\x95", "\xe2\x9c\x85", "\xe2\x9d\x8c", "\xe2\x9d\x8e", "\xe2\x9e\xb0", "\xe2\x9e\xbf", "\xe2\x9a\xbe", "\xe2\x9a\xbd", "\xe2\x9c\xa8", "\xe2\x9b\x84", "\xe2\x9a\xa1", "\xe2\x9c\x8a", "\xe2\x98\x94", "\xe2\x9b\x85", "\xe2\xad\x90", "\xe2\x8f\xb0", "\xe2\x8c\x9a", "\xe2\x8f\xb3", "\xe2\x8c\x9b", "\xe2\x9c\x8b", "\xe2\x9b\xb3", "\xe2\x9a\x93", "\xe2\x9b\xbd", "\xe2\x99\xbf", "\xe2\x9b\xba", "\xe2\x9b\x94", "\xe2\x9b\xb2", "\xe2\x9b\xaa", "\xe2\x97\xbd", "\xe2\x97\xbe", "\xe2\xac\x9c", "\xe2\xac\x9b", "\xe2\x99\x88", "\xe2\x99\x89"}; // static std::unordered_set const SignalBackup::s_emoji_first_bytes({'\x23', '\x2a', '\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39', '\xc2', '\xe2', '\xe3', '\xf0'}); // static signalbackup-tools-20250313-1/signalbackup/statics_html.cc000066400000000000000000000342161476450434500234040ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" /* app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java // this map is actually the other way around, so can;t really be used for this purpose, // but im doing it anyway NAME_MAP.put("crimson", A170); NAME_MAP.put("vermillion", A170); NAME_MAP.put("burlap", A190); NAME_MAP.put("forest", A130); NAME_MAP.put("wintergreen", A130); NAME_MAP.put("teal", A120); NAME_MAP.put("blue", A110); NAME_MAP.put("indigo", A100); NAME_MAP.put("violet", A140); NAME_MAP.put("plum", A150); NAME_MAP.put("taupe", A190); NAME_MAP.put("steel", A210); NAME_MAP.put("ultramarine", A100); NAME_MAP.put("unknown", A210); NAME_MAP.put("red", A170); NAME_MAP.put("orange", A170); NAME_MAP.put("deep_orange", A170); NAME_MAP.put("brown", A190); NAME_MAP.put("green", A130); NAME_MAP.put("light_green", A130); NAME_MAP.put("purple", A140); NAME_MAP.put("deep_purple", A140); NAME_MAP.put("pink", A150); NAME_MAP.put("blue_grey", A190); NAME_MAP.put("grey", A210); app/src/main/java/org/thoughtcrime/securesms/color/MaterialColor.java private static final Map COLOR_MATCHES = new HashMap() {{ put("red", CRIMSON); put("deep_orange", CRIMSON); put("orange", VERMILLION); put("amber", VERMILLION); put("brown", BURLAP); put("yellow", BURLAP); put("pink", PLUM); put("purple", VIOLET); put("deep_purple", VIOLET); put("indigo", INDIGO); put("blue", BLUE); put("light_blue", BLUE); put("cyan", TEAL); put("teal", TEAL); put("green", FOREST); put("light_green", WINTERGREEN); put("lime", WINTERGREEN); put("blue_grey", TAUPE); put("grey", STEEL); put("ultramarine", ULTRAMARINE); put("group_color", GROUP); app/src/main/java/org/thoughtcrime/securesms/conversation/colors/ChatColorsPalette.kt val ULTRAMARINE = ChatColors.forColor(ChatColors.Id.BuiltIn,0xFF315FF4.toInt()) val CRIMSON = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFFCF163E.toInt()) val VERMILION = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFFC73F0A.toInt()) val BURLAP = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF6F6A58.toInt()) val FOREST = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF3B7845.toInt()) val WINTERGREEN = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF1D8663.toInt()) val TEAL = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF077D92.toInt()) val BLUE = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF336BA3.toInt()) val INDIGO = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF6058CA.toInt()) val VIOLET = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF9932CB.toInt()) val PLUM = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFFAA377A.toInt()) val TAUPE = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF8F616A.toInt()) val STEEL = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF71717F.toInt()) */ std::map const SignalBackup::s_html_colormap = {{"red", "CF163E"}, {"deep_orange", "CF163E"}, {"CRIMSON", "CF163E"}, {"orange", "C73F0A"}, {"amber", "C73F0A"}, {"A170", "C73F0A"}, {"C010", "C73F0A"}, {"C020", "C73F0A"}, {"C030", "C73F0A"}, {"VERMILION", "C73F0A"}, {"brown", "6F6A58"}, {"yellow", "6F6A58"}, {"C000", "6F6A58"}, {"C060", "6F6A58"}, {"C070", "6F6A58"}, {"A190", "6F6A58"}, // OR taupe {"BURLAP", "6F6A58"}, {"pink", "AA377A"}, {"C300", "AA377A"}, {"C310", "AA377A"}, {"C320", "AA377A"}, {"A150", "AA377A"}, {"PLUM", "AA377A"}, {"purple", "9932CB"}, {"deep_purple", "9932CB"}, {"C270", "9932CB"}, {"C280", "9932CB"}, {"C290", "9932CB"}, {"A140", "9932CB"}, {"VIOLET", "9932CB"}, {"indigo", "6058CA"}, {"C230", "6058CA"}, {"C240", "6058CA"}, {"C250", "6058CA"}, {"C260", "6058CA"}, {"A100", "6058CA"}, // OR ultramarine {"INDIGO", "6058CA"}, {"blue", "336BA3"}, {"light_blue", "336BA3"}, {"C200", "336BA3"}, {"C210", "336BA3"}, {"C220", "336BA3"}, {"A110", "336BA3"}, {"BLUE", "336BA3"}, {"cyan", "077D92"}, {"teal", "077D92"}, {"C170", "077D92"}, {"C180", "077D92"}, {"C190", "077D92"}, {"A120", "077D92"}, {"TEAL", "077D92"}, {"green", "3B7845"}, {"C080", "3B7845"}, {"C090", "3B7845"}, {"C100", "3B7845"}, {"C110", "3B7845"}, {"C120", "3B7845"}, {"C130", "3B7845"}, {"C140", "3B7845"}, {"C150", "3B7845"}, {"C160", "3B7845"}, {"A130", "3B7845"}, // OR wintergreen {"FOREST", "3B7845"}, {"light_green", "1D8663"}, {"lime", "1D8663"}, {"WINTERGREEN", "1D8663"}, {"blue_grey", "8F616A"}, {"TAUPE", "8F616A"}, {"grey", "71717F"}, {"A210", "71717F"}, {"STEEL", "71717F"}, {"ultramarine", "315FF4"}, {"ULTRAMARINE", "315FF4"}, {"group_color", "315FF4"}//, /* {"GROUP", "315FF4"}, */ /* {"A180", "FEF5D0"}, {"C040", "FEF5D0"}, {"C050", "FEF5D0"}, {"A160", "F6D8EC"}, {"C330", "F6D8EC"}, {"C340", "F6D8EC"}, {"C350", "F6D8EC"}, {"A200", "D2D2DC"} */ }; /* app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java A100("A100", 0xFFE3E3FE), A110("A110", 0xFFDDE7FC), A120("A120", 0xFFD8E8F0), A130("A130", 0xFFCDE4CD), A140("A140", 0xFFEAE0F8), A150("A150", 0xFFF5E3FE), A160("A160", 0xFFF6D8EC), A170("A170", 0xFFF5D7D7), A180("A180", 0xFFFEF5D0), A190("A190", 0xFFEAE6D5), A200("A200", 0xFFD2D2DC), A210("A210", 0xFFD7D7D9), UNKNOWN("UNKNOWN", 0x00000000), ON_SURFACE_VARIANT("ON_SURFACE_VARIANT", 0x00000000); */ std::array, 12> const SignalBackup::s_html_random_colors{std::pair{"A100", "E3E3FE"}, std::pair{"A110", "DDE7FC"}, std::pair{"A120", "D8E8F0"}, std::pair{"A130", "CDE4CD"}, std::pair{"A140", "EAE0F8"}, std::pair{"A150", "F5E3FE"}, std::pair{"A160", "F6D8EC"}, std::pair{"A170", "F5D7D7"}, std::pair{"A180", "FEF5D0"}, std::pair{"A190", "EAE6D5"}, std::pair{"A200", "D2D2DC"}, std::pair{"A210", "D7D7D9"}}; signalbackup-tools-20250313-1/signalbackup/statics_linkify.cc000066400000000000000000000422541476450434500241060ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ // Modified (slightly) from The Android Open Source Project // (https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/util/Patterns.java) #include "signalbackup.ih" #define IANA_TOP_LEVEL_DOMAINS "(?:" \ "(?:aaa|aarp|abb|abbott|abogado|academy|accenture|accountant|accountants|aco|active" \ "|actor|ads|adult|aeg|aero|afl|agency|aig|airforce|airtel|allfinanz|alsace|amica|amsterdam" \ "|android|apartments|app|apple|aquarelle|aramco|archi|army|arpa|arte|asia|associates" \ "|attorney|auction|audio|auto|autos|axa|azure|a[cdefgilmoqrstuwxz])" \ "|(?:band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bbc|bbva" \ "|bcn|beats|beer|bentley|berlin|best|bet|bharti|bible|bid|bike|bing|bingo|bio|biz|black" \ "|blackfriday|bloomberg|blue|bms|bmw|bnl|bnpparibas|boats|bom|bond|boo|boots|boutique" \ "|bradesco|bridgestone|broadway|broker|brother|brussels|budapest|build|builders|business" \ "|buzz|bzh|b[abdefghijmnorstvwyz])" \ "|(?:cab|cafe|cal|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards" \ "|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|ceb|center|ceo" \ "|cern|cfa|cfd|chanel|channel|chat|cheap|chloe|christmas|chrome|church|cipriani|cisco" \ "|citic|city|cityeats|claims|cleaning|click|clinic|clothing|cloud|club|clubmed|coach" \ "|codes|coffee|college|cologne|com|commbank|community|company|computer|comsec|condos" \ "|construction|consulting|contractors|cooking|cool|coop|corsica|country|coupons|courses" \ "|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc|cuisinella|cymru|cyou|c[acdfghiklmnoruvwxyz])" \ "|(?:dabur|dad|dance|date|dating|datsun|day|dclk|deals|degree|delivery|dell|delta" \ "|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount" \ "|dnp|docs|dog|doha|domains|doosan|download|drive|durban|dvag|d[ejkmoz])" \ "|(?:earth|eat|edu|education|email|emerck|energy|engineer|engineering|enterprises" \ "|epson|equipment|erni|esq|estate|eurovision|eus|events|everbank|exchange|expert|exposed" \ "|express|e[cegrstu])" \ "|(?:fage|fail|fairwinds|faith|family|fan|fans|farm|fashion|feedback|ferrero|film" \ "|final|finance|financial|firmdale|fish|fishing|fit|fitness|flights|florist|flowers|flsmidth" \ "|fly|foo|football|forex|forsale|forum|foundation|frl|frogans|fund|furniture|futbol|fyi" \ "|f[ijkmor])" \ "|(?:gal|gallery|game|garden|gbiz|gdn|gea|gent|genting|ggee|gift|gifts|gives|giving" \ "|glass|gle|global|globo|gmail|gmo|gmx|gold|goldpoint|golf|goo|goog|google|gop|gov|grainger" \ "|graphics|gratis|green|gripe|group|gucci|guge|guide|guitars|guru|g[abdefghilmnpqrstuwy])" \ "|(?:hamburg|hangout|haus|healthcare|help|here|hermes|hiphop|hitachi|hiv|hockey|holdings" \ "|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house|how|hsbc|hyundai" \ "|h[kmnrtu])" \ "|(?:ibm|icbc|ice|icu|ifm|iinet|immo|immobilien|industries|infiniti|info|ing|ink|institute" \ "|insure|int|international|investments|ipiranga|irish|ist|istanbul|itau|iwc|i[delmnoqrst])" \ "|(?:jaguar|java|jcb|jetzt|jewelry|jlc|jll|jobs|joburg|jprs|juegos|j[emop])" \ "|(?:kaufen|kddi|kia|kim|kinder|kitchen|kiwi|koeln|komatsu|krd|kred|kyoto|k[eghimnprwyz])" \ "|(?:lacaixa|lancaster|land|landrover|lasalle|lat|latrobe|law|lawyer|lds|lease|leclerc" \ "|legal|lexus|lgbt|liaison|lidl|life|lifestyle|lighting|limited|limo|linde|link|live" \ "|lixil|loan|loans|lol|london|lotte|lotto|love|ltd|ltda|lupin|luxe|luxury|l[abcikrstuvy])" \ "|(?:madrid|maif|maison|man|management|mango|market|marketing|markets|marriott|mba" \ "|media|meet|melbourne|meme|memorial|men|menu|meo|miami|microsoft|mil|mini|mma|mobi|moda" \ "|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|movistar" \ "|mtn|mtpc|mtr|museum|mutuelle|m[acdeghklmnopqrstuvwxyz])" \ "|(?:nadex|nagoya|name|navy|nec|net|netbank|network|neustar|new|news|nexus|ngo|nhk" \ "|nico|ninja|nissan|nokia|nra|nrw|ntt|nyc|n[acefgilopruz])" \ "|(?:obi|office|okinawa|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|osaka" \ "|otsuka|ovh|om)" \ "|(?:page|panerai|paris|partners|parts|party|pet|pharmacy|philips|photo|photography" \ "|photos|physio|piaget|pics|pictet|pictures|ping|pink|pizza|place|play|playstation|plumbing" \ "|plus|pohl|poker|porn|post|praxi|press|pro|prod|productions|prof|properties|property" \ "|protection|pub|p[aefghklmnrstwy])" \ "|(?:qpon|quebec|qa)" \ "|(?:racing|realtor|realty|recipes|red|redstone|rehab|reise|reisen|reit|ren|rent|rentals" \ "|repair|report|republican|rest|restaurant|review|reviews|rich|ricoh|rio|rip|rocher|rocks" \ "|rodeo|rsvp|ruhr|run|rwe|ryukyu|r[eosuw])" \ "|(?:saarland|sakura|sale|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|saxo" \ "|sbs|sca|scb|schmidt|scholarships|school|schule|schwarz|science|scor|scot|seat|security" \ "|seek|sener|services|seven|sew|sex|sexy|shiksha|shoes|show|shriram|singles|site|ski" \ "|sky|skype|sncf|soccer|social|software|sohu|solar|solutions|sony|soy|space|spiegel|spreadbetting" \ "|srl|stada|starhub|statoil|stc|stcgroup|stockholm|studio|study|style|sucks|supplies" \ "|supply|support|surf|surgery|suzuki|swatch|swiss|sydney|systems|s[abcdeghijklmnortuvxyz])" \ "|(?:tab|taipei|tatamotors|tatar|tattoo|tax|taxi|team|tech|technology|tel|telefonica" \ "|temasek|tennis|thd|theater|theatre|tickets|tienda|tips|tires|tirol|today|tokyo|tools" \ "|top|toray|toshiba|tours|town|toyota|toys|trade|trading|training|travel|trust|tui|t[cdfghjklmnortvwz])" \ "|(?:ubs|university|uno|uol|u[agksyz])" \ "|(?:vacations|vana|vegas|ventures|versicherung|vet|viajes|video|villas|vin|virgin" \ "|vision|vista|vistaprint|viva|vlaanderen|vodka|vote|voting|voto|voyage|v[aceginu])" \ "|(?:wales|walter|wang|watch|webcam|website|wed|wedding|weir|whoswho|wien|wiki|williamhill" \ "|win|windows|wine|wme|work|works|world|wtc|wtf|w[fs])" \ "|(?:\u03b5\u03bb|\u0431\u0435\u043b|\u0434\u0435\u0442\u0438|\u043a\u043e\u043c|\u043c\u043a\u0434" \ "|\u043c\u043e\u043d|\u043c\u043e\u0441\u043a\u0432\u0430|\u043e\u043d\u043b\u0430\u0439\u043d" \ "|\u043e\u0440\u0433|\u0440\u0443\u0441|\u0440\u0444|\u0441\u0430\u0439\u0442|\u0441\u0440\u0431" \ "|\u0443\u043a\u0440|\u049b\u0430\u0437|\u0570\u0561\u0575|\u05e7\u05d5\u05dd|\u0627\u0631\u0627\u0645\u0643\u0648" \ "|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629" \ "|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0627\u06cc\u0631\u0627\u0646" \ "|\u0628\u0627\u0632\u0627\u0631|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633" \ "|\u0633\u0648\u062f\u0627\u0646|\u0633\u0648\u0631\u064a\u0629|\u0634\u0628\u0643\u0629" \ "|\u0639\u0631\u0627\u0642|\u0639\u0645\u0627\u0646|\u0641\u0644\u0633\u0637\u064a\u0646" \ "|\u0642\u0637\u0631|\u0643\u0648\u0645|\u0645\u0635\u0631|\u0645\u0644\u064a\u0633\u064a\u0627" \ "|\u0645\u0648\u0642\u0639|\u0915\u0949\u092e|\u0928\u0947\u091f|\u092d\u093e\u0930\u0924" \ "|\u0938\u0902\u0917\u0920\u0928|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4" \ "|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd" \ "|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e04\u0e2d\u0e21|\u0e44\u0e17\u0e22" \ "|\u10d2\u10d4|\u307f\u3093\u306a|\u30b0\u30fc\u30b0\u30eb|\u30b3\u30e0|\u4e16\u754c" \ "|\u4e2d\u4fe1|\u4e2d\u56fd|\u4e2d\u570b|\u4e2d\u6587\u7f51|\u4f01\u4e1a|\u4f5b\u5c71" \ "|\u4fe1\u606f|\u5065\u5eb7|\u516b\u5366|\u516c\u53f8|\u516c\u76ca|\u53f0\u6e7e|\u53f0\u7063" \ "|\u5546\u57ce|\u5546\u5e97|\u5546\u6807|\u5728\u7ebf|\u5927\u62ff|\u5a31\u4e50|\u5de5\u884c" \ "|\u5e7f\u4e1c|\u6148\u5584|\u6211\u7231\u4f60|\u624b\u673a|\u653f\u52a1|\u653f\u5e9c" \ "|\u65b0\u52a0\u5761|\u65b0\u95fb|\u65f6\u5c1a|\u673a\u6784|\u6de1\u9a6c\u9521|\u6e38\u620f" \ "|\u70b9\u770b|\u79fb\u52a8|\u7ec4\u7ec7\u673a\u6784|\u7f51\u5740|\u7f51\u5e97|\u7f51\u7edc" \ "|\u8c37\u6b4c|\u96c6\u56e2|\u98de\u5229\u6d66|\u9910\u5385|\u9999\u6e2f|\ub2f7\ub137" \ "|\ub2f7\ucef4|\uc0bc\uc131|\ud55c\uad6d|xbox" \ "|xerox|xin|xn\\-\\-11b4c3d|xn\\-\\-1qqw23a|xn\\-\\-30rr7y|xn\\-\\-3bst00m|xn\\-\\-3ds443g" \ "|xn\\-\\-3e0b707e|xn\\-\\-3pxu8k|xn\\-\\-42c2d9a|xn\\-\\-45brj9c|xn\\-\\-45q11c|xn\\-\\-4gbrim" \ "|xn\\-\\-55qw42g|xn\\-\\-55qx5d|xn\\-\\-6frz82g|xn\\-\\-6qq986b3xl|xn\\-\\-80adxhks" \ "|xn\\-\\-80ao21a|xn\\-\\-80asehdb|xn\\-\\-80aswg|xn\\-\\-90a3ac|xn\\-\\-90ais|xn\\-\\-9dbq2a" \ "|xn\\-\\-9et52u|xn\\-\\-b4w605ferd|xn\\-\\-c1avg|xn\\-\\-c2br7g|xn\\-\\-cg4bki|xn\\-\\-clchc0ea0b2g2a9gcd" \ "|xn\\-\\-czr694b|xn\\-\\-czrs0t|xn\\-\\-czru2d|xn\\-\\-d1acj3b|xn\\-\\-d1alf|xn\\-\\-efvy88h" \ "|xn\\-\\-estv75g|xn\\-\\-fhbei|xn\\-\\-fiq228c5hs|xn\\-\\-fiq64b|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s" \ "|xn\\-\\-fjq720a|xn\\-\\-flw351e|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-gecrj9c" \ "|xn\\-\\-h2brj9c|xn\\-\\-hxt814e|xn\\-\\-i1b6b1a6a2e|xn\\-\\-imr513n|xn\\-\\-io0a7i" \ "|xn\\-\\-j1aef|xn\\-\\-j1amh|xn\\-\\-j6w193g|xn\\-\\-kcrx77d1x4a|xn\\-\\-kprw13d|xn\\-\\-kpry57d" \ "|xn\\-\\-kput3i|xn\\-\\-l1acc|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgb9awbf|xn\\-\\-mgba3a3ejt" \ "|xn\\-\\-mgba3a4f16a|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbab2bd|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e" \ "|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-mgbpl2fh|xn\\-\\-mgbtx2b|xn\\-\\-mgbx4cd0ab" \ "|xn\\-\\-mk1bu44c|xn\\-\\-mxtq1m|xn\\-\\-ngbc5azd|xn\\-\\-node|xn\\-\\-nqv7f|xn\\-\\-nqv7fs00ema" \ "|xn\\-\\-nyqy26a|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1acf|xn\\-\\-p1ai|xn\\-\\-pgbs0dh" \ "|xn\\-\\-pssy2u|xn\\-\\-q9jyb4c|xn\\-\\-qcka1pmc|xn\\-\\-qxam|xn\\-\\-rhqv96g|xn\\-\\-s9brj9c" \ "|xn\\-\\-ses554g|xn\\-\\-t60b56a|xn\\-\\-tckwe|xn\\-\\-unup4y|xn\\-\\-vermgensberater\\-ctb" \ "|xn\\-\\-vermgensberatung\\-pwb|xn\\-\\-vhquv|xn\\-\\-vuq861b|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a" \ "|xn\\-\\-xhq521b|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-y9a3aq|xn\\-\\-yfro4i67o" \ "|xn\\-\\-ygbi2ammx|xn\\-\\-zfr164b|xperia|xxx|xyz)" \ "|(?:yachts|yamaxun|yandex|yodobashi|yoga|yokohama|youtube|y[et])" \ "|(?:zara|zip|zone|zuerich|z[amw]))" #define IP_ADDRESS "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" \ "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" \ "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" \ "|[1-9][0-9]|[0-9]))" /* translated ECMAScript version of original (which had nested [[]] and intersection &&). may have errors... */ #define UCS_CHAR "\u00A1-\u1FFF\u200B-\u2027\u2030-\u202E\u2030-\u2FFF\u3001-\uD7FF" \ "\uF900-\uFDCF" \ "\uFDF0-\uFFEF" \ "\U00010000-\U0001FFFD" \ "\U00020000-\U0002FFFD" \ "\U00030000-\U0003FFFD" \ "\U00040000-\U0004FFFD" \ "\U00050000-\U0005FFFD" \ "\U00060000-\U0006FFFD" \ "\U00070000-\U0007FFFD" \ "\U00080000-\U0008FFFD" \ "\U00090000-\U0009FFFD" \ "\U000A0000-\U000AFFFD" \ "\U000B0000-\U000BFFFD" \ "\U000C0000-\U000CFFFD" \ "\U000D0000-\U000DFFFD" \ "\U000E1000-\U000EFFFD" #define LABEL_CHAR "a-zA-Z0-9" UCS_CHAR #define TLD_CHAR "a-zA-Z" UCS_CHAR #define IRI_LABEL "[" LABEL_CHAR "](?:[" LABEL_CHAR "_\\-]{0,61}[" LABEL_CHAR "]){0,1}" #define PUNYCODE_TLD "xn\\-\\-[\\w\\-]{0,58}\\w" #define TLD "(" PUNYCODE_TLD "|[" TLD_CHAR "]{2,63})" #define HOST_NAME "(" IRI_LABEL "\\.)+" TLD #define DOMAIN_NAME "(" HOST_NAME "|" IP_ADDRESS ")" #define PROTOCOL "(?:http|https|rtsp|ftp):\\/\\/" // NOTE originally started with (?i: -> non capturing group with i (case insensitive flag) not valid ECMAScript (its PCRE) (we add icase to std::regex object) #define WORD_BOUNDARY "(?:\\b|$|^)" #define USER_INFO "(?:[a-zA-Z0-9\\$\\-_\\.\\+\\!\\*\\'\\(\\)" \ "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-_" \ "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@" #define PORT_NUMBER "\\:\\d{1,5}" #define PATH_AND_QUERY "\\/(?:(?:[" \ LABEL_CHAR \ "\\;\\/\\?\\:\\@\\&\\=\\#\\~" \ "\\-\\.\\+\\!\\*\\'\\(\\)\\,_])|(?:\\%[a-fA-F0-9]{2}))*" #define STRICT_TLD "(?:" IANA_TOP_LEVEL_DOMAINS "|" PUNYCODE_TLD ")" #define STRICT_HOST_NAME "(?:(?:" IRI_LABEL "\\.)+" STRICT_TLD ")" #define STRICT_DOMAIN_NAME "(?:" STRICT_HOST_NAME "|" IP_ADDRESS ")" #define RELAXED_DOMAIN_NAME "(?:(?:" IRI_LABEL "(?:\\.(?=\\S))?)+|" IP_ADDRESS ")" #define EMAIL_PATTERN "[a-zA-Z0-9\\+\\._\\%\\-\\+]{1,256}" \ "\\@" \ "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" \ "(" \ "\\." \ "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" \ ")+" #define WEB_URL_WITHOUT_PROTOCOL "(" \ WORD_BOUNDARY \ "(?!:\\/\\/)" /* NOTE: was originally (?. */ #include "signalbackup.ih" bool SignalBackup::summarize() const { Logger::message("Database version: ", d_databaseversion); if (d_fd) Logger::message("Filesize: ", d_fd->total(), " bytes"); if (d_databaseversion < 25) return false; SqliteDB::QueryResults results; // daterange if (d_database.containsTable("sms")) { if (d_database.exec("SELECT DATETIME((MIN(mindate)) / 1000, 'unixepoch', 'localtime') AS 'Min Date', DATETIME(MAX((maxdate) / 1000), 'unixepoch', 'localtime') AS 'Max Date' FROM (SELECT MIN(sms." + d_sms_date_received + ") AS mindate, MAX(sms." + d_sms_date_received + ") AS maxdate FROM sms UNION ALL SELECT MIN(" + d_mms_table + ".date_received) AS mindate, MAX(" + d_mms_table + ".date_received) AS maxdate FROM " + d_mms_table + ")", &results)) Logger::message("Period: ", results.valueAsString(0, "Min Date"), " - ", results.valueAsString(0, "Max Date")); } else { if (d_database.exec("SELECT DATETIME(mindate / 1000, 'unixepoch', 'localtime') AS 'Min Date', DATETIME(maxdate / 1000, 'unixepoch', 'localtime') AS 'Max Date' FROM (SELECT MIN(" + d_mms_table + ".date_received) AS mindate, MAX(" + d_mms_table + ".date_received) AS maxdate FROM " + d_mms_table + ")", &results)) Logger::message("Period: ", results.valueAsString(0, "Min Date"), " - ", results.valueAsString(0, "Max Date")); } // tables + counts if (d_database.exec("SELECT name FROM sqlite_master WHERE type = 'table'", &results)) { Logger::message("Tables:"); for (unsigned int i = 0; i < results.rows(); ++i) { SqliteDB::QueryResults results2; if (d_database.exec("SELECT COUNT(*) FROM " + results.valueAsString(i, "name"), &results2)) { Logger::message_start(results.valueAsString(i, "name"), " : ", results2.getValueAs(0, 0)); if (results.valueAsString(i, "name") == "sms" || results.valueAsString(i, "name") == d_mms_table) { SqliteDB::QueryResults results3; if (d_database.exec("SELECT GROUP_CONCAT(counts, ',') FROM (SELECT COUNT(*) AS counts from " + results.valueAsString(i, "name") + " WHERE thread_id IN (SELECT _id FROM thread) GROUP BY thread_id ORDER BY thread_id ASC)", &results3)) Logger::message_start(" (per thread: ", results3.valueAsString(0, 0), ")"); } Logger::message_end(); } } } return true; } signalbackup-tools-20250313-1/signalbackup/tgbuildbody.cc000066400000000000000000000025221476450434500232110ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" std::string SignalBackup::tgBuildBody(std::string const &bodyjson) const { std::string body; long long int fragments = d_database.getSingleResultAs("SELECT json_array_length(?, '$')", bodyjson, -1); if (fragments == -1) { Logger::error("Failed to get number of text fragments from message body. Body data: '" + bodyjson + "'"); return body; } for (unsigned int i = 0; i < fragments; ++i) body += d_database.getSingleResultAs("SELECT json_extract(?, '$[" + bepaald::toString(i) + "].text')", bodyjson, std::string()); return body; } signalbackup-tools-20250313-1/signalbackup/tgimportmessages.cc000066400000000000000000000262761476450434500243120ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::tgImportMessages(SqliteDB const &db, std::vector, long long int>> const &contactmap, std::string const &datapath, std::string const &threadid, long long int chat_idx, bool prependforwarded, bool markdelivered, bool markread, bool isgroup) { // get recipient id for conversation auto find_in_contactmap = [&contactmap](std::string const &identifier) -> long long int { for (unsigned int i = 0; i < contactmap.size(); ++i) for (unsigned int j = 0; j < contactmap[i].first.size(); ++j) if (contactmap[i].first[j] == identifier) return contactmap[i].second; return -1; }; long long int thread_recipient_id = -1; if ((thread_recipient_id = find_in_contactmap(threadid)) == -1) { Logger::error("Recipient id not found in contactmap"); return false; } // get or create thread_id long long int thread_id = getThreadIdFromRecipient(thread_recipient_id); if (thread_id == -1) { Logger::warning_start("Failed to find matching thread for conversation, creating. (", threadid, " (id: ", thread_recipient_id, ")"); std::any new_thread_id; if (!insertRow("thread", {{d_thread_recipient_id, thread_recipient_id}, {"active", 1}, {"archived", 0}, //{d_thread_pinned, 0} }, "_id", &new_thread_id)) { Logger::message_end(); Logger::error("Failed to create thread for conversation."); return false; } //std::cout << "Raw any_cast 1" << std::endl; thread_id = std::any_cast(new_thread_id); Logger::message(", thread_id: ", thread_id, ")"); } // loop over messages from requested chat and insert SqliteDB::QueryResults message_data; if (!db.exec("SELECT type, date, from_id, forwarded_from, body, id, reply_to_id, photo, width, height, file, media_type, mime_type, contact_vcard, poll FROM messages " "WHERE chatidx = ? " "ORDER BY date ASC", chat_idx, &message_data)) return false; // we save the android message id that was created from telegram message id to be able to // properly handle quotes... std::map telegram_msg_id_to_adb_msg_id; // save timestamp of previous message (to merge messages with multiple attachments) std::pair prevtimestamp_to_id; for (unsigned int i = 0; i < message_data.rows(); ++i) { Logger::message("Dealing with message ", i + 1, "/", message_data.rows()); if (!message_data.isNull(i, "poll")) { Logger::warnOnce("Message is 'poll'. This is not supported in Signal. Skipping..."); continue; } if (message_data.valueAsString(i, "type") == "message") { // prepend notice to forwarded message std::string bodyjson = message_data.valueAsString(i, "body"); if (prependforwarded && !message_data.isNull(i, "forwarded_from")) { //Logger::message("Body json before: ", bodyjson); long long int n_text_entities = db.getSingleResultAs("SELECT json_array_length(?, '$')", bodyjson, -1); if (n_text_entities == -1) { Logger::warning("Failed to get number of text entities in forwarded message"); break; } std::string fname = message_data(i, "forwarded_from"); std::string tmp = db.getSingleResultAs("SELECT json_array(json_object('type', 'italic', 'text', ?), json_object('type', 'plain', 'text', '\n'))", "Forwarded from " + fname + ":", std::string()); for (unsigned int nt = 0; nt < n_text_entities; ++nt) tmp = db.getSingleResultAs("SELECT json_insert(?, '$[#]', json_extract(?, '$[" + bepaald::toString(nt) + "]'))", {tmp, bodyjson}, std::string()); if (!tmp.empty()) bodyjson = std::move(tmp); //Logger::message("Body json after: ", bodyjson); } // gather data std::string body = tgBuildBody(bodyjson); std::string fromid = message_data.valueAsString(i, "from_id"); long long int from_recid = -1; if ((from_recid = find_in_contactmap(fromid)) == -1) { Logger::error("Recipient id not found in contactmap"); return false; } bool incoming = from_recid != d_selfid; long long int address = !isgroup ? from_recid : (incoming ? from_recid : thread_recipient_id); // check if we need to merge message if (message_data.valueAsInt(i, "date") == prevtimestamp_to_id.first && body.empty() && message_data.isNull(i, "reply_to_id") && (!message_data(i, "file").empty() || !message_data(i, "photo").empty()) && from_recid == d_database.getSingleResultAs("SELECT " + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE _id = ?", prevtimestamp_to_id.second, -1) && d_database.getSingleResultAs("SELECT _id FROM " + d_part_table + " WHERE " + d_part_mid + " = ? LIMIT 1", prevtimestamp_to_id.second, -1) != -1) { Logger::message("Attachment-only message with same timestamp as previous: assuming attachment belongs to previous message"); // add attachments tgSetAttachment(message_data, datapath, i, prevtimestamp_to_id.second); continue; } // make sure date/from/thread is available long long int date = message_data.valueAsInt(i, "date") * 1000; long long int freedate = getFreeDateForMessage(date, thread_id, incoming ? address : d_selfid); if (freedate == -1) { Logger::error("Getting free date for inserting message into mms"); continue; } // insert message std::any retval; if (!insertRow(d_mms_table, {{"thread_id", thread_id}, {d_mms_date_sent, freedate}, {"date_received", freedate}, {"date_server", freedate}, {d_mms_type, Types::SECURE_MESSAGE_BIT | Types::PUSH_MESSAGE_BIT | (incoming ? Types::BASE_INBOX_TYPE : Types::BASE_SENT_TYPE)}, {"body", body.empty() ? std::any(nullptr) : std::any(body)}, {"read", 1}, // defaults to 0, but causes tons of unread message notifications {d_mms_delivery_receipts, (incoming ? 0 : (markdelivered ? 1 : 0))}, {d_mms_read_receipts, (incoming ? 0 : (markread ? 1 : 0))}, //{d_mms_viewed_receipts, (incoming ? 0 : 1)}, {d_mms_recipient_id, incoming ? address : d_selfid}, {"to_recipient_id", incoming ? d_selfid : address}, {"m_type", incoming ? 132 : 128}}, // dont know what this is, but these are the values... //{"quote_id", hasquote ? (bepaald::contains(adjusted_timestamps, mmsquote_id) ? adjusted_timestamps[mmsquote_id] : mmsquote_id) : 0}, //{"quote_author", hasquote ? std::any(mmsquote_author) : std::any(nullptr)}, //{"quote_body", hasquote ? mmsquote_body : nullptr}, //{"quote_missing", hasquote ? mmsquote_missing : 0}, //{"quote_mentions", hasquote ? std::any(mmsquote_mentions) : std::any(nullptr)}, //{"shared_contacts", shared_contacts_json.empty() ? std::any(nullptr) : std::any(shared_contacts_json)}, //{"remote_deleted", 0}, //{"view_once", 0}}, // if !createrecipient -> this message was already skipped "_id", &retval)) { Logger::error("Failed to insert message"); continue; } long long int new_msg_id = std::any_cast(retval); bool msg_deleted = false; // add attachments if (!tgSetAttachment(message_data, datapath, i, new_msg_id)) { if (body.empty()) { Logger::warning("Failed to set attachment on otherwise empty message. Deleting message..."); if (d_database.exec("DELETE FROM " + d_mms_table + " WHERE _id = ?", new_msg_id)) msg_deleted = true; } else Logger::warning("Failed to set attachment"); } // save to map for quotes, we do this AFTER adding attachment, because that // may delete the new message on failure... telegram_msg_id_to_adb_msg_id[message_data.valueAsInt(i, "id")] = new_msg_id; // save message timestamp and id prevtimestamp_to_id = {message_data.valueAsInt(i, "date"), new_msg_id}; // deal with quotes if (!message_data.isNull(i, "reply_to_id")) { long long int quotemsg = message_data.valueAsInt(i, "reply_to_id"); if (bepaald::contains(telegram_msg_id_to_adb_msg_id, quotemsg)) { if (d_verbose) [[unlikely]] Logger::message("Found quote: ", telegram_msg_id_to_adb_msg_id[quotemsg]); tgSetQuote(telegram_msg_id_to_adb_msg_id[quotemsg], new_msg_id); } else Logger::message("Message was wuote, but quoted message not found..."); } // set body ranges if (!tgSetBodyRanges(bodyjson, new_msg_id)) { // warn? // error? // return false? continue; } if (!msg_deleted && d_database.getSingleResultAs("SELECT body FROM " + d_mms_table + " WHERE _id = ?", new_msg_id, std::string()).empty() && // no message body d_database.getSingleResultAs("SELECT _id FROM " + d_part_table + " WHERE " + d_part_mid + " = ?", new_msg_id, -1) == -1 && // no attachment d_database.getSingleResultAs("SELECT quote_id FROM " + d_mms_table + " WHERE _id = ?", new_msg_id, 0) == 0) // no quote { Logger::message("Maybe inserted empty message."); Logger::message("Data:"); message_data.getRow(i).prettyPrint(d_truncate); } } // else if... else { Logger::warnOnce("Unsupported message type `" + message_data.valueAsString(i, "type") + "'. Skipping..."); continue; } } return true; } signalbackup-tools-20250313-1/signalbackup/tgmapcontacts.cc000066400000000000000000000325241476450434500235550ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../jsondatabase/jsondatabase.h" bool SignalBackup::tgMapContacts(JsonDatabase const &jsondb, std::string const &chatlist, std::vector, long long int>> *contactmap, std::vector const &inhibitmappping) const { if (d_verbose && contactmap->size()) [[unlikely]] { Logger::message("[INITIAL CONTACT MAP ]"); for (unsigned int i = 0; i < contactmap->size(); ++i) { std::string name = getNameFromRecipientId((*contactmap)[i].second); Logger::message(" * ", (*contactmap)[i].first, " -> ", (*contactmap)[i].second, "(", name, ")"); } } std::vector, long long int>> realcontactmap = *contactmap; auto find_in_contactmap = [&realcontactmap](std::string const &identifier) -> long long int { for (unsigned int i = 0; i < realcontactmap.size(); ++i) for (unsigned int j = 0; j < realcontactmap[i].first.size(); ++j) if (realcontactmap[i].first[j] == identifier) return i; return -1; }; std::vector>> recipientsnotfound; auto move_from_not_found_to_contactmap = [&recipientsnotfound, &realcontactmap](SqliteDB::QueryResults const &contacts, std::string const &identifyer) { for (auto it = recipientsnotfound.begin(); it != recipientsnotfound.end(); ++it) if (contacts.valueAsString(it->first, "id") == identifyer) { for (unsigned int i = 0; i < it->second.size(); ++i) if (!bepaald::contains(realcontactmap.back().first, it->second[i])) realcontactmap.back().first.push_back(it->second[i]); recipientsnotfound.erase(it); break; } }; // get contacts that need matching from database SqliteDB::QueryResults json_contacts; if (!jsondb.d_database.exec("SELECT DISTINCT id FROM chats " + (chatlist.empty() ? "" : "WHERE idx IN " + chatlist + " ") + "UNION " "SELECT DISTINCT from_id AS id FROM messages WHERE type IS 'message' " + (chatlist.empty() ? "" : "AND chatidx IN " + chatlist), &json_contacts)) return false; if (d_verbose) [[unlikely]] { Logger::message("ALL CONTACTS IN JSON: "); json_contacts.prettyPrint(d_truncate); } for (unsigned int i = 0; i < json_contacts.rows(); ++i) { std::string contact = json_contacts.valueAsString(i, "id"); // if it's already in contactmap, we can skip it if (find_in_contactmap(contact) != -1) { //std::cout << "Skipping " << contact << std::endl; //std::cout << realcontactmap[contact].first << std::endl; continue; } // find it in android db by name long long int found_id = -1; SqliteDB::QueryResults aliases; jsondb.d_database.exec("SELECT DISTINCT from_name AS name FROM messages WHERE from_id = ?1 " "UNION " "SELECT DISTINCT name FROM chats WHERE id = ?1", contact, &aliases); for (unsigned int j = 0; j < aliases.rows(); ++j) { if (bepaald::contains(inhibitmappping, aliases(j, "name"))) continue; // if the contact is already in contactmap by alias, we are done again long long int contactidx = -1; if ((contactidx = find_in_contactmap(aliases(j, "name"))) != -1) found_id = realcontactmap[contactidx].second; else found_id = getRecipientIdFromName(aliases(j, "name"), false); if (found_id != -1) { if (d_verbose) [[unlikely]] Logger::message("Found json contact by name: ", contact, " (", aliases(j, "name"), ") -> ", found_id); break; } } if (found_id != -1) { if (d_verbose) [[unlikely]] Logger::message("Found json contact by name: ", contact, " -> ", found_id); // we found this contact, add it (and all names) to map realcontactmap.push_back({{contact}, found_id}); for (unsigned int j = 0; j < aliases.rows(); ++j) realcontactmap.back().first.push_back(aliases(j, "name")); continue; } else { recipientsnotfound.emplace_back(std::make_pair(i, std::vector())); for (unsigned int j = 0; j < aliases.rows(); ++j) recipientsnotfound.back().second.push_back(aliases.isNull(j, "name") ? "null" : aliases(j, "name")); } } // now let's try to find self std::vector self_json_id; // check if we have already (maybe passed in map, maybe matched by name) for (unsigned int i = 0; i < realcontactmap.size(); ++i) if (realcontactmap[i].second == d_selfid) { self_json_id = realcontactmap[i].first; break; } // try to determine: 1. If the database has a 'saved_messages' type chat, it should only contain messages "from": self (messages.from_id => self) // also, the chat itself should be self (chat.id => self) if (self_json_id.empty()) { SqliteDB::QueryResults ids_in_saved_messages; if (jsondb.d_database.exec("SELECT DISTINCT from_id FROM messages WHERE chatidx IN (SELECT DISTINCT idx FROM chats WHERE type = 'saved_messages')", &ids_in_saved_messages) && ids_in_saved_messages.rows() == 1) { realcontactmap.push_back({{ids_in_saved_messages("from_id")}, d_selfid}); // copy aliases and erase from not found move_from_not_found_to_contactmap(json_contacts, ids_in_saved_messages("from_id")); self_json_id = realcontactmap.back().first; } SqliteDB::QueryResults saved_messages_id; if (jsondb.d_database.exec("SELECT DISTINCT id FROM chats WHERE type = 'saved_messages'", &saved_messages_id) && saved_messages_id.rows() == 1) { realcontactmap.push_back({{saved_messages_id("id")}, d_selfid}); // copy aliases and erase from not found move_from_not_found_to_contactmap(json_contacts, saved_messages_id("id")); self_json_id = realcontactmap.back().first; } } // try to determine: 2. only one from_id will be present in multiple 1-on-1 chats if (self_json_id.empty()) { SqliteDB::QueryResults ids_in_personal_chats; //jsondb.d_database.exec("SELECT from_id, COUNT(from_id) AS idcount FROM (SELECT DISTINCT from_id, chatidx FROM messages WHERE type = 'message' AND chatidx IN (SELECT idx FROM chats WHERE type = 'personal_chat')) GROUP BY from_id", &ids_in_personal_chats); //jsondb.d_database.exec("SELECT from_id, COUNT(from_id) AS idcount FROM (SELECT DISTINCT from_id, chatidx FROM messages WHERE type = 'message' AND chatidx IN (SELECT idx FROM chats WHERE type = 'personal_chat')) GROUP BY from_id HAVING idcount == 1", &ids_in_personal_chats); jsondb.d_database.exec("SELECT from_id, COUNT(from_id) AS idcount FROM (SELECT DISTINCT from_id, chatidx FROM messages WHERE type = 'message' AND chatidx IN (SELECT idx FROM chats WHERE type = 'personal_chat')) GROUP BY from_id HAVING idcount > 1", &ids_in_personal_chats); //ids_in_personal_chats.prettyPrint(); // if all ids except one occur only once, the exception surely is self if (ids_in_personal_chats.rows() == 1) { if (d_verbose) [[unlikely]] Logger::message("Found json contact for self: ", ids_in_personal_chats(0, "from_id"), " -> ", d_selfid); realcontactmap.push_back({{ids_in_personal_chats("from_id")}, d_selfid}); // copy aliases and erase from not found move_from_not_found_to_contactmap(json_contacts, ids_in_personal_chats("from_id")); self_json_id = realcontactmap.back().first; } } // if we have self, we can probably merge some chatids and userids (somehow)... if (!self_json_id.empty()) { // for each chat id that is personal_chat SqliteDB::QueryResults personal_chat_contacts; if (!jsondb.d_database.exec("SELECT DISTINCT from_id, chatidx FROM messages " "WHERE type = 'message' " //"AND chatidx IN (SELECT idx FROM chats WHERE type = 'personal_chat')", "AND chatidx IN (SELECT idx FROM chats WHERE type = 'personal_chat'" + (chatlist.empty() ? "" : "AND idx IN " + chatlist) + ")", &personal_chat_contacts)) return false; //personal_chat_contacts.prettyPrint(); for (unsigned int i = 0; i < personal_chat_contacts.rows(); ++i) { if (bepaald::contains(self_json_id, personal_chat_contacts(i, "from_id"))) continue; std::string linkchat = jsondb.d_database.getSingleResultAs("SELECT id FROM chats WHERE idx = ?", personal_chat_contacts.value(i, "chatidx"), std::string()); if (linkchat.empty()) // we failed apparently continue; //std::cout << "SHOULD LINK " << personal_chat_contacts(i, "from_id") << " WITH " << linkchat << std::endl; long long int contactidx = find_in_contactmap(personal_chat_contacts(i, "from_id")); long long int chatidx = find_in_contactmap(linkchat); // if both are already present if (contactidx != -1 && chatidx != -1) continue; // if one of the two is already in the contactmap, just make sure they link to the same Signal_id (and erase from notfound) if (contactidx != -1) { if (d_verbose) [[unlikely]] Logger::message("Linking contacts: ", personal_chat_contacts(i, "from_id"), " == ", linkchat); realcontactmap.push_back({{linkchat}, realcontactmap[contactidx].second}); // copy aliases and erase from not found move_from_not_found_to_contactmap(json_contacts, linkchat); continue; } if (chatidx != -1) { if (d_verbose) [[unlikely]] Logger::message("Linking contacts: ", personal_chat_contacts(i, "from_id"), " == ", linkchat); realcontactmap.push_back({{personal_chat_contacts(i, "from_id")}, realcontactmap[chatidx].second}); // copy aliases and erase from not found move_from_not_found_to_contactmap(json_contacts, personal_chat_contacts(i, "from_id")); continue; } // if both are not in contactmap, merge them in recipientsnotfound just for a cleaner prompt to user std::pair> tmp; // find the first in the recipientsnotfound, save it and delete it bool removed = false; for (auto it = recipientsnotfound.begin(); it != recipientsnotfound.end(); ++it) { if (json_contacts.valueAsString(it->first, "id") == linkchat) { tmp = *it; recipientsnotfound.erase(it); removed = true; break; } } if (!removed) [[unlikely]] Logger::warning("Something went wrong merging unknown json contacts"); else // then merge its aliases into the others (if not present) { if (d_verbose) [[unlikely]] Logger::message("Linking unmapped contacts: ", personal_chat_contacts(i, "from_id"), " == ", linkchat); for (auto it = recipientsnotfound.begin(); it != recipientsnotfound.end(); ++it) { if (json_contacts.valueAsString(it->first, "id") == personal_chat_contacts(i, "from_id")) { // the first of each of these should be guaranteed the json id? it->second.insert(it->second.begin(), json_contacts(tmp.first, "id")); for (unsigned int j = 1; j < tmp.second.size(); ++j) if (!bepaald::contains(it->second, tmp.second[j])) it->second.push_back(tmp.second[j]); break; } } } } } if (d_verbose) [[unlikely]] { Logger::message("[FINAL CONTACT MAP] "); for (unsigned int i = 0; i < realcontactmap.size(); ++i) { std::string name = getNameFromRecipientId(realcontactmap[i].second); Logger::message(" * ", Logger::VECTOR(realcontactmap[i].first, ", "), " -> ", realcontactmap[i].second, " (", name, ")"); } } if (!recipientsnotfound.empty()) { Logger::error("The following contacts in the JSON input were not found in the Android backup:"); for (auto const &r : recipientsnotfound) if (r.second.size() == 0) Logger::error_indent(" - \"", json_contacts.valueAsString(r.first, "id"), "\""); else Logger::error_indent(" - \"", json_contacts.valueAsString(r.first, "id"), "\"/\"", Logger::VECTOR(r.second, "\"/\""), "\""); Logger::message("Use `--mapjsoncontacts [NAME1]=[id1],[NAME2]=[id2],...' to map these to an existing recipient id \n" "from the backup. The list of available recipients and their id's can be obtained by running \n" "with `--listrecipients'."); return false; } *contactmap = std::move(realcontactmap); return recipientsnotfound.empty(); } signalbackup-tools-20250313-1/signalbackup/tgsetattachment.cc000066400000000000000000000134231476450434500241020ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../attachmentmetadata/attachmentmetadata.h" bool SignalBackup::tgSetAttachment(SqliteDB::QueryResults const &message_data, std::string const &datapath, long long int r, long long int new_msg_id) { std::string photo = message_data.valueAsString(r, "photo"); std::string file = message_data.valueAsString(r, "file"); std::string vcard = message_data.valueAsString(r, "contact_vcard"); if (photo.empty() && file.empty() && vcard.empty()) return true; bool attachmentadded = false; for (std::string const &a : {photo, file, vcard}) { if (a.empty()) continue; //std::cout << "Attachment: " << datapath << a << std::endl; std::string filename_for_db(std::filesystem::path(a).filename().string()); //std::cout << filename_for_db << std::endl; AttachmentMetadata amd = AttachmentMetadata::getAttachmentMetaData(datapath + a); if (amd.filename.empty() || amd.filesize == 0) { Logger::warning("Failed to get attachment data. Skipping."); continue; } std::string ct = message_data.valueAsString(r, "mime_type"); if (ct.empty()) ct = amd.filetype; if (a == vcard) ct = "text/vcard"; if (ct.empty()) { Logger::warning("Attachment has no mime_type. Skipping."); continue; } long long int w = message_data.valueAsInt(r, "width", 0); long long int h = message_data.valueAsInt(r, "height", 0); long long int voice_note = 0; if (message_data.valueAsString(r, "media_type") == "voice_message") voice_note = 1; // get unqiue id / or -1 long long int unique_id = d_database.getSingleResultAs("SELECT " + d_mms_date_sent + " FROM " + d_mms_table + " WHERE _id = ?", new_msg_id, -1); if (d_database.tableContainsColumn(d_part_table, "unique_id") && unique_id == -1) { Logger::warning("Failed to get unique_id for attachment. Skipping."); continue; } //insert into part std::any retval; if (!insertRow(d_part_table, {{d_part_mid, new_msg_id}, {d_part_ct, ct}, {d_part_pending, 0}, {"data_size", amd.filesize}, {(d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id" : ""), unique_id}, {(!filename_for_db.empty() ? "file_name" : ""), filename_for_db}, {"voice_note", voice_note}, {"width", amd.width == -1 ? w : amd.width}, {"height", amd.height == -1 ? h : amd.height}, {"quote", 0}, {(d_database.tableContainsColumn(d_part_table, "data_hash") ? "data_hash" : ""), amd.hash}, {(d_database.tableContainsColumn(d_part_table, "data_hash_start") ? "data_hash_start" : ""), amd.hash}, {(d_database.tableContainsColumn(d_part_table, "data_hash_end") ? "data_hash_end" : ""), amd.hash}, //{"upload_timestamp", results_attachment_data.value(0, "upload_timestamp")}, // will be 0 on sticker //{"cdn_number", results_attachment_data.value(0, "cdn_number")}, // will be 0 on sticker, usually 0 or 2, but I dont know what it means //{"file_name", results_attachment_data.value(0, "file_name")}}, }, "_id", &retval)) { Logger::error("Inserting part-data"); continue; } long long int new_part_id = std::any_cast(retval); // now the actual attachment DeepCopyingUniquePtr new_attachment_frame; if (setFrameFromStrings(&new_attachment_frame, std::vector {"ROWID:uint64:" + bepaald::toString(new_part_id), (d_database.tableContainsColumn(d_part_table, "unique_id") ? "ATTACHMENTID:uint64:" + bepaald::toString(unique_id) : ""), "LENGTH:uint32:" + bepaald::toString(amd.filesize)})) { //new_attachment_frame->setLazyDataRAW(amd.filesize, datapath + a); new_attachment_frame->setReader(new RawFileAttachmentReader(datapath + a)); d_attachments.emplace(std::make_pair(new_part_id, (d_database.tableContainsColumn(d_part_table, "unique_id") ? unique_id : -1)), new_attachment_frame.release()); } else { Logger::error("Failed to create AttachmentFrame for data"); Logger::error_indent("rowid : ", new_part_id); Logger::error_indent("attachmentid: ", unique_id); Logger::error_indent("length : ", amd.filesize); Logger::error_indent("path : ", datapath, a); // try to remove the inserted part entry: d_database.exec("DELETE FROM " + d_part_table + " WHERE _id = ?", new_part_id); continue; } //std::cout << "ADDED ATTACHMENT TO MESSAGE:" << std::endl; //d_database.prettyPrint("SELECT _id, body, " + d_mms_date_sent + " FROM " + d_mms_table + " WHERE _id = ?", new_msg_id); attachmentadded = true; } return attachmentadded; } signalbackup-tools-20250313-1/signalbackup/tgsetbodyranges.cc000066400000000000000000000070241476450434500241070ustar00rootroot00000000000000/* Copyright (C) 2023-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::tgSetBodyRanges(std::string const &bodyjson, long long int message_id) { //std::cout << "bodydata: " << bodyjson << std::endl; long long int fragments = d_database.getSingleResultAs("SELECT json_array_length(?, '$')", bodyjson, -1); if (fragments == -1) { Logger::error("Failed to get number of text fragments from message body. Body data: '" + bodyjson + "'"); return false; } long long int currentpos = 0; BodyRanges bodyrangelist; SqliteDB::QueryResults br; for (unsigned int i = 0; i < fragments; ++i) { if (!d_database.exec("SELECT " "json_extract(?1, '$[" + bepaald::toString(i) + "].text') AS text, " "json_extract(?1, '$[" + bepaald::toString(i) + "].type') AS type", bodyjson, &br) || br.rows() != 1) { Logger::error("Failed to get text fragment (", i, ") from message body. Body data: '" + bodyjson + "'"); return false; } BodyRange bodyrange; std::string bodyfrag = br("text"); unsigned int fraglen = 0; for (unsigned int c = 0; c < bodyfrag.length(); c += bytesToUtf8CharSize(bodyfrag, c)) fraglen += utf16CharSize(bodyfrag, c); if (br("type") == "plain") [[likely]] ; else if (bepaald::contains(std::vector{"bold", "italic", "spoiler", "strikethrough", "code"}, br("type"))) { // start pos if (currentpos != 0) bodyrange.addField<1>(currentpos); // length bodyrange.addField<2>(fraglen); // type if (br("type") == "bold") bodyrange.addField<4>(0); else if (br("type") == "italic") bodyrange.addField<4>(1); else if (br("type") == "spoiler") bodyrange.addField<4>(2); else if (br("type") == "strikethrough") bodyrange.addField<4>(3); else if (br("type") == "code") // monospace bodyrange.addField<4>(4); // add to list bodyrangelist.addField<1>(bodyrange); } else if (br("type") == "underline") Logger::warnOnce("Underline text styling is not supported by Signal"); else if (br("type") == "link") Logger::warnOnce("'Link' text styling is not supported by Signal"); else Logger::warnOnce("(unknown text styling: '" + br("type") + "')"); currentpos += fraglen; } //bodyrangelist.print(); // set it if (bodyrangelist.size()) { std::pair bodyrangesdata(bodyrangelist.data(), bodyrangelist.size()); if (!d_database.exec("UPDATE " + d_mms_table + " SET " + d_mms_ranges + " = ? WHERE rowid = ?", {bodyrangesdata, message_id}) || d_database.changed() != 1) { Logger::error("Failed to set body ranges for message. Body data: '" + bodyjson + "'"); return false; } } return true; } signalbackup-tools-20250313-1/signalbackup/tgsetquote.cc000066400000000000000000000145601476450434500231120ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::tgSetQuote(long long int quoted_message_id, long long int new_msg_id) { SqliteDB::QueryResults quote_res; if (!d_database.exec("SELECT body, " + d_mms_recipient_id + ", " + d_mms_date_sent + " FROM " + d_mms_table + " " "WHERE _id = ?", quoted_message_id, "e_res) || quote_res.rows() != 1) { Logger::warning("Failed to get quote data."); return false; } long long int quote_id = quote_res.valueAsInt(0, d_mms_date_sent, -1); long long int quote_author = quote_res.valueAsInt(0, d_mms_recipient_id, -1); std::string quote_body = quote_res.valueAsString(0, "body"); if (quote_id == -1 || quote_author == -1) { Logger::warning("Failed to get quote data."); return false; } if (!d_database.exec("UPDATE " + d_mms_table + " SET " "quote_id = ?, " "quote_author = ?, " "quote_body = ?, " "quote_type = 0, " "quote_missing = 0 " "WHERE _id = ?", {quote_id, quote_author, (quote_res.isNull(0, "body") ? std::any(nullptr) : std::any(quote_body)), new_msg_id})) { Logger::warning("Failed to set quote data."); return false; } // set quoted-attachment SqliteDB::QueryResults quote_att_res; std::string query = "SELECT _id, " + d_part_mid + ", " + d_part_ct + ", " + d_part_pending + ", data_size, " + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id" : "-1 AS unique_id") + ", voice_note, width, height, quote"; if (d_database.tableContainsColumn(d_part_table, "data_hash")) query += ", data_hash"; else if (d_database.tableContainsColumn(d_part_table, "data_hash_start") && d_database.tableContainsColumn(d_part_table, "data_hash_end")) query += ", data_hash_start, data_hash_end"; query += " FROM " + d_part_table + " WHERE " + d_part_mid + " = ?"; if (d_database.exec(query, quoted_message_id, "e_att_res) && quote_att_res.rows() >= 1) { //quote_att_res.prettyPrint(); // get unique id for new attachments long long int unique_id = d_database.getSingleResultAs("SELECT " + d_mms_date_sent + " FROM " + d_mms_table + " WHERE _id = ?", new_msg_id, -1); if (d_database.tableContainsColumn(d_part_table, "unique_id") && unique_id == -1) { Logger::warning("Failed to get unique_id for attachment in quote. Skipping."); return false; } // get unique id for existing attachments (to retrieve attachment from d_attachments)... long long int quoted_unique_id = quote_att_res.valueAsInt(0, "unique_id", -1); if (d_database.tableContainsColumn(d_part_table, "unique_id") && unique_id == -1) { Logger::warning("Failed to get unique_id for quoted attachment. Skipping."); return false; } for (unsigned int i = 0; i < quote_att_res.rows(); ++i) { // set sql data std::any retval; if (!insertRow(d_part_table, {{d_part_mid, new_msg_id}, {d_part_ct, quote_att_res.value(i, d_part_ct)}, {d_part_pending, quote_att_res.value(i, d_part_pending)}, {"data_size", quote_att_res.value(i, "data_size")}, {(d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id" : ""), unique_id}, {"voice_note", quote_att_res.value(i, "voice_note")}, {"width", quote_att_res.value(i, "width")}, {"height", quote_att_res.value(i, "height")}, {"quote", 1}, {(d_database.tableContainsColumn(d_part_table, "data_hash") ? "data_hash" : ""), (d_database.tableContainsColumn(d_part_table, "data_hash") ? quote_att_res.value(i, "data_hash") : std::any())}, {(d_database.tableContainsColumn(d_part_table, "data_hash_start") ? "data_hash_start" : ""), (d_database.tableContainsColumn(d_part_table, "data_hash_start") ? quote_att_res.value(i, "data_hash_start") : std::any())}, {(d_database.tableContainsColumn(d_part_table, "data_hash_end") ? "data_hash_end" : ""), (d_database.tableContainsColumn(d_part_table, "data_hash_end") ? quote_att_res.value(i, "data_hash_end") : std::any())}}, "_id", &retval)) { Logger::error("Inserting part-data"); continue; } long long int new_part_id = std::any_cast(retval); // add attachment if (!bepaald::contains(d_attachments, std::pair{quote_att_res.valueAsInt(i, "_id", -1), quoted_unique_id})) { Logger::warning("Failed to find original of quoted attachment. Skipping."); d_database.exec("DELETE FROM " + d_part_table + " WHERE _id = ?", new_part_id); continue; } std::unique_ptr new_attachment_frame(new AttachmentFrame(*d_attachments.at({quote_att_res.valueAsInt(i, "_id", -1), quoted_unique_id}).get())); //std::cout << "Creating new attachment: " << quote_att_res.valueAsInt(i, "_id", -1) << ", " << quoted_unique_id << " -> " << new_part_id << ", " << unique_id << std::endl; new_attachment_frame->setRowId(new_part_id); new_attachment_frame->setAttachmentId(unique_id); //new_attachment_frame->printInfo(); d_attachments.emplace(std::make_pair(new_part_id, unique_id), new_attachment_frame.release()); } } return true; } signalbackup-tools-20250313-1/signalbackup/unescapexmlstring.cc000066400000000000000000000065531476450434500244640ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include bool SignalBackup::unescapeXmlString(std::string *s) const { //std::cout << " IN: " << *s << std::endl; if (s->find('&') == std::string::npos) return true; bepaald::replaceAll(s, "&", "&"); bepaald::replaceAll(s, "'", "'"); bepaald::replaceAll(s, """, "\""); bepaald::replaceAll(s, "<", "<"); bepaald::replaceAll(s, ">", ">"); std::regex entity_regex("&#(x?[0-9a-fA-F]+);"); std::smatch m; std::string searchstr(*s); int codepointcomplete = 0; uint32_t unicode_codepoint = 0; int match_position = 0; int match_length = 0; while (std::regex_search(searchstr, m, entity_regex)) { // digits std::string utf16str(m[1]); // value-part of the match uint32_t utf16 = 0; if (codepointcomplete == 1) // we're here because we need more data match_length += m.length(0); else { match_position = m.position(0); // pos and length of entire match_length = m.length(0); // match (including &#;) } // check leading x, interpret as hex... if (utf16str[0] == 'x') [[unlikely]] // I dont think this exists in Signal plaintext { // backups but it does in SMS Backup & Restore utf16str = utf16str.substr(1); utf16 = bepaald::toNumberFromHex(utf16str); } else utf16 = bepaald::toNumber(utf16str); //std::cout << "found utf16: " << utf16 << " (" << utf16str << ")" << std::endl; if (utf16 > 0xFFFF) [[unlikely]] // SMS Backup & Restore stores as unicode32 { codepointcomplete = 0; unicode_codepoint = utf16; } else codepointcomplete = utf16ToUnicodeCodepoint(utf16, &unicode_codepoint); if (codepointcomplete == 1) { //std::cout << "requested more" << std::endl; searchstr = m.suffix(); continue; } if (codepointcomplete == -1) [[unlikely]] { Logger::warning("Failed to fully un-escape XML string: '", *s, "'"); return false; } //std::cout << "Codepoint: " << unicode_codepoint << " UTF8: "; std::string utf8(unicodeToUtf8(unicode_codepoint)); //std::cout << bepaald::bytesToHexString(reinterpret_cast(utf8.data()), utf8.size()) << std::endl; // codepointcomplete == 0 : do the replace... start search at top... s->replace(match_position, match_length, unicodeToUtf8(unicode_codepoint)); unicode_codepoint = 0; searchstr = *s; } if (codepointcomplete != 0) { Logger::warning("Failed fully to un-escape XML string: '", *s, "'"); return false; } //std::cout << "OUT: " << *s << std::endl; return true; } signalbackup-tools-20250313-1/signalbackup/unicodetoutf8.cc000066400000000000000000000033411476450434500235010ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" std::string SignalBackup::unicodeToUtf8(uint32_t unicode) const { std::string utf8; if (unicode <= 0x7F) // 1 byte char utf8 = static_cast(unicode); else if (unicode <= 0x7FF) { utf8 = static_cast((unicode >> 6) | 0b11000000); utf8 += static_cast((unicode & 0b00111111) | 0b10000000); } else if (unicode <= 0xFFFF) { utf8 = static_cast((unicode >> 12) | 0b11100000); utf8 += static_cast(((unicode >> 6) & 0b00111111) | 0b10000000); utf8 += static_cast((unicode & 0b00111111) | 0b10000000); } else // if (unicode <= 0x10FFF) { utf8 = static_cast((unicode >> 18) | 0b11110000); utf8 += static_cast(((unicode >> 12) & 0b00111111) | 0b10000000); utf8 += static_cast(((unicode >> 6) & 0b00111111) | 0b10000000); utf8 += static_cast((unicode & 0b00111111) | 0b10000000); } return utf8; } signalbackup-tools-20250313-1/signalbackup/updateavatars.cc000066400000000000000000000024461476450434500235520ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::updateAvatars(long long int id1, long long int id2) // if id2 == -1, id1 is an offset { // else, change id1 into id2 for (unsigned int i = 0; i < d_avatars.size(); ++i) { int oldrid = bepaald::toNumber(d_avatars[i].first); if (oldrid == id1 || id2 == -1) { d_avatars[i].first = bepaald::toString(id2 == -1 ? oldrid + id1 : id2); d_avatars[i].second->setRecipient(bepaald::toString(id2 == -1 ? oldrid + id1 : id2)); } } } signalbackup-tools-20250313-1/signalbackup/updategroupmembers.cc000066400000000000000000000050171476450434500246150ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::updateGroupMembers(long long int id1, long long int id2) const // if id2 == -1, id1 is an offset { // else, change id1 into id2 for (auto const &members : {"members"s, d_groups_v1_members}) { if (!d_database.tableContainsColumn("groups", members)) continue; // get group members SqliteDB::QueryResults results; bool changed = false; d_database.exec("SELECT _id,"s + members + " FROM groups WHERE " + members + " IS NOT NULL", &results); //d_database.prettyPrint("SELECT _id,members FROM groups"); for (unsigned int i = 0; i < results.rows(); ++i) { long long int gid = results.getValueAs(i, "_id"); std::string membersstr = results.getValueAs(i, members); std::vector membersvec; std::stringstream ss(membersstr); while (ss.good()) { std::string substr; std::getline(ss, substr, ','); membersvec.emplace_back(bepaald::toNumber(substr)); } std::string newmembers; for (unsigned int m = 0; m < membersvec.size(); ++m) { if (m > 0) newmembers += ","; newmembers += bepaald::toString((id2 != -1 && membersvec[m] == id1) ? id2 : ((id2 == -1) ? membersvec[m] + id1 : membersvec[m])); } if (membersstr != newmembers) { changed = true; d_database.exec("UPDATE groups SET "s + members + " = ? WHERE _id == ?", {newmembers, gid}); if (d_verbose) Logger::message(" Updated groups.", members, ", changed: ", membersstr, " -> ", newmembers); } if (d_verbose && changed) Logger::message(" Updated groups.", members, ", changed: 0"); } } } signalbackup-tools-20250313-1/signalbackup/updategv1migrationmessage.cc000066400000000000000000000064511476450434500260650ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" // in groups, during the v1 -> v2 update, members may have been removed from the group, these messages // are of type "GV1_MIGRATION_TYPE" and have a body that looks like '_id,_id,...|_id,_id,_id,...' (I think, I have // not seen one with more than 1 id). These id_s must also be updated. void SignalBackup::updateGV1MigrationMessage(long long int id1, long long int id2) const // if id2 == -1, id1 is an offset { // else, change id1 into id2 SqliteDB::QueryResults results; int changed = 0; std::string table = d_database.containsTable("sms") ? "sms" : d_mms_table; if (d_database.exec("SELECT _id,body FROM " + table + " WHERE type == ? AND body IS NOT NULL", bepaald::toString(Types::GV1_MIGRATION_TYPE), &results)) { //results.prettyPrint(); for (unsigned int i = 0; i < results.rows(); ++i) { if (results.valueHasType(i, "body")) { //std::cout << results.getValueAs(i, "body") << std::endl; std::string body = results.getValueAs(i, "body"); std::string output; std::string tmp; // to hold part of number while reading unsigned int body_idx = 0; while (true) { if (body_idx >= body.length() || !std::isdigit(body[body_idx])) { // deal with any number we have if (tmp.size()) { long long int id = bepaald::toNumber(tmp); //std::cout << "FOUND ID: " << id << std::endl; if (id2 == -1) id += id1; else if (id == id1) id = id2; output += bepaald::toString(id); tmp.clear(); } // add non-digit-char //std::cout << "ADD NON-DIGIT: " << body[body_idx] << std::endl; if (body_idx < body.length()) output += body[body_idx]; } else tmp += body[body_idx]; ++body_idx; if (body_idx > body.length()) break; } //std::cout << "NEW OUTPUT: " << output << std::endl; if (body != output) { long long int sms_id = results.getValueAs(i, "_id"); d_database.exec("UPDATE " + table + " SET body = ? WHERE _id == ?", {output, sms_id}); ++changed; } } } } if (d_verbose && changed > 0) Logger::message(" update ", table, ".body (GV1_MIGRATION), changed: ", changed); } signalbackup-tools-20250313-1/signalbackup/updatereactionauthors.cc000066400000000000000000000052031476450434500253150ustar00rootroot00000000000000/* Copyright (C) 2022-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" // update (old-style)reaction authors // current (and future) databases do not have reactions in the [s|m]ms tables, // but in their own table called 'reaction'. void SignalBackup::updateReactionAuthors(long long int id1, long long int id2) const // if id2 == -1, id1 is an offset { // else, change id1 into id2 for (auto const &msgtable : {"sms"s, d_mms_table}) { if (d_database.tableContainsColumn(msgtable, "reactions")) { int changedcount = 0; SqliteDB::QueryResults results; d_database.exec("SELECT _id, reactions FROM "s + msgtable + " WHERE reactions IS NOT NULL", &results); for (unsigned int i = 0; i < results.rows(); ++i) { bool changed = false; ReactionList reactions(results.getValueAs, size_t>>(i, "reactions")); for (unsigned int j = 0; j < reactions.numReactions(); ++j) { //std::cout << "Updating reaction author (" << msgtable << ") : " << reactions.getAuthor(j) << "..." << std::endl; if (id2 == -1) { reactions.setAuthor(j, reactions.getAuthor(j) + id1); ++changedcount; changed = true; } else if (reactions.getAuthor(j) == static_cast(id1)) { reactions.setAuthor(j, id2); ++changedcount; changed = true; } } if (changed) d_database.exec("UPDATE "s + msgtable + " SET reactions = ? WHERE _id = ?", {std::make_pair(reactions.data(), static_cast(reactions.size())), results.getValueAs(i, "_id")}); } if (d_verbose) [[unlikely]] Logger::message(" Updated ", changedcount, " ", msgtable, ".reaction authors"); } } } signalbackup-tools-20250313-1/signalbackup/updaterecipientid.cc000066400000000000000000000374301476450434500244110ustar00rootroot00000000000000/* Copyright (C) 2020-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::updateRecipientId(long long int targetid, long long int sourceid) { Logger::message_start(" Mapping ", sourceid, " -> ", targetid); if (d_database.tableContainsColumn("recipient", d_recipient_aci, d_recipient_e164, "group_id", "distribution_list_id", "notification_channel")) { SqliteDB::QueryResults r; if (d_database.exec("SELECT " "CASE WHEN NULLIF(" + d_recipient_aci + ", '') IS NULL THEN '' ELSE 'u' END || " "CASE WHEN NULLIF(" + d_recipient_e164 + ", '') IS NULL THEN '' ELSE 'p' END || " "CASE WHEN NULLIF(group_id, '') IS NULL THEN '' ELSE 'g' END || " "CASE WHEN NULLIF(distribution_list_id, '') IS NULL THEN '' ELSE 'd' END || " "CASE WHEN NULLIF(notification_channel, '') IS NULL THEN '' ELSE 'n' END " "AS recipient_type FROM recipient WHERE _id = ?", sourceid, &r)) { if (r.rows()) Logger::message_continue(" (", r.valueAsString(0, "recipient_type"), ")"); else Logger::message_continue(" (x)"); } } else if (d_database.tableContainsColumn("recipient", d_recipient_aci, d_recipient_e164, "group_id")) { SqliteDB::QueryResults r; if (d_database.exec("SELECT " "CASE WHEN NULLIF(" + d_recipient_aci + ", '') IS NULL THEN '' ELSE 'u' END || " "CASE WHEN NULLIF(" + d_recipient_e164 + ", '') IS NULL THEN '' ELSE 'p' END || " "CASE WHEN NULLIF(group_id, '') IS NULL THEN '' ELSE 'g' END " "AS recipient_type FROM recipient WHERE _id = ?", sourceid, &r)) { if (r.rows()) Logger::message_continue(" (", r.valueAsString(0, "recipient_type"), ")"); else Logger::message_continue(" (x)"); } } Logger::message_end(); for (auto const &dbl : s_databaselinks) { if (dbl.table != "recipient") continue; if (!d_database.containsTable(dbl.table)) [[unlikely]] continue; for (auto const &c : dbl.connections) if (d_databaseversion >= c.mindbvversion && d_databaseversion <= c.maxdbvversion && d_database.containsTable(c.table) && d_database.tableContainsColumn(c.table, c.column)) { d_database.exec("UPDATE " + c.table + " SET " + c.column + " = ? WHERE " + c.column + " = ?", {targetid, sourceid}); if (d_verbose) Logger::message(" update table '" + c.table + "', changed: ", d_database.changed()); } } updateGV1MigrationMessage(sourceid, targetid); updateGroupMembers(sourceid, targetid); updateReactionAuthors(sourceid, targetid); updateAvatars(sourceid, targetid); updateSnippetExtrasRecipient(sourceid, targetid); } // // OLD VERSION // void SignalBackup::updateRecipientId(long long int targetid, long long int sourceid, bool verbose) // { // using namespace std::string_literals; // std::cout << " Mapping " << sourceid << " -> " << targetid << std::endl; // d_database.exec("UPDATE sms SET address = ? WHERE address = ?", {targetid, sourceid}); // if (verbose) std::cout << " update sms, changed: " << d_database.changed() << std::endl; // d_database.exec("UPDATE mms SET address = ? WHERE address = ?", {targetid, sourceid}); // if (verbose) std::cout << " update mms, changed: " << d_database.changed() << std::endl; // d_database.exec("UPDATE mms SET quote_author = ? WHERE quote_author = ?", {targetid, sourceid}); // if (verbose) std::cout << " update mms.quote_author, changed: " << d_database.changed() << std::endl; // d_database.exec("UPDATE identities SET address = ? WHERE address = ?", {targetid, sourceid}); // if (verbose) std::cout << " update identities, changed: " << d_database.changed() << std::endl; // d_database.exec("UPDATE group_receipts SET address = ? WHERE address = ?", {targetid, sourceid}); // if (verbose) std::cout << " update group_receipts, changed: " << d_database.changed() << std::endl; // d_database.exec("UPDATE thread SET " + d_thread_recipient_id + " = ? WHERE " + d_thread_recipient_id + " = ?", {targetid, sourceid}); // if (verbose) std::cout << " update thread, changed: " << d_database.changed() << std::endl; // d_database.exec("UPDATE groups SET recipient_id = ? WHERE recipient_id = ?", {targetid, sourceid}); // if (verbose) std::cout << " update groups, changed: " << d_database.changed() << std::endl; // d_database.exec("UPDATE sessions SET address = ? WHERE address = ?", {targetid, sourceid}); // if (verbose) std::cout << " update sessions, changed: " << d_database.changed() << std::endl; // // if (d_database.containsTable("remapped_recipients")) // // { // // d_database.exec("UPDATE remapped_recipients SET old_id = ? WHERE old_id = ?", {targetid, sourceid}); // // if (verbose) std::cout << " update remapped_recipients.old_id, changed: " << d_database.changed() << std::endl; // // d_database.exec("UPDATE remapped_recipients SET new_id = ? WHERE new_id = ?", {targetid, sourceid}); // // if (verbose) std::cout << " update remapped_recipient.new_id, changed: " << d_database.changed() << std::endl; // // } // if (d_database.containsTable("mention")) // { // d_database.exec("UPDATE mention SET recipient_id = ? WHERE recipient_id = ?", {targetid, sourceid}); // if (verbose) std::cout << " update mention, changed: " << d_database.changed() << std::endl; // } // if (d_database.containsTable("msl_recipient")) // { // d_database.exec("UPDATE msl_recipient SET recipient_id = ? WHERE recipient_id = ?", {targetid, sourceid}); // if (verbose) std::cout << " update msl_recipient, changed: " << d_database.changed() << std::endl; // } // if (d_database.containsTable("reaction")) // dbv >= 121 // { // d_database.exec("UPDATE reaction SET author_id = ? WHERE author_id = ?", {targetid, sourceid}); // if (verbose) std::cout << " update reaction, changed: " << d_database.changed() << std::endl; // } // if (d_database.containsTable("notification_profile_allowed_members")) // dbv >= 121 // { // d_database.exec("UPDATE notification_profile_allowed_members SET recipient_id = ? WHERE recipient_id = ?", {targetid, sourceid}); // if (verbose) std::cout << " update notification_profile_allowed_members, changed: " << d_database.changed() << std::endl; // } // // recipient_id can also mentioned in the body of group v1 -> v2 migration message, when recipient // // was thrown out of group. // SqliteDB::QueryResults results; // bool changedsomething = false; // if (d_database.exec("SELECT _id,body FROM sms WHERE type == ?", bepaald::toString(Types::GV1_MIGRATION_TYPE), &results)) // { // //results.prettyPrint(); // for (unsigned int i = 0; i < results.rows(); ++i) // { // if (results.valueHasType(i, "body")) // { // //std::cout << " ** FROM TO **" << std::endl; // //std::cout << results.getValueAs(i, "body") << std::endl; // std::string body = results.getValueAs(i, "body"); // std::string output; // std::string tmp; // to hold part of number while reading // unsigned int body_idx = 0; // while (true) // { // if (!std::isdigit(body[body_idx]) || body_idx >= body.length()) // { // // deal with any number we have // if (tmp.size()) // { // int id = bepaald::toNumber(tmp); // if (id == sourceid) // { // id = targetid; // if (verbose) std::cout << " updated gv1_migration message" << std::endl; // } // output += bepaald::toString(id); // tmp.clear(); // } // // add non-digit-char // if (body_idx < body.length()) // output += body[body_idx]; // } // else // tmp += body[body_idx]; // ++body_idx; // if (body_idx > body.length()) // break; // } // //std::cout << output << std::endl; // if (body != output) // { // long long int sms_id = results.getValueAs(i, "_id"); // d_database.exec("UPDATE sms SET body = ? WHERE _id == ?", {output, sms_id}); // changedsomething = true; // if (verbose) std::cout << " update sms.body (GV1_MIGRATION), changed: " << d_database.changed() << std::endl; // } // } // } // if (!changedsomething) // if (verbose) std::cout << " update sms.body (GV1_MIGRATION), changed: 0" << std::endl; // } // // get group members: // for (auto const &members : {"members"s, d_groups_v1_members}) // { // if (!d_database.tableContainsColumn("recipient", members)) // continue; // SqliteDB::QueryResults results2; // changedsomething = false; // d_database.exec("SELECT _id, "s + members + " FROM groups WHERE " + members + " IS NOT NULL", &results2); // //std::cout << "RESULTS:" << std::endl; // //results2.prettyPrint(); // for (unsigned int i = 0; i < results2.rows(); ++i) // { // long long int gid = results2.getValueAs(i, "_id"); // std::string membersstr = results2.getValueAs(i, members); // std::vector membersvec; // std::stringstream ss(membersstr); // while (ss.good()) // { // std::string substr; // std::getline(ss, substr, ','); // membersvec.emplace_back(bepaald::toNumber(substr)); // } // std::string newmembers; // for (unsigned int m = 0; m < membersvec.size(); ++m) // newmembers += (m == 0) ? // bepaald::toString((membersvec[m] == sourceid) ? targetid : membersvec[m]) : // ("," + bepaald::toString((membersvec[m] == sourceid) ? targetid : membersvec[m])); // if (membersstr != newmembers) // { // changedsomething = true; // d_database.exec("UPDATE groups SET "s + members + " = ? WHERE _id == ?", {newmembers, gid}); // if (verbose) std::cout << " update groups." << members << ", changed: " << membersstr << " -> " << newmembers << std::endl; // } // } // if (!changedsomething) // if (verbose) std::cout << " update groups." << members << ", changed: 0" << std::endl; // } // //d_database.prettyPrint("SELECT _id,members FROM groups"); // // UPDATE 'reactions' field in sms and mms tables.... // for (auto const &msgtable : {"sms", "mms"}) // { // if (d_database.tableContainsColumn(msgtable, "reactions")) // { // SqliteDB::QueryResults res; // d_database.exec("SELECT _id, reactions FROM "s + msgtable + " WHERE reactions IS NOT NULL", &res); // for (unsigned int i = 0; i < res.rows(); ++i) // { // changedsomething = false; // ReactionList reactions(res.getValueAs, size_t>>(i, "reactions")); // for (unsigned int j = 0; j < reactions.numReactions(); ++j) // { // //std::cout << "Dealing with " << msgtable << " reaction author: " << reactions.getAuthor(j) << std::endl; // if (reactions.getAuthor(j) == static_cast(sourceid)) // { // reactions.setAuthor(j, targetid); // changedsomething = true; // if (verbose) std::cout << " updated " << msgtable << " reaction" << std::endl; // } // } // if (changedsomething) // d_database.exec("UPDATE "s + msgtable + " SET reactions = ? WHERE _id = ?", {std::make_pair(reactions.data(), static_cast(reactions.size())), // res.getValueAs(i, "_id")}); // } // } // } // } /* void SignalBackup::updateRecipientId(long long int targetid, std::string const &identifier) { //std::cout << __FUNCTION__ << std::endl; // CALLED ON SOURCE // the targetid should already be guaranteed to not exist in source as this is called // after makeIdsUnique() & friends if (d_databaseversion < 24) // recipient table does not exist return; // get the current (to be deleted) recipient._id for this identifier (=phone,group_id,possibly uuid) SqliteDB::QueryResults results; d_database.exec("SELECT _id FROM recipient WHERE COALESCE(uuid,phone,group_id) = '" + identifier + "'", &results); if (results.rows() > 1) { std::cout << "ERROR! Unexpectedly got multiple results" << std::endl; return; } // the target recipient was not found in this source db, nothing to do. if (results.rows() == 0) return; long long int sourceid = results.getValueAs(0, "_id"); //std::cout << " Mapping " << sourceid << " -> " << targetid << " (" << ident << ")" << std::endl; updateRecipientId(targetid, sourceid); } */ void SignalBackup::updateRecipientId(long long int targetid, RecipientIdentification const &rec_id) { //std::cout << __FUNCTION__ << std::endl; // CALLED ON SOURCE // the targetid should already be guaranteed to not exist in source as this is called // after makeIdsUnique() & friends if (d_databaseversion < 24) // recipient table does not exist return; // get the current (to be deleted) recipient._id for this identifier (=phone,group_id,possibly uuid) SqliteDB::QueryResults results; if (d_database.tableContainsColumn("recipient", d_recipient_aci, d_recipient_e164, "group_id", "distribution_list_id", "notification_channel")) { long long int distribution_list_id = d_database.getSingleResultAs("SELECT _id FROM distribution_list WHERE distribution_id = ?", rec_id.distribution_id, -1); d_database.exec("SELECT _id FROM recipient WHERE " "(" + d_recipient_aci + " IS NOT NULL AND " + d_recipient_aci + " IS ?) OR " "(" + d_recipient_e164 + " IS NOT NULL AND " + d_recipient_e164 + " IS ?) OR " "(group_id IS NOT NULL AND group_id IS ?) OR " "(distribution_list_id IS NOT NULL AND distribution_list_id = ?)", {rec_id.uuid, rec_id.phone, rec_id.group_id, distribution_list_id}, &results); } else d_database.exec("SELECT _id FROM recipient WHERE " "(" + d_recipient_aci + " IS NOT NULL AND " + d_recipient_aci + " IS ?) OR " "(" + d_recipient_e164 + " IS NOT NULL AND " + d_recipient_e164 + " IS ?) OR " "(group_id IS NOT NULL AND group_id IS ?)", {rec_id.uuid, rec_id.phone, rec_id.group_id}, &results); if (results.rows() > 1) { Logger::error("Unexpectedly got multiple results"); return; } // the target recipient was not found in this source db, nothing to do. if (results.rows() == 0) return; long long int sourceid = results.getValueAs(0, "_id"); //std::cout << " Mapping " << sourceid << " -> " << targetid << " (" << ident << ")" << std::endl; updateRecipientId(targetid, sourceid); } signalbackup-tools-20250313-1/signalbackup/updaterows.cc000066400000000000000000000064661476450434500231110ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #if __cpp_lib_ranges >= 201911L #include #endif bool SignalBackup::updateRows(std::string const &table, std::vector> data, std::vector> whereclause, std::string const &returnfield, std::any *returnvalue) const { // check if columns exist... for (auto it = data.begin(); it != data.end();) { if (it->first.empty()) it = data.erase(it); else if (!d_database.tableContainsColumn(table, it->first)) { Logger::warning("Table '", table, "' does not contain any column '", it->first, "'. Removing"); it = data.erase(it); } else ++it; } std::string query = "UPDATE " + table + " SET "; for (unsigned int i = 0; i < data.size(); ++i) query += data[i].first + (i < data.size() -1 ? " = ?, " : " = ?"); if (whereclause.size()) { query += " WHERE "; for (unsigned int i = 0; i < whereclause.size(); ++i) query += whereclause[i].first + (i < whereclause.size() -1 ? " = ? AND " : " = ?"); } if (!returnfield.empty() && returnvalue) { #if SQLITE_VERSION_NUMBER < 3035000 // RETURNING was not available prior to 3.35.0 Logger::warning("Your SQLite version does not support the RETURNING clause."); Logger::warning_indent("This will likely not end well. Please update your SQLite"); Logger::warning_indent("to a more recent version"); #else query += " RETURNING " + returnfield; #endif } SqliteDB::QueryResults res; // when concat_view gets implemented... // - https://en.cppreference.com/w/cpp/utility/feature_test #if __cpp_lib_ranges >= 201911L && __cpp_lib_ranges_concat >= 202403L #warning this is currently untested bool ret = d_database.exec(query, std::ranges::views::values(std::ranges::views::concat(data, whereclause)), &res, d_verbose); #else std::vector values; std::transform(data.begin(), data.end(), std::back_inserter(values), [](auto const &pair){ return pair.second; }); std::transform(whereclause.begin(), whereclause.end(), std::back_inserter(values), [](auto const &pair){ return pair.second; }); bool ret = d_database.exec(query, values, &res, d_verbose); #endif if (ret && !returnfield.empty() && returnvalue && res.rows() && res.columns()) { if (res.rows() > 1 || res.columns() > 1) [[unlikely]] Logger::warning("Requested return of '", returnfield, "', but query returned multiple results. Returning first."); *returnvalue = res.value(0, 0); } return ret; } signalbackup-tools-20250313-1/signalbackup/updatesnippetextrasrecipient.cc000066400000000000000000000054161476450434500267250ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" void SignalBackup::updateSnippetExtrasRecipient(long long int id1, long long int id2) const // if id2 == -1, id1 is an offset { // else, change id1 into id2 if (d_database.tableContainsColumn("thread", "snippet_extras")) { if (id2 == -1) { d_database.exec("UPDATE thread SET snippet_extras = " "json_set(snippet_extras, '$.individualRecipientId', CAST(json_extract(snippet_extras, '$.individualRecipientId') + ? AS text))", id1); int changed = d_database.changed(); if (d_verbose && changed) [[unlikely]] Logger::message(" Updated ", changed, " individualrecipientids in thread.snippet_extras"); d_database.exec("UPDATE thread SET snippet_extras = " "json_set(snippet_extras, '$.groupAddedBy', CAST(json_extract(snippet_extras, '$.groupAddedBy') + ? AS text))", id1); changed = d_database.changed(); if (d_verbose && changed) [[unlikely]] Logger::message(" Updated ", changed, " groupaddedby-ids in thread.snippet_extras"); } else { d_database.exec("UPDATE thread SET snippet_extras = " "json_set(snippet_extras, '$.individualRecipientId', CAST(? AS text)) " "WHERE json_extract(snippet_extras, '$.individualRecipientId') = CAST(? AS text)", {id2, id1}); int changed = d_database.changed(); if (d_verbose && changed) [[unlikely]] Logger::message(" Updated ", changed, " individualrecipientids in thread.snippet_extras"); d_database.exec("UPDATE thread SET snippet_extras = " "json_set(snippet_extras, '$.groupAddedBy', CAST(? AS text)) " "WHERE json_extract(snippet_extras, '$.groupAddedBy') = CAST(? AS text)", {id2, id1}); changed = d_database.changed(); if (d_verbose && changed) [[unlikely]] Logger::message(" Updated ", changed, " groupaddedby-ids in thread.snippet_extras"); } } } signalbackup-tools-20250313-1/signalbackup/updatethreadsentries.cc000066400000000000000000000544161476450434500251410ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "msgrange.h" void SignalBackup::updateThreadsEntries(long long int thread) { Logger::message(__FUNCTION__); SqliteDB::QueryResults results; std::string query = "SELECT DISTINCT _id, " + d_thread_recipient_id + " FROM thread"; // gets all threads if (thread > -1) query += " WHERE _id = " + bepaald::toString(thread); d_database.exec(query, &results); for (unsigned int i = 0; i < results.rows(); ++i) { if (results.valueHasType(i, "_id")) { // set message count std::string threadid = bepaald::toString(results.getValueAs(i, "_id")); if (i == 0) Logger::message_start(" Dealing with thread id: ", threadid); else Logger::message_continue(", ", threadid); long long int thread_recipient = -1; if (results.valueHasType(i, d_thread_recipient_id)) thread_recipient = results.getValueAs(i, d_thread_recipient_id); //std::cout << " Updating msgcount" << std::endl; /* ThreadTable:: private fun isSilentType(type: Long): Boolean { return MessageTypes.isProfileChange(type) || MessageTypes.isGroupV1MigrationEvent(type) || MessageTypes.isChangeNumber(type) || MessageTypes.isBoostRequest(type) || MessageTypes.isGroupV2LeaveOnly(type) || MessageTypes.isThreadMergeType(type) } */ SqliteDB::QueryResults results2; if (d_database.containsTable("sms")) { d_database.exec("UPDATE thread SET " + d_thread_message_count + " = " "(SELECT (SELECT count(*) FROM sms WHERE thread_id = " + threadid + ") + (SELECT count(*) FROM " + d_mms_table + " WHERE thread_id = " + threadid + ")) WHERE _id = " + threadid); d_database.exec("SELECT sms.date_sent AS union_date, sms.type AS union_type, sms.body AS union_body, sms._id AS [sms._id], '' AS [mms._id] FROM 'sms' WHERE sms.thread_id = " + threadid + " UNION SELECT " + d_mms_table + "." + d_mms_date_sent + " AS union_date, " + d_mms_table + "." + d_mms_type + " AS union_type, " + d_mms_table + ".body AS union_body, '' AS [sms._id], " + d_mms_table + "._id AS [mms._id] FROM " + d_mms_table + " WHERE " + d_mms_table + ".thread_id = " + threadid + " AND (union_type & ?) = 0" " AND (union_type & ?) = 0" " AND (union_type & ?) != ?" " AND (union_type & ?) != ?" " AND (union_type & ?) IS NOT ?" " AND (union_type & ?) IS NOT ?" " AND (union_type & ?) IS NOT ?" " AND (union_type & ?) IS NOT ?" " AND (union_type & ?) IS NOT ?" " AND (union_type & ?) IS NOT ?" " ORDER BY union_date DESC LIMIT 1", {Types::KEY_EXCHANGE_IDENTITY_DEFAULT_BIT, Types::KEY_EXCHANGE_IDENTITY_VERIFIED_BIT, Types::SPECIAL_TYPES_MASK, Types::SPECIAL_TYPE_REPORTED_SPAM, Types::SPECIAL_TYPES_MASK, Types::SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED, Types::BASE_TYPE_MASK, Types::PROFILE_CHANGE_TYPE, Types::BASE_TYPE_MASK, Types::GV1_MIGRATION_TYPE, Types::BASE_TYPE_MASK, Types::CHANGE_NUMBER_TYPE, Types::BASE_TYPE_MASK, Types::BOOST_REQUEST_TYPE, Types::GROUP_V2_LEAVE_BITS, Types::GROUP_V2_LEAVE_BITS, Types::BASE_TYPE_MASK, Types::THREAD_MERGE_TYPE}, &results2); } else // dbv >= 168 { d_database.exec("UPDATE thread SET " + d_thread_message_count + " = " "(SELECT count(*) FROM " + d_mms_table + " WHERE thread_id = " + threadid + ") WHERE _id = " + threadid); // at dbv199, an active column was added to thread. When deleted, only a thread contents are actually deleted, // but the thread itself is simply marked inactive (preventing it from showing up in the thread list). // Since this only happens to deleted threads, inactive implies 0 messages (meaningful or otherwise) in the thread, // we set to active if _anything_ is there if (d_database.tableContainsColumn("thread", "active")) d_database.exec("UPDATE thread SET active = " "((SELECT count(*) FROM " + d_mms_table + " WHERE thread_id = " + threadid + ") > 0) WHERE _id = " + threadid + " AND active = 0"); d_database.exec("SELECT " + d_mms_table + "." + d_mms_date_sent + " AS union_date, " + d_mms_table + "." + d_mms_type + " AS union_type, " + d_mms_table + ".body AS union_body, '' AS [sms._id], " + d_mms_table + "._id AS [mms._id] FROM " + d_mms_table + " WHERE " + d_mms_table + ".thread_id = " + threadid + " AND (union_type & ?) = 0" " AND (union_type & ?) = 0" " AND (union_type & ?) != ?" " AND (union_type & ?) != ?" " AND (union_type & ?) IS NOT ?" " AND (union_type & ?) IS NOT ?" " AND (union_type & ?) IS NOT ?" " AND (union_type & ?) IS NOT ?" " AND (union_type & ?) IS NOT ?" " AND (union_type & ?) IS NOT ?" " ORDER BY union_date DESC LIMIT 1", {Types::KEY_EXCHANGE_IDENTITY_DEFAULT_BIT, Types::KEY_EXCHANGE_IDENTITY_VERIFIED_BIT, Types::SPECIAL_TYPES_MASK, Types::SPECIAL_TYPE_REPORTED_SPAM, Types::SPECIAL_TYPES_MASK, Types::SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED, Types::BASE_TYPE_MASK, Types::PROFILE_CHANGE_TYPE, Types::BASE_TYPE_MASK, Types::GV1_MIGRATION_TYPE, Types::BASE_TYPE_MASK, Types::CHANGE_NUMBER_TYPE, Types::BASE_TYPE_MASK, Types::BOOST_REQUEST_TYPE, Types::GROUP_V2_LEAVE_BITS, Types::GROUP_V2_LEAVE_BITS, Types::BASE_TYPE_MASK, Types::THREAD_MERGE_TYPE}, &results2); } if (results2.rows() == 0) continue; std::any mid = results2.value(0, "mms._id"); // not d_mms_table, we used an alias in query std::any date = results2.value(0, "union_date"); if (date.type() == typeid(long long int)) { long long int roundeddate = std::any_cast(date) - (std::any_cast(date) % 1000); //std::cout << " Setting last msg date (" << roundeddate << ")" << std::endl; d_database.exec("UPDATE thread SET date = ? WHERE _id = ?", {roundeddate, threadid}); } std::any body = results2.value(0, "union_body"); std::string newsnippet; if (body.type() == typeid(std::string)) { newsnippet = std::any_cast(body); if (d_database.containsTable("mention")) { SqliteDB::QueryResults snippet_mentions; if (mid.type() == typeid(long long int)) { if (d_database.exec("SELECT * FROM mention WHERE message_id = ?", mid, &snippet_mentions)) { std::vector ranges; for (unsigned int m = 0; m < snippet_mentions.rows(); ++m) { std::string displayname = getNameFromRecipientId(snippet_mentions.getValueAs(m, "recipient_id")); if (displayname.empty()) continue; ranges.emplace_back(Range{snippet_mentions.getValueAs(m, "range_start"), snippet_mentions.getValueAs(m, "range_length"), "", "@" + displayname, "", false}); } applyRanges(&newsnippet, &ranges, nullptr); } } } //std::cout << " Updating snippet (" << newsnippet << ")" << std::endl; d_database.exec("UPDATE thread SET snippet = ? WHERE _id = ?", {newsnippet, threadid}); } else { //std::cout << " Updating snippet (NULL)" << std::endl; d_database.exec("UPDATE thread SET snippet = NULL WHERE _id = ?", threadid); } std::any type = results2.value(0, "union_type"); if (type.type() == typeid(long long int)) { //std::cout << " Updating snippet type (" << std::any_cast(type) << ")" << std::endl; d_database.exec("UPDATE thread SET snippet_type = ? WHERE _id = ?", {std::any_cast(type), threadid}); } if (mid.type() == typeid(long long int)) { //std::cout << "Checking mms" << std::endl; SqliteDB::QueryResults results3; if (d_database.tableContainsColumn(d_part_table, "sticker_pack_id") && d_database.tableContainsColumn(d_part_table, "sticker_emoji")) d_database.exec("SELECT " + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id"s : "-1 AS unique_id"s) + ", _id, " + d_part_ct + ", sticker_pack_id, IFNULL(sticker_emoji, '') AS sticker_emoji " "FROM " + d_part_table + " WHERE " + d_part_mid + " = ?", {mid}, &results3); else d_database.exec("SELECT " + (d_database.tableContainsColumn(d_part_table, "unique_id") ? "unique_id"s : "-1 AS unique_id"s) + ", _id, " + d_part_ct + ", NULL AS sticker_pack_id, NULL AS sticker_emoji " "FROM " + d_part_table + " WHERE " + d_part_mid + " = ?", {mid}, &results3); if (results3.rows()) { std::any uniqueid = results3.value(0, "unique_id"); std::any id = results3.value(0, "_id"); std::any filetype = results3.value(0, d_part_ct); // snippet_uri = content://org.thoughtcrime.securesms/part/ + part.unique_id + '/' + part._id if (id.type() == typeid(long long int) && uniqueid.type() == typeid(long long int)) { //std::cout << " Updating snippet_uri" << std::endl; d_database.exec("UPDATE thread SET snippet_uri = 'content://org.thoughtcrime.securesms/part/" + bepaald::toString(std::any_cast(uniqueid)) + "/" + bepaald::toString(std::any_cast(id)) + "' WHERE _id = " + threadid); } // update body to show photo/movie/file if (!results3.isNull(0, "sticker_pack_id") && !results3("sticker_pack_id").empty()) { std::string snippet = results3("sticker_emoji"); snippet += (snippet.empty() ? "" : " ") + "Sticker"s; d_database.exec("UPDATE thread SET snippet = ? WHERE _id = ?", {snippet, threadid}); } else if (filetype.type() == typeid(std::string)) { std::string t = std::any_cast(filetype); //std::cout << "FILE TYPE: " << t << std::endl; std::string snippet; if (STRING_STARTS_WITH(t, "image/gif")) { snippet = "\xF0\x9F\x8E\xA1 "; // ferris wheel emoji for some reason snippet += (newsnippet.empty()) ? "GIF" : newsnippet; } else if (STRING_STARTS_WITH(t, "image")) { snippet = "\xF0\x9F\x93\xB7 "; // (still) camera emoji snippet += (newsnippet.empty()) ? "Photo" : newsnippet; } else if (STRING_STARTS_WITH(t, "audio")) { snippet = "\xF0\x9F\x8E\xA4 "; // microphone emoji snippet += (newsnippet.empty()) ? "Voice message" : newsnippet; } else if (STRING_STARTS_WITH(t, "video")) { snippet = "\xF0\x9F\x8E\xA5 "; // (movie) camera emoji snippet += (newsnippet.empty()) ? "Video" : newsnippet; } else // if binary file { snippet = "\xF0\x9F\x93\x8E "; // paperclip snippet += (newsnippet.empty()) ? "File" : newsnippet; } //std::cout << " Updating snippet (" << snippet << ")" << std::endl; d_database.exec("UPDATE thread SET snippet = ? WHERE _id = ?", {snippet, threadid}); } } else // was mms, but no part -> maybe contact sharing? { // -> '[{"name":{"displayName":"Basje Timmer",...}}]' SqliteDB::QueryResults results4; d_database.exec("SELECT json_extract(" + d_mms_table + ".shared_contacts, '$[0].name.displayName') AS shared_contact_name from " + d_mms_table + " WHERE _id = ? AND shared_contacts IS NOT NULL", mid, &results4); if (results4.rows() != 0 && results4.valueHasType(0, "shared_contact_name")) { std::string snippet = "\xF0\x9F\x91\xA4 " + results4.getValueAs(0, "shared_contact_name"); // bust in silouette emoji //std::cout << " Updating snippet (" << snippet << ")" << std::endl; d_database.exec("UPDATE thread SET snippet = ? WHERE _id = ?", {snippet, threadid}); } } } else { //std::cout << " Updating snippet (NULL)" << std::endl; d_database.exec("UPDATE thread SET snippet_uri = NULL"); } // if isgroup && database has snippet_extras // set snippet_extras = {"individualRecipientId":"8"}; if (!d_database.containsTable("sms") && d_database.tableContainsColumn("thread", "snippet_extras")) { long long int isgroup = d_database.getSingleResultAs("SELECT group_id IS NOT NULL FROM recipient WHERE _id = ?", thread_recipient, 0); if (isgroup) { long long int sender = -1; if (!d_database.tableContainsColumn(d_mms_table, "to_recipient_id")) // old style -> incoming: sender = message.recipient_id { // outgoing: sender = self SqliteDB::QueryResults snippet_extras; if (d_database.exec("SELECT " + d_mms_type + "," + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE " + d_mms_table + ".thread_id = " + threadid + " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " ORDER BY " + d_mms_date_sent + " DESC LIMIT 1", {Types::BASE_TYPE_MASK, Types::PROFILE_CHANGE_TYPE, Types::BASE_TYPE_MASK, Types::GV1_MIGRATION_TYPE, Types::BASE_TYPE_MASK, Types::CHANGE_NUMBER_TYPE, Types::BASE_TYPE_MASK, Types::BOOST_REQUEST_TYPE, Types::GROUP_V2_LEAVE_BITS, Types::GROUP_V2_LEAVE_BITS, Types::BASE_TYPE_MASK, Types::THREAD_MERGE_TYPE}, &snippet_extras) && snippet_extras.rows() == 1 && snippet_extras.valueHasType(0, d_mms_type) && snippet_extras.valueHasType(0, d_mms_recipient_id)) { long long int mmstype = snippet_extras.getValueAs(0, d_mms_type); if (Types::isOutgoing(mmstype)) { if (d_selfid == -1) d_selfid = scanSelf(); sender = d_selfid; } else sender = snippet_extras.getValueAs(0, d_mms_recipient_id); } } else // new style sender = d_database.getSingleResultAs("SELECT " + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE " + d_mms_table + ".thread_id = " + threadid + " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " ORDER BY " + d_mms_date_sent + " DESC LIMIT 1", {Types::BASE_TYPE_MASK, Types::PROFILE_CHANGE_TYPE, Types::BASE_TYPE_MASK, Types::GV1_MIGRATION_TYPE, Types::BASE_TYPE_MASK, Types::CHANGE_NUMBER_TYPE, Types::BASE_TYPE_MASK, Types::BOOST_REQUEST_TYPE, Types::GROUP_V2_LEAVE_BITS, Types::GROUP_V2_LEAVE_BITS, Types::BASE_TYPE_MASK, Types::THREAD_MERGE_TYPE}, -1); if (sender > -1) // got sender, set snippet_extras { d_database.exec("UPDATE thread SET snippet_extras = json_object('individualRecipientId', '" + bepaald::toString(sender) + "') WHERE _id = ?", threadid); //d_database.prettyPrint("SELECT snippet_extras FROM thread WHERE _id = ?", threadid); } else { // could not set 'individualRecipientId' for some reason, should probably clear it (the currently present id might not exist)? Logger::message_end(); Logger::warning("Not updating thread[", threadid, "].snippet_extras: failed to get sender (", sender, ")"); Logger::warning_indent("Query: ", "SELECT " + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE " + d_mms_table + ".thread_id = " + threadid + " AND (" + d_mms_type + " & ", Types::BASE_TYPE_MASK, ") IS NOT ", Types::PROFILE_CHANGE_TYPE, " AND (" + d_mms_type + " & ", Types::BASE_TYPE_MASK, ") IS NOT ", Types::GV1_MIGRATION_TYPE, " AND (" + d_mms_type + " & ", Types::BASE_TYPE_MASK, ") IS NOT ", Types::CHANGE_NUMBER_TYPE, " AND (" + d_mms_type + " & ", Types::BASE_TYPE_MASK, ") IS NOT ", Types::BOOST_REQUEST_TYPE, " AND (" + d_mms_type + " & ", Types::GROUP_V2_LEAVE_BITS, ") IS NOT ", Types::GROUP_V2_LEAVE_BITS, " AND (" + d_mms_type + " & ", Types::BASE_TYPE_MASK, ") IS NOT ", Types::THREAD_MERGE_TYPE, " ORDER BY " + d_mms_date_sent + " DESC LIMIT 1"); d_database.prettyPrint(d_truncate, "SELECT " + d_mms_recipient_id + " FROM " + d_mms_table + " WHERE " + d_mms_table + ".thread_id = " + threadid + " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " AND (" + d_mms_type + " & ?) IS NOT ?" " ORDER BY " + d_mms_date_sent + " DESC LIMIT 1", {Types::BASE_TYPE_MASK, Types::PROFILE_CHANGE_TYPE, Types::BASE_TYPE_MASK, Types::GV1_MIGRATION_TYPE, Types::BASE_TYPE_MASK, Types::CHANGE_NUMBER_TYPE, Types::BASE_TYPE_MASK, Types::BOOST_REQUEST_TYPE, Types::GROUP_V2_LEAVE_BITS, Types::GROUP_V2_LEAVE_BITS, Types::BASE_TYPE_MASK, Types::THREAD_MERGE_TYPE}); } } } } else Logger::warning("Unexpected type in database"); } Logger::message_end(); } /* meaningful messages NOT $TYPE & ${MessageTypes.IGNORABLE_TYPESMASK_WHEN_COUNTING} AND $TYPE != ${MessageTypes.PROFILE_CHANGE_TYPE} AND $TYPE != ${MessageTypes.CHANGE_NUMBER_TYPE} AND $TYPE != ${MessageTypes.SMS_EXPORT_TYPE} AND $TYPE != ${MessageTypes.BOOST_REQUEST_TYPE} AND $TYPE & ${MessageTypes.GROUP_V2_LEAVE_BITS} != ${MessageTypes.GROUP_V2_LEAVE_BITS} long IGNORABLE_TYPESMASK_WHEN_COUNTING = END_SESSION_BIT | KEY_EXCHANGE_IDENTITY_UPDATE_BIT | KEY_EXCHANGE_IDENTITY_VERIFIED_BIT; */ signalbackup-tools-20250313-1/signalbackup/utf16tounicodecodepoint.cc000066400000000000000000000033741476450434500254730ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #if __cpp_lib_unreachable >= 202202L #include #endif // return: // 0 = ok, finished // -1 = error, stop! // 1 = need more data to complete codepoint int SignalBackup::utf16ToUnicodeCodepoint(uint16_t utf16, uint32_t *codepoint) const { /* U = some unicode codepoint > 0x100000 U' = yyyyyyyyyyxxxxxxxxxx // U - 0x10000 W1 = 110110yyyyyyyyyy // 0xD800 + yyyyyyyyyy W2 = 110111xxxxxxxxxx // 0xDC00 + xxxxxxxxxx */ // low surrogate if (utf16 >= 0xDC00 && utf16 < 0xE000) { *codepoint += (utf16 & 0b0000'0011'1111'1111); *codepoint += 0x10000; return 0; } if (*codepoint != 0) [[unlikely]] return -1; // check if it is a high surrogate if (utf16 >= 0xD800 && utf16 < 0xE000) { *codepoint = ((utf16 & 0b0000'0011'1111'1111) << 10); return 1; } if (utf16 < 0xD800 || utf16 >= 0xE000) // single char codepoint { *codepoint = utf16; return 0; } #if __cpp_lib_unreachable >= 202202L std::unreachable(); #endif return -1; } signalbackup-tools-20250313-1/signalbackup/utf8bytestohexstring.cc000066400000000000000000000030311476450434500251310ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" #include "../common_bytes.h" std::string SignalBackup::utf8BytesToHexString(unsigned char const *const data, size_t data_size) const { // NOTE THIS IS NOT GENERIC UTF-8 CONVERSION, THIS // DATA IS GUARANTEED TO HAVE ONLY SINGLE- AND TWO-BYTE // CHARS (NO 3 OR 4-BYTE). THE TWO-BYTE CHARS NEVER // CONTAIN MORE THAN TWO BITS OF DATA unsigned char output[16]{0}; unsigned int outputpos = 0; for (unsigned int i = 0; i < data_size; ++i) { if (outputpos >= 16) [[unlikely]] return std::string(); if ((data[i] & 0b10000000) == 0) // single byte char output[outputpos++] += data[i]; else // 2 byte char output[outputpos++] = ((data[i] & 0b00000011) << 6) | (data[i + 1] & 0b00111111), ++i; } return bepaald::bytesToHexString(output, 16, true); } signalbackup-tools-20250313-1/signalbackup/writeencryptedframe.cc000066400000000000000000000064571476450434500247770ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalbackup.ih" bool SignalBackup::writeEncryptedFrameWithoutAttachment(std::ofstream &outputfile, std::pair, uint64_t> framedata) { // write frame (the non-attachmentdata part) std::pair encryptedframe = d_fe.encryptFrame(framedata); if (!encryptedframe.first) { Logger::error("Failed to encrypt framedata"); return false; } bool writeok = !(outputfile.write(reinterpret_cast(encryptedframe.first), encryptedframe.second)).fail(); delete[] encryptedframe.first; if (!writeok) Logger::error("Failed to write encrypted frame data to file"); return writeok; } bool SignalBackup::writeEncryptedFrame(std::ofstream &outputfile, BackupFrame *frame) { std::pair, uint64_t> framedata(nullptr, 0); { std::pair framedataraw = frame->getData(); framedata.first.reset(framedataraw.first); framedata.second = framedataraw.second; } if (!framedata.first) [[unlikely]] { Logger::error("Failed to get framedata from frame"); return false; } uint32_t attachmentsize = 0; if ((attachmentsize = frame->attachmentSize()) > 0) { FrameWithAttachment *f = reinterpret_cast(frame); bool badmac = false; unsigned char *attachmentdata = f->attachmentData(&badmac); if (!attachmentdata) { if (badmac) { Logger::warning("Corrupted data encountered. Skipping frame."); return true; } return false; } if (!writeEncryptedFrameWithoutAttachment(outputfile, framedata)) [[unlikely]] { Logger::error("Failed to write encrypt and write BackupFrame. Info:"); frame->printInfo(); return false; } // we are done with framedata now... lets destroy it already... framedata.first.reset(); // write attachment data std::pair newdata = d_fe.encryptAttachment(attachmentdata, attachmentsize); //std::cout << "Writing attachment data..." << std::endl; outputfile.write(reinterpret_cast(newdata.first), newdata.second); delete[] newdata.first; if (!outputfile.good()) [[unlikely]] { Logger::error("Failed to write encrypted attachmentdata to file"); return false; } } else // not an attachmentframe, write it if (!writeEncryptedFrameWithoutAttachment(outputfile, framedata)) [[unlikely]] { Logger::error("Failed to write encrypt and write BackupFrame. Info:"); frame->printInfo(); return false; } return true; } signalbackup-tools-20250313-1/signalplaintextbackupattachmentreader/000077500000000000000000000000001476450434500255565ustar00rootroot00000000000000signalplaintextbackupattachmentreader.h000066400000000000000000000143251476450434500355050ustar00rootroot00000000000000signalbackup-tools-20250313-1/signalplaintextbackupattachmentreader/* Copyright (C) 2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef SIGNALPLAINTEXTBACKUPATTACHMENTREADER_H_ #define SIGNALPLAINTEXTBACKUPATTACHMENTREADER_H_ #include "../baseattachmentreader/baseattachmentreader.h" #include "../framewithattachment/framewithattachment.h" #include "../base64/base64.h" #include class SignalPlainTextBackupAttachmentReader : public AttachmentReader { std::string const d_base64data; std::string d_filename; long long int d_pos; long long int d_size; // size of the base64 string data long long int d_truesize; public: inline explicit SignalPlainTextBackupAttachmentReader(std::string const &b64data, std::string const &filename = std::string(), long long int pos = -1, long long int size = -1); SignalPlainTextBackupAttachmentReader(SignalPlainTextBackupAttachmentReader const &other) = default; SignalPlainTextBackupAttachmentReader(SignalPlainTextBackupAttachmentReader &&other) = default; SignalPlainTextBackupAttachmentReader &operator=(SignalPlainTextBackupAttachmentReader const &other) = default; SignalPlainTextBackupAttachmentReader &operator=(SignalPlainTextBackupAttachmentReader &&other) = default; virtual ~SignalPlainTextBackupAttachmentReader() override = default; inline virtual int getAttachment(FrameWithAttachment *frame, bool verbose) override; inline int getAttachmentData(unsigned char **data, bool verbose); inline long long int dataSize(); //inline virtual void clearData() override; }; SignalPlainTextBackupAttachmentReader::SignalPlainTextBackupAttachmentReader(std::string const &b64data, std::string const &filename, long long int pos, long long int size) : d_base64data(b64data), d_filename(filename), d_pos(pos), d_size(size), d_truesize(-1) {} inline int SignalPlainTextBackupAttachmentReader::getAttachment(FrameWithAttachment *frame, bool verbose) // virtual { unsigned char *data = nullptr; int ret = getAttachmentData(&data, verbose); if (ret == 0) frame->setAttachmentDataBacked(data, d_truesize); // NOTE: test this when d_filename.empty() return ret; } inline int SignalPlainTextBackupAttachmentReader::getAttachmentData(unsigned char **data, bool verbose) { // read the data if needed std::string local_b64_data; if (d_size > 0 && d_base64data.empty() && !d_filename.empty()) { std::ifstream file(std::filesystem::path(d_filename), std::ios_base::binary | std::ios_base::in); if (!file.is_open()) { Logger::error("Failed to open file '", d_filename, "' for reading attachment"); return 1; } if (!file.seekg(d_pos)) { Logger::error("Failed to seek to correct offset in file '", d_filename, " (", d_pos, ")"); return 1; } if (verbose) [[unlikely]] Logger::message("Reading attachment data, length: ", d_size); local_b64_data.reserve(d_size + 1); std::istreambuf_iterator file_it(file); std::copy_n(file_it, d_size, std::back_inserter(local_b64_data)); if (file.tellg() != (d_pos + d_size - 1)) { Logger::error("Failed to read base64-encoded attachment from \"", d_filename, "\""); return 1; } } if (d_size > 0 && d_base64data.empty() && local_b64_data.empty()) // filename.empty(), but so is data, while size is > 0 { Logger::error("SignalPlainTextBackupAttachmentReader has no base64 encoded data"); return 1; } unsigned char *attdata; std::tie(attdata, d_truesize) = Base64::base64StringToBytes(d_base64data.empty() ? local_b64_data : d_base64data); if (!attdata) { d_truesize = -1; // truesize was set 0 by failed base64decode, reset to -1 to mark 'unset' Logger::error("Failed to decode base64-encoded attachment.");// from \"", d_filename, "\""); Logger::error_indent("Base64 data: ", d_base64data.substr(0, 10), (d_base64data.size() > 10 ? "..." : "")); Logger::error_indent("Filename: '", d_filename, "'"); Logger::error_indent("Offset: ", d_pos); Logger::error_indent("Size: ", d_size); return 1; } *data = attdata; return 0; } inline long long int SignalPlainTextBackupAttachmentReader::dataSize() { if (d_truesize == -1) { if (d_base64data.empty() && !d_filename.empty()) { std::ifstream file(std::filesystem::path(d_filename), std::ios_base::binary | std::ios_base::in); if (!file.is_open()) { Logger::error("Failed to open file '", d_filename, "' for reading attachment"); return 1; } if (!file.seekg(d_pos + d_size - 2)) // we want the last two characters to check if they are padding { Logger::error("Failed to seek to correct offset in file '", d_filename, " (", d_pos, ")"); return 1; } int numpadding = 0; char ch = file.get(); if (ch == '=') ++numpadding; ch = file.get(); if (ch == '=') ++numpadding; d_truesize = ((d_size / 4) * 3) - numpadding; } else { int numpadding = 0; if (d_base64data.size() > 0 && d_base64data[d_base64data.size() - 1] == '=') { ++numpadding; if (d_base64data.size() > 1 && d_base64data[d_base64data.size() - 2] == '=') ++numpadding; } d_truesize = ((d_base64data.size() / 4) * 3) - numpadding; } } return d_truesize; } // inline void SignalPlainTextBackupAttachmentReader::clearData() // { // if (!d_filename.empty() && d_pos != -1) // std::string().swap(d_base64data); // this is pretty ugly... // } #endif signalbackup-tools-20250313-1/signalplaintextbackupdatabase/000077500000000000000000000000001476450434500240075ustar00rootroot00000000000000signalbackup-tools-20250313-1/signalplaintextbackupdatabase/signalplaintextbackupdatabase.cc000066400000000000000000000577051476450434500324150ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalplaintextbackupdatabase.ih" #include "../xmldocument/xmldocument.h" #if __cpp_lib_span >= 202002L SignalPlaintextBackupDatabase::SignalPlaintextBackupDatabase(std::span const &sptbxmls, bool truncate, bool verbose, std::vector> namemap, std::string const &namemap_filename, std::vector> const &addressmap, std::string const &addressmap_filename, std::string const &countrycode, bool autogroupnames) #else SignalPlaintextBackupDatabase::SignalPlaintextBackupDatabase(std::vector const &sptbxmls, bool truncate, bool verbose, std::vector> namemap, std::string const &namemap_filename, std::vector> const &addressmap, std::string const &addressmap_filename, std::string const &countrycode, bool autogroupnames) #endif : d_ok(false), d_truncate(truncate), d_verbose(verbose), d_countrycode(countrycode), d_addressmap(addressmap) { // read map from file auto readMapFromFile = [](std::string const &mapfilename, std::vector> *map) { if (mapfilename.empty()) return; // do NOT open in binary mode. We are using getline, on Windows, // using binary mode will append '\r' at the end of each line. std::ifstream mapfile(mapfilename, std::ios_base::in); if (mapfile.is_open()) [[unlikely]] { std::string line; while (std::getline(mapfile, line)) { if (line.empty() || line[0] == '#') continue; std::string::size_type pos; if ((pos = line.find('=')) == std::string::npos) { Logger::warning("Failed to find delimiter in line ('", line, "')"); continue; } // std::cout << line.substr(0, pos) << std::endl; // std::cout << line.substr(pos + 1) << std::endl; map->emplace_back(line.substr(0, pos), line.substr(pos + 1)); } } else Logger::warning("Failed to open file '", mapfilename, "'"); }; // read and append namemap from file readMapFromFile(namemap_filename, &namemap); // read and append addressmap from file readMapFromFile(addressmap_filename, &d_addressmap); /* if (!namemap_filename.empty()) { // do NOT open in binary mode. We are using getline, on Windows, // using binary mode will append '\r' at the end of each line. std::ifstream namemapfile(namemap_filename, std::ios_base::in); if (namemapfile.is_open()) [[unlikely]] { std::string line; while (std::getline(namemapfile, line)) { if (line.empty() || line[0] == '#') continue; std::string::size_type pos; if ((pos = line.find('=')) == std::string::npos) { Logger::warning("Failed to find delimiter in line ('", line, "')"); continue; } // std::cout << line.substr(0, pos) << std::endl; // std::cout << line.substr(pos + 1) << std::endl; namemap.emplace_back(line.substr(0, pos), line.substr(pos + 1)); } } else Logger::warning("Failed to open file '", namemap_filename, "'"); } */ /* columns (XML attributes) imported into SQL table available columns: date = ms since epoch (same as Signal database) address = recipient.e164 read = 1/0 (read/unread) status = -1/0/32/64 (none/complete/pending/failed) type = 1 = Received, 2 = Sent, 3 = Draft, 4 = Outbox, 5 = Failed, 6 = Queued */ struct PlaintextColumnInfo { std::string name; std::string type; std::string required_in_node; // empty = ALL std::string columnname; }; std::vector const columninfo{{"date", "INTEGER", "", ""}, {"type", "INTEGER", "sms", ""}, {"msg_box", "INTEGER", "mms", "type"}, {"read", "INTEGER", "", ""}, {"body", "TEXT", "sms", ""}, {"contact_name", "TEXT", "", ""}, {"address", "TEXT", "", ""}, {"ismms", "INTEGER", "none", ""}, {"sourceaddress", "TEXT", "none", ""}, {"targetaddresses", "TEXT", "none", ""}, // the target addresses of group message (json array) {"numaddresses", "INTEGER", "none", ""}, {"numattachments", "INTEGER", "none", ""}, {"skip", "INTEGER", "none", ""}}; // skip entries are not real messages, they are just there to set a // contacts name who otherwise does not appear in the database (never // sent a message, but could be member of group) // create message table std::string tablecreate; #if __cplusplus > 201703L for (unsigned int i = 0; auto const &rc : columninfo) #else unsigned int i = 0; for (auto const &rc : columninfo) #endif { if (rc.columnname.empty()) [[likely]] tablecreate += (tablecreate.empty() ? "CREATE TABLE smses (" : ", ") + rc.name + " " + rc.type + " DEFAULT NULL"; ++i; } tablecreate += ")"; if (!d_database.exec(tablecreate)) return; // create attachment table if (!d_database.exec("CREATE TABLE attachments (mid INTEGER, data TEXT DEFAULT NULL, filename TEXT DEFAULT NULL, " "pos INTEGER DEFAULT -1, size INTEGER DEFAULT -1, ct TEXT default NULL, cl TEXT default NULL)")) return; // fill tables std::vector values; std::string columns; std::string placeholders; std::set group_only_contacts; // the set of contacts that only appear in groups, and only receives messages std::set group_recipients; // the same but for each message... for (auto const &xmlfile : sptbxmls) { Logger::message("Parsing file: ", xmlfile); // open and parse XML file XmlDocument xmldoc(xmlfile); if (!xmldoc.ok()) { Logger::error("Reading xml data"); return; } // check expected rootnode XmlDocument::Node const &rootnode = xmldoc.root(); if (rootnode.name() != "smses") { Logger::error("Unexpected rootnode '", rootnode.name(), "', expected 'smses'."); return; } auto addvalue = [&](std::string const &column, std::any &&value) { values.emplace_back(value); columns += (columns.empty() ? ""s : ", "s) + column; placeholders += (placeholders.empty() ? ""s : ", "s) + "?"s; }; for (auto const &n : rootnode) { // check required attributes exist if (!std::all_of(columninfo.begin(), columninfo.end(), [&](PlaintextColumnInfo const &rc) { if ((rc.required_in_node.empty() || rc.required_in_node == n.name()) && !n.hasAttribute(rc.name)) { Logger::warning("Skipping message, missing required attribute '", rc.name, "'"); //if (d_verbose) [[unlikely]] { Logger::warning_indent("Full node data:"); n.print(); } return false; } return true; })) continue; if (n.name() == "sms" || n.name() == "mms") { // build statement columns.clear(); placeholders.clear(); values.clear(); group_recipients.clear(); for (auto const &rc : columninfo) { if (rc.required_in_node != n.name() && !rc.required_in_node.empty()) continue; std::string val = n.getAttribute(rc.name); if (val != "null") { if (rc.name == "address") addvalue(rc.name, normalizePhoneNumber(val)); else addvalue((rc.columnname.empty() ? rc.name : rc.columnname), (rc.type == "INTEGER") ? std::any(bepaald::toNumber(val)) : std::any(val)); } } std::vector> attachments; if (n.name() == "mms") { // get message body && attachments std::string body; bool hasbody = false; std::string sourceaddress; for (auto const &childnode : n) { //std::cout << childnode.name() << std::endl; if (childnode.name() == "parts") { for (auto const &part : childnode) { if (part.hasAttribute("text")) { if (part.hasAttribute("ct") && part.getAttribute("ct") == "text/plain") { body += part.getAttribute("text"); hasbody = true; } } if (part.hasAttribute("data")) { //std::cout << "HAS DATA" << std::endl; // do something with data... XmlDocument::Node::StringOrRef attachmentdata = part.getAttributeStringOrRef("data"); if (attachmentdata.size > 0 && attachmentdata.file.empty() && attachmentdata.value.empty()) [[unlikely]] { Logger::warning("Got data attribute, but no value or reference"); n.print(); continue; } std::string ct; if (part.hasAttribute("ct")) ct = part.getAttribute("ct"); std::string cl; if (part.hasAttribute("cl")) cl = part.getAttribute("cl"); attachments.emplace_back(std::make_tuple(attachmentdata, ct, cl)); } } } else if (childnode.name() == "addrs") { int numaddresses = 0; for (auto const &addr : childnode) { ++numaddresses; //addr.print(); if (!addr.hasAttribute("address")) { Logger::warning("No address attribute found in "); continue; } std::string groupmsgaddress = normalizePhoneNumber(addr.getAttribute("address")); // type - The type of address, 129 = BCC, 130 = CC, 151 = To, 137 = From if (addr.hasAttribute("type") && addr.getAttribute("type") == "137") { if (!sourceaddress.empty()) [[unlikely]] { Logger::warning("Multiple source addresses for message"); sourceaddress.clear(); continue; } sourceaddress = groupmsgaddress; group_only_contacts.insert(std::move(groupmsgaddress)); } else // likely a receiving addr { group_recipients.insert(groupmsgaddress); group_only_contacts.insert(std::move(groupmsgaddress)); } } addvalue("numaddresses", numaddresses); } } if (hasbody) addvalue("body", std::move(body)); if (!sourceaddress.empty()) addvalue("sourceaddress", std::move(sourceaddress)); if (!group_recipients.empty()) { // might need to not do this manually... std::string json_array_recipients("["); for (auto it = group_recipients.begin(); it != group_recipients.end(); ++it) json_array_recipients += (it != group_recipients.begin() ? (", \"" + *it + "\"") : ("\"" + *it + "\"")); json_array_recipients += ']'; addvalue("targetaddresses", json_array_recipients); } } //std::cout << "attachments: " << attachments.size() << std::endl; addvalue("numattachments", attachments.size()); // is sms addvalue("ismms", (n.name() == "mms") ? 1 : 0); // dont skip, this is a real message addvalue("skip", 0); if (!d_database.exec("INSERT INTO smses (" + columns + ") VALUES (" + placeholders + ")", values)) return; if (!attachments.empty()) { long long int lastid = d_database.lastId(); for (auto const &a : attachments) d_database.exec("INSERT INTO attachments (mid, data, filename, pos, size, ct, cl) " "VALUES " "(?, ?, ?, ?, ?, ?, ?)", {lastid, std::get<0>(a).value, std::get<0>(a).file, std::get<0>(a).pos, std::get<0>(a).size, std::get<1>(a), std::get<2>(a)}); } } else [[unlikely]] Logger::warnOnce("Skipping unsupported element: '" + n.name() + "'"); } } // add group-only-contacts, as 'skip' messages, so they can be mapped... Logger::message("Marking group-only contacts"); for (auto const &a : group_only_contacts) if (!d_database.exec("INSERT INTO smses (address, skip, contact_name) SELECT ?1, 1, ?1 " "WHERE NOT EXISTS (SELECT 1 FROM smses WHERE address = ?1 AND skip = 0)", a)) { Logger::warning("Failed to add group-only-contact ", a); continue; } //d_database.prettyPrint(false, "SELECT address FROM smses WHERE skip = 1"); //d_database.prettyPrint(false, "SELECT address FROM smses WHERE skip = 1 AND address IN (SELECT DISTINCT address FROM smses WHERE skip = 0)"); // If contact_name IS NULL, "", or "(Unknown)", set it to MAX(contact_name) for that address, // If still empty (all messsages from that contact were NULL, "", OR "(Unknown)", set it // to address //d_database.prettyPrint(true, "SELECT DISTINCT address, contact_name FROM smses ORDER BY address ASC"); Logger::message("Setting contact names where empty"); SqliteDB::QueryResults addresses; if (!d_database.exec("SELECT DISTINCT address FROM smses", &addresses)) return; for (unsigned int i = 0; i < addresses.rows(); ++i) { std::string cn = d_database.getSingleResultAs("SELECT MAX(contact_name) FROM smses " "WHERE contact_name IS NOT '(Unknown)' AND contact_name IS NOT NULL AND contact_name IS NOT '' " "AND address = ?", addresses.value(i, 0), std::string()); //std::cout << addresses(i, "address") << " '" << cn << "'" << std::endl; if (!d_database.exec("UPDATE smses SET contact_name = ? WHERE address = ?", {cn.empty() ? addresses.value(i, 0) : cn, addresses.value(i, 0)})) return; } //d_database.prettyPrint(true, "SELECT DISTINCT address, contact_name FROM smses ORDER BY address ASC"); Logger::message("Apply name-mapping"); // apply name-mapping.... for (auto const &[addr, cn] : namemap) { //std::cout << "Map " << addr << " -> " << cn << std::endl; if (!d_database.exec("UPDATE smses SET contact_name = ? WHERE address = ?", {cn, addr})) { Logger::warning("Failed to set contact name of ", addr, " to \"", cn, "\""); continue; } if (d_database.changed() == 0) // no messages from this contact... add special entry { if (!d_database.exec("INSERT INTO smses (address, contact_name, skip) VALUES (?, ?, ?)", {addr, cn, 1})) { Logger::warning("Failed to set contact name of ", addr, " to \"", cn, "\""); continue; } } } if (autogroupnames) { Logger::message("Auto-generating groupnames"); SqliteDB::QueryResults allgroups; if (d_database.exec("SELECT DISTINCT address FROM smses WHERE address LIKE '%~%' AND SUBSTR(address, 1, 1) != '~' AND SUBSTR(address, LENGTH(address), 1) != '~'", &allgroups)) { //std::cout << "allgroups size: " << allgroups.rows() << std::endl; for (unsigned int i = 0; i < allgroups.rows(); ++i) { std::string new_contact_name; std::string groupaddress = allgroups(i, "address"); //std::cout << "Doing: " << groupaddress << std::endl; std::string::size_type start = 0; std::string::size_type end; while ((end = groupaddress.find('~', start))) { //std::cout << "Single: " << groupaddress.substr(start, (end == std::string::npos ? end : end - start)) << std::endl; std::string groupname_part = d_database.getSingleResultAs("SELECT DISTINCT contact_name FROM smses WHERE address = ? LIMIT 1", groupaddress.substr(start, (end == std::string::npos ? end : end - start)), std::string()); if (groupname_part.empty()) [[unlikely]] groupname_part = groupaddress.substr(start, (end == std::string::npos ? end : end - start)); new_contact_name += new_contact_name.empty() ? groupname_part : ", " + groupname_part; if (end == std::string::npos) break; start = end + 1; } //std::cout << "Got: '" << new_contact_name << "'" << std::endl; if (!d_database.exec("UPDATE smses SET contact_name = ? WHERE address = ?", {new_contact_name, groupaddress})) Logger::warning("Failed to set contact_name of group ('", groupaddress, "') to '", new_contact_name, "'"); // else // std::cout << "Updated " << d_database.changed() << std::endl; } } } // show if we have other sources than self as the sender of outgoing messages //d_database.prettyPrint(d_truncate, "SELECT DISTINCT sourceaddress FROM smses WHERE ismms = 1 AND type = 2"); /* // for all distinct names, set address for that name to be the same..? //d_database.prettyPrint(false, "SELECT DISTINCT rowid,targetaddresses FROM smses WHERE targetaddresses IS NOT NULL"); SqliteDB::QueryResults all_names_res; if (d_database.exec("SELECT contact_name, address FROM smses GROUP BY contact_name, address LIKE '%~%'", &all_names_res)) // "pick one address for each name" { // all_names_res.prettyPrint(false); SqliteDB::QueryResults old_addresses; for (unsigned int i = 0; i < all_names_res.rows(); ++i) { // get the old addresses, that we are going to change d_database.exec("SELECT DISTINCT address FROM smses WHERE contact_name IS ? AND address IS NOT ? AND " "((address LIKE '%~%' AND ? LIKE '%~%') OR (address NOT LIKE '%~%' AND ? NOT LIKE '%~%'))", {all_names_res.value(i, "contact_name"), all_names_res.value(i, "address"), all_names_res.value(i, "address"), all_names_res.value(i, "address")}, &old_addresses); Logger::message(all_names_res(i, "address"), ":"); old_addresses.prettyPrint(false); // change address, and sourceaddress, and targetaddress for (unsigned int j = 0; j < old_addresses.rows(); ++j) { d_database.exec("UPDATE smses SET address = ? WHERE address = ?", {all_names_res.value(i, "address"), old_addresses.value(j, "address")}); //std::cout << "Addr change: " << d_database.changed() << std::endl; d_database.exec("UPDATE smses SET sourceaddress = ? WHERE sourceaddress = ?", {all_names_res.value(i, "address"), old_addresses.value(j, "address")}); //std::cout << "SrcAddr change: " << d_database.changed() << std::endl; d_database.exec("WITH to_update AS " "(" " SELECT smses.rowid, json_set(smses.targetaddresses, fullkey, ?) AS new_array FROM smses, json_each(targetaddresses) WHERE value = ?" ") " "UPDATE smses SET targetaddresses = " " (" " SELECT new_array FROM to_update WHERE to_update.rowid = smses.rowid" " )" " WHERE smses.rowid IN (SELECT to_update.rowid FROM to_update)", {all_names_res.value(i, "address"), old_addresses.value(j, "address")}); // alternative... looks better, not sure if it is better (will always change all rows?) // d_database.exec("UPDATE smses SET targetaddresses = " // "(" // " SELECT json_group_array(" // " CASE" // " WHEN value = ? THEN ?" // " ELSE value" // " END" // " )" // " FROM json_each(smses.targetaddresses)" // ") " // "WHERE targetaddresses IS NOT NULL", // {old_addresses.value(j, "address"), all_names_res.value(i, "address")}); //std::cout << "TgtAddr change: " << d_database.changed() << std::endl; } } //d_database.prettyPrint(false, "SELECT DISTINCT rowid,targetaddresses FROM smses WHERE targetaddresses IS NOT NULL"); } */ //d_database.prettyPrint(true, "SELECT DISTINCT address, contact_name FROM smses ORDER BY address ASC"); //d_database.prettyPrint(true, "SELECT DISTINCT address, contact_name FROM smses ORDER BY address ASC"); //d_database.prettyPrint(true, "SELECT min(date), max(date) FROM smses"); //d_database.prettyPrint(true, "SELECT DISTINCT contact_name, body FROM smses WHERE address = '+31611496644'"); //d_database.prettyPrint(true, "SELECT DISTINCT contact_name, body FROM smses WHERE address = '+31645756298'"); //d_database.prettyPrint(true, "SELECT DISTINCT ismms, sourceaddress, numaddresses FROM smses"); //d_database.prettyPrint(true, "SELECT DISTINCT sourceaddress, numaddresses FROM smses WHERE type = 2 AND ismms = 1"); // this is how to scan for selfid (sourceaddress of outgoing message with at least one target recipient) //d_database.prettyPrint(true, "SELECT DISTINCT sourceaddress FROM smses WHERE numaddresses > 1 AND type = 2 AND ismms = 1"); //d_database.prettyPrint(true, "SELECT * FROM smses WHERE sourceaddress IS NULL AND type = 2 AND ismms = 1"); //d_database.prettyPrint(true, "SELECT * FROM smses LIMIT 50"); //d_database.prettyPrint(false, "SELECT DISTINCT COUNT(DISTINCT numaddresses) FROM smses WHERE address LIKE '%~%' GROUP BY address"); //d_database.printLineMode("SELECT body,HEX(body) FROM smses WHERE date = 1734628440524"); //d_database.saveToFile("plaintext.sqlite"); d_ok = true; } signalbackup-tools-20250313-1/signalplaintextbackupdatabase/signalplaintextbackupdatabase.h000066400000000000000000000205741476450434500322510ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef SIGNALPLAINTEXTBACKUPDATABASE_H_ #define SIGNALPLAINTEXTBACKUPDATABASE_H_ #include "../memsqlitedb/memsqlitedb.h" #include "../logger/logger.h" #include "../common_be.h" #if __cpp_lib_span >= 202002L #include #endif class SignalPlaintextBackupDatabase { MemSqliteDB d_database; bool d_ok; bool d_truncate; bool d_verbose; //std::set d_warningsgiven; std::string d_countrycode; std::vector> d_addressmap; public: #if __cpp_lib_span >= 202002L SignalPlaintextBackupDatabase(std::span const &sptbxmls, bool truncate, bool verbose, std::vector> namemap, std::string const &namemap_file, std::vector> const &addressmap, std::string const &addressmap_file, std::string const &countrycode, bool autogroupnames); #else SignalPlaintextBackupDatabase(std::vector const &sptbxmls, bool truncate, bool verbose, std::vector> namemap, std::string const &namemap_file, std::vector> const &addressmap, std::string const &addressmap_file, std::string const &countrycode, bool autogroupnames); #endif SignalPlaintextBackupDatabase(SignalPlaintextBackupDatabase const &other) = delete; SignalPlaintextBackupDatabase(SignalPlaintextBackupDatabase &&other) = delete; SignalPlaintextBackupDatabase &operator=(SignalPlaintextBackupDatabase const &other) = delete; SignalPlaintextBackupDatabase &operator=(SignalPlaintextBackupDatabase &&other) = delete; inline bool ok() const; inline bool listContacts() const; friend class SignalBackup; friend class DummyBackup; private: inline std::string normalizePhoneNumber(std::string const &in, bool show = true);// const; std::set norm_shown; }; inline bool SignalPlaintextBackupDatabase::ok() const { return d_ok; } inline bool SignalPlaintextBackupDatabase::listContacts() const { SqliteDB::QueryResults addresses; d_database.exec("WITH adrs AS " "(" " SELECT DISTINCT address FROM smses UNION ALL SELECT DISTINCT sourceaddress AS address FROM smses" ") " "SELECT DISTINCT address FROM adrs WHERE address IS NOT NULL ORDER BY address", &addresses); //addresses.prettyPrint(d_truncate); if (addresses.rows() == 0) [[unlikely]] Logger::message("(no contacts found in XML file)"); else Logger::message(" is_chat ", std::setw(20), std::left, " address", std::setw(0), " : name"); for (unsigned int i = 0; i < addresses.rows(); ++i) { std::string cn = d_database.getSingleResultAs("SELECT MAX(contact_name) FROM smses " "WHERE contact_name IS NOT '(Unknown)' " " AND contact_name IS NOT NULL " " AND contact_name IS NOT '' " " AND address = ?", addresses.value(i, 0), std::string()); long long int is_chat = d_database.getSingleResultAs("SELECT COUNT(*) FROM smses WHERE address = ? AND skip = 0", addresses.value(i, 0), 0); Logger::message((is_chat > 0 ? " (*) " : " "), std::setw(20), std::left, addresses(i, "address"), std::setw(0), " : \"", cn, "\""); } /* std::vector numbers { {"00-1-202-688-5500"}, {"(202)688-5500"}, {"+12026885500"}, {"011-1-202-688-5500"}, {"011381688-5500"}, {"00381688-5500"}, {"+381688-5500"} }; std::cout << std::endl; std::cout << std::endl; std::cout << std::endl; for (auto const &n : numbers) std::cout << std::setw(19) << std::left << n << " : " << std::setw(0) << normalizePhoneNumber(n) << std::endl; std::cout << std::endl; std::cout << std::endl; std::cout << std::endl; */ return true; } inline std::string SignalPlaintextBackupDatabase::normalizePhoneNumber(std::string const &in, bool show)// const { if (show && norm_shown.find(in) == norm_shown.end()) Logger::message("normalizePhoneNumber in: ", in); std::string result; // if in is group (phone1~phone2~phone3~etc), split and recurse... if (std::string::size_type pos = in.find('~'); pos != std::string::npos && pos != 0 && pos != in.size() - 1) { std::string::size_type start = 0; std::string::size_type end; while ((end = in.find('~', start)) != std::string::npos) { result += normalizePhoneNumber(in.substr(start, end - start), false) + '~'; start = end + 1; } // get last bit result += normalizePhoneNumber(in.substr(start), false); } else { result = in; // find in in address-map for (auto const &[from, to] : d_addressmap) { if (from == result) { if (show && norm_shown.find(in) == norm_shown.end()) { norm_shown.insert(in); Logger::message("normalizePhoneNumber out: ", to); } return to; } } #if __cpp_lib_erase_if >= 202002L unsigned int removed = std::erase_if(result, [](char c) { return (c < '0' || c > '9') && c != '+'; }); #else unsigned int removed = 0; result.erase(std::remove_if(result.begin(), result.end(), [&](char c) { if ((c < '0' || c > '9') && c != '+') { ++removed; return true; } return false; }), result.end()); #endif if (removed > result.size()) { if (show && norm_shown.find(in) == norm_shown.end()) { norm_shown.insert(in); Logger::message("normalizePhoneNumber out: ", in); } return in; } if (STRING_STARTS_WITH(result, "00")) result = "+" + result.substr(STRLEN("00")); else if (STRING_STARTS_WITH(result, "011")) result = "+" + result.substr(STRLEN("011")); // Special case to deal with numbers that start with _two_ international call prefixes _ countrycodes: // eg (with countrycode '1'): 01110019999999999 if (result.size() >= 15 && // we'll assume max number size of 15 (sources differ), the plus stands for (at least) 2 digits. !d_countrycode.empty() && d_countrycode[0] == '+' && STRING_STARTS_WITH(result, d_countrycode) && (result.substr(d_countrycode.size(), (d_countrycode.size() - 1) + 2) == ("00" + d_countrycode.substr(1)) || result.substr(d_countrycode.size(), (d_countrycode.size() - 1) + 3) == ("011" + d_countrycode.substr(1)))) [[unlikely]] { Logger::warning("Detected doubled prefix and countrycode in phone number (", in, ")"); result = normalizePhoneNumber(result.substr(d_countrycode.size()), false); } if (result[0] != '+' && !d_countrycode.empty()) result = d_countrycode + (result[0] == '0' ? result.substr(1) : result); } if (result.size() >= 9) { if (show && norm_shown.find(in) == norm_shown.end()) { norm_shown.insert(in); Logger::message("normalizePhoneNumber out: ", result); } return result; } if (show && norm_shown.find(in) == norm_shown.end()) { norm_shown.insert(in); Logger::message("normalizePhoneNumber out: ", in); } return in; } #endif signalbackup-tools-20250313-1/signalplaintextbackupdatabase/signalplaintextbackupdatabase.ih000066400000000000000000000014201476450434500324070ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "signalplaintextbackupdatabase.h" signalbackup-tools-20250313-1/sqlcipherdecryptor/000077500000000000000000000000001476450434500216545ustar00rootroot00000000000000signalbackup-tools-20250313-1/sqlcipherdecryptor/decryptdata.cc000066400000000000000000000203041476450434500244660ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlcipherdecryptor.ih" #include "../common_bytes.h" bool SqlCipherDecryptor::decryptData(std::ifstream *dbfile) { // decrypt data d_decrypteddata = new unsigned char[d_decrypteddatasize]; unsigned int pos = 0; // write header std::memcpy(d_decrypteddata + pos, s_sqlliteheader, s_sqlliteheader_size); pos += s_sqlliteheader_size; /* PAGE page_size ------------------------------------------------------------------------------------------------------------------------------- / real_page_size \ | -----------------------------------------------------------------------------------------------| | / | | | | | | page + (real_page_size - (digest_size + page_padding) - iv_size) | | | v | [salt, 16 bytes, only first page][encrypted bytes][iv, 16 bytes][mac, padded to 16 bytes (for version < 3, 20 bytes, padded to 32] */ unsigned int iv_size = 16; unsigned int page_padding = (((d_digestsize - 1) | 15) + 1) - d_digestsize; // pad to multiple of 16 bytes ??? (maybe 32?) std::unique_ptr page(new unsigned char[d_pagesize]); unsigned int pagenumber = 1; // encryption context std::unique_ptr dctx(EVP_CIPHER_CTX_new(), &::EVP_CIPHER_CTX_free); while (true) { unsigned int real_page_size = pagenumber == 1 ? d_pagesize - d_saltsize : d_pagesize; if (!dbfile->read(reinterpret_cast(page.get()), real_page_size)) { if (dbfile->gcount() == 0 && dbfile->eof()) // all bytes were read break; // we failed to read an entire page, but we did read _some_ data from the file, this should not be possible Logger::error("Unexpectedly failed to read next block", (dbfile->eof() ? " (EOF)" : "")); return false; } // these pointers all point to specific data inside 'page' (which was just read from dbfile unsigned char *page_data_to_hash = page.get(); unsigned int page_data_to_hash_size = real_page_size - (d_digestsize + page_padding); unsigned char *iv = page.get() + page_data_to_hash_size - iv_size; unsigned char *page_encrypted_data = page.get(); unsigned int page_encrypted_data_size = page_data_to_hash_size - iv_size; // calculate MAC #if OPENSSL_VERSION_NUMBER >= 0x30000000L std::unique_ptr mac(EVP_MAC_fetch(nullptr, "hmac", nullptr), &::EVP_MAC_free); std::unique_ptr hctx(EVP_MAC_CTX_new(mac.get()), &::EVP_MAC_CTX_free); OSSL_PARAM params[] = {OSSL_PARAM_construct_utf8_string("digest", d_digestname, 0), OSSL_PARAM_construct_end()}; if (EVP_MAC_init(hctx.get(), d_hmackey, d_hmackeysize, params) != 1) { Logger::error("Failed to initialize HMAC context"); return false; } std::unique_ptr calculatedmac(new unsigned char[d_digestsize]); if (EVP_MAC_update(hctx.get(), page_data_to_hash, page_data_to_hash_size) != 1 || EVP_MAC_update(hctx.get(), reinterpret_cast(&pagenumber), sizeof(pagenumber)) != 1 || EVP_MAC_final(hctx.get(), calculatedmac.get(), nullptr, d_digestsize) != 1) { Logger::error("Failed to update/finalize hmac"); return false; } #else std::unique_ptr hctx(HMAC_CTX_new(), &::HMAC_CTX_free); if (HMAC_Init_ex(hctx.get(), d_hmackey, d_hmackeysize, d_digest, nullptr) != 1) { Logger::error("Failed to initialize HMAC context"); return false; } std::unique_ptr calculatedmac(new unsigned char[d_digestsize]); if (HMAC_Update(hctx.get(), page_data_to_hash, page_data_to_hash_size) != 1 || HMAC_Update(hctx.get(), reinterpret_cast(&pagenumber), sizeof(pagenumber)) != 1 || HMAC_Final(hctx.get(), calculatedmac.get(), &d_digestsize) != 1) { Logger::error("Failed to update/finalize hmac"); return false; } #endif // compare calculated mac to the mac from file if (std::memcmp(page.get() + (real_page_size - (d_digestsize + page_padding)), calculatedmac.get(), d_digestsize) != 0) [[unlikely]] { // note: a bad mac can occur if the page is empty (all 0x00). An empty page is not an error, and should simply be skipped. bool containsdata = false; for (unsigned int i = 0; i < page_data_to_hash_size; ++i) { if (page_data_to_hash[i] != 0x00) { containsdata = true; break; } } if (!containsdata) // UNTESTED // skip decryption, but set entire page of zeros?? { if (d_verbose) [[unlikely]] Logger::message("Read empty page from SqlCipherDatabase. Inserting empty page in output..."); int decodedframelength = d_pagesize; if (pagenumber == 1) decodedframelength -= d_saltsize; std::memset(d_decrypteddata + pos, 0, decodedframelength); // write all-zero page pos += decodedframelength; ++pagenumber; continue; } else // mac did not match, but page contained data -> ERROR { Logger::error("BAD MAC! (pagenumber: ", pagenumber, " (at ", static_cast(dbfile->tellg()) - (d_digestsize + page_padding), "/", d_decrypteddatasize, "))"); Logger::error_indent("MAC in file: ", bepaald::bytesToHexString(page.get() + (real_page_size - (d_digestsize + page_padding)), d_digestsize)); Logger::error_indent("Calculated : ", bepaald::bytesToHexString(calculatedmac.get(), d_digestsize)); return false; } } //std::cout << ("MAC OK!" << std::endl; int decodedframelength = d_pagesize; if (pagenumber == 1) decodedframelength -= d_saltsize; // init decryptor if (EVP_DecryptInit_ex(dctx.get(), EVP_aes_256_cbc(), nullptr, d_key, iv) != 1) { Logger::error("CTX INIT FAILED"); return false; } // disable padding EVP_CIPHER_CTX_set_padding(dctx.get(), 0); //std::cout << ("INIT OK!" << std::endl; int actualdecodedframelength = 0; if (EVP_DecryptUpdate(dctx.get(), d_decrypteddata + pos, &actualdecodedframelength, page_encrypted_data, page_encrypted_data_size) != 1) { Logger::error("Failed to update decryption context"); ERR_print_errors_fp(stderr); return false; } //std::cout << ("DECRYPT OK!" << std::endl; // reset decryptor if (EVP_CIPHER_CTX_reset(dctx.get()) != 1) { Logger::error("CTX RESET FAILED"); return false; } //std::cout << "RESET OK!" << std::endl; std::memset(d_decrypteddata + pos + page_encrypted_data_size, 0, decodedframelength - page_encrypted_data_size); // append zeros pos += decodedframelength; //std::cout << "Writing " << decodedframelength << " bytes to file" << std::endl; //outputdb.write(reinterpret_cast(decodedframe2.get()), decodedframelength); ++pagenumber; } return true; } signalbackup-tools-20250313-1/sqlcipherdecryptor/destructor.cc000066400000000000000000000021311476450434500243560ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlcipherdecryptor.ih" #include "../common_be.h" SqlCipherDecryptor::~SqlCipherDecryptor() { bepaald::destroyPtr(&d_key, &d_keysize); bepaald::destroyPtr(&d_hmackey, &d_hmackeysize); bepaald::destroyPtr(&d_salt, &d_saltsize); bepaald::destroyPtr(&d_decrypteddata, &d_decrypteddatasize); bepaald::destroyPtr(&d_digestname, &d_digestname_size); } signalbackup-tools-20250313-1/sqlcipherdecryptor/gethmackey.cc000066400000000000000000000026001476450434500243020ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlcipherdecryptor.ih" bool SqlCipherDecryptor::getHmacKey() { // initialize hmac salt to salt unsigned int hmac_saltsize = d_saltsize; std::unique_ptr hmac_salt(new unsigned char[hmac_saltsize]); std::memcpy(hmac_salt.get(), d_salt, d_saltsize); // then switch it up by xoring with mask for (unsigned int i = 0; i < hmac_saltsize; ++i) hmac_salt[i] ^= s_saltmask; d_hmackeysize = 32; d_hmackey = new unsigned char[d_hmackeysize]; return PKCS5_PBKDF2_HMAC(reinterpret_cast(d_key), d_keysize, hmac_salt.get(), hmac_saltsize, 2/*iterations*/, d_digest, d_hmackeysize, d_hmackey) == 1; return true; } signalbackup-tools-20250313-1/sqlcipherdecryptor/sqlcipherdecryptor.cc000066400000000000000000000065321476450434500261170ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlcipherdecryptor.ih" #include "../common_be.h" #include "../common_bytes.h" /* SQLCipher 1,2,3 -> 4 - KDF Algorithm: PBKDF2-HMAC-SHA1 -> PBKDF2-HMAC-SHA512 - KDF Iterations: 4000,4000,64000,256000 (") // unused, no password->key derivation is done, raw key is in config - HMAC: HMAC-SHA1 -> HMAC-SHA512 - Pagesize: 1024 -> 4096 */ SqlCipherDecryptor::SqlCipherDecryptor(std::string const &databasepath, std::string const &hexkey, int version, bool verbose) : d_ok(false), d_databasepath(databasepath), d_key(nullptr), d_keysize(0), d_hmackey(nullptr), d_hmackeysize(0), d_salt(nullptr), d_saltsize(0), d_digest(version >= 4 ? EVP_sha512() : EVP_sha1()), d_digestname_size((version >= 4 ? STRLEN("SHA512") : STRLEN("SHA1")) + 1), d_digestname(version >= 4 ? new char[d_digestname_size] {'S', 'H', 'A', '5', '1', '2', '\0'} : new char[d_digestname_size] {'S', 'H', 'A', '1', '\0'}), d_digestsize(EVP_MD_size(d_digest)), d_pagesize(version >= 4 ? 4096 : 1024), d_decrypteddata(nullptr), d_decrypteddatasize(0), d_verbose(verbose) { if (hexkey.empty()) return; d_keysize = hexkey.size() / 2; d_key = new unsigned char[d_keysize]; if (!bepaald::hexStringToBytes(hexkey, d_key, d_keysize)) { Logger::error("Failed to set key from provided hex string"); return; } // open database file std::ifstream dbfile(d_databasepath, std::ios_base::in | std::ios_base::binary); if (!dbfile.is_open()) { Logger::error("Failed to open database file '", d_databasepath, "'"); return; } // get file size (this will also be the output file size) //dbfile.seekg(0, std::ios_base::end); //d_decrypteddatasize = dbfile.tellg(); //dbfile.seekg(0, std::ios_base::beg); d_decrypteddatasize = bepaald::fileSize(d_databasepath); if (d_verbose) [[unlikely]] Logger::message("Opening Desktop database `", d_databasepath, "' (", d_decrypteddatasize, " bytes)"); // read salt d_saltsize = 16; d_salt = new unsigned char[d_saltsize]; if (!dbfile.read(reinterpret_cast(d_salt), d_saltsize)) { Logger::error("Failed to read salt from database file"); return; } if (!getHmacKey()) return; if (d_verbose) [[unlikely]] Logger::message("Starting decrypt..."); if (!decryptData(&dbfile)) return; if (d_verbose) [[unlikely]] Logger::message("Done!"); // std::cout << "CIPHER KEY: " << bepaald::bytesToHexString(d_key, d_keysize) << std::endl; // std::cout << " HMAC KEY: " << bepaald::bytesToHexString(d_hmackey, d_hmackeysize) << std::endl; d_ok = true; } signalbackup-tools-20250313-1/sqlcipherdecryptor/sqlcipherdecryptor.h000066400000000000000000000057501476450434500257620ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef SQLCIPHERDECRYPTOR_H_ #define SQLCIPHERDECRYPTOR_H_ #include #include #include "../common_filesystem.h" #include "../logger/logger.h" struct evp_md_st; class SqlCipherDecryptor { bool d_ok; std::string d_databasepath; unsigned char *d_key; unsigned int d_keysize; unsigned char *d_hmackey; unsigned int d_hmackeysize; unsigned char *d_salt; unsigned int d_saltsize; evp_md_st const *d_digest; size_t d_digestname_size; char *d_digestname; unsigned int d_digestsize; unsigned int d_pagesize; unsigned char *d_decrypteddata; uint64_t d_decrypteddatasize; bool d_verbose; static unsigned char constexpr s_saltmask = 0x3a; static int constexpr s_sqlliteheader_size = 16; static char constexpr s_sqlliteheader[s_sqlliteheader_size] = {'S', 'Q', 'L', 'i', 't', 'e', ' ', 'f', 'o', 'r', 'm', 'a', 't', ' ', '3', '\0'}; struct DecodedData { unsigned char *d_data; uint64_t d_datasize; }; public: explicit SqlCipherDecryptor(std::string const &databasepath, std::string const &hexkey, int version, bool verbose); SqlCipherDecryptor(SqlCipherDecryptor const &other) = delete; SqlCipherDecryptor &operator=(SqlCipherDecryptor const &other) = delete; ~SqlCipherDecryptor(); inline bool ok() const; inline DecodedData data() const; inline bool writeToFile(std::string const &filename, bool overwrite) const; private: bool getHmacKey(); bool decryptData(std::ifstream *dbfile); }; inline bool SqlCipherDecryptor::ok() const { return d_ok; } inline SqlCipherDecryptor::DecodedData SqlCipherDecryptor::data() const { return {d_decrypteddata, d_decrypteddatasize}; } inline bool SqlCipherDecryptor::writeToFile(std::string const &filename, bool overwrite) const { if (!overwrite && bepaald::fileOrDirExists(filename)) { Logger::error("File ", filename, " exists, use --overwrite to overwrite"); return false; } std::ofstream out(filename, std::ios_base::binary); if (!out.is_open()) { Logger::error("Failed to open ", filename, " for writing"); return false; } if (!out.write(reinterpret_cast(d_decrypteddata), d_decrypteddatasize)) { Logger::error("Error writing data to file"); return false; } return true; } #endif signalbackup-tools-20250313-1/sqlcipherdecryptor/sqlcipherdecryptor.ih000066400000000000000000000016041476450434500261250ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlcipherdecryptor.h" #include #include #include #include #include signalbackup-tools-20250313-1/sqlitedb/000077500000000000000000000000001476450434500175355ustar00rootroot00000000000000signalbackup-tools-20250313-1/sqlitedb/availablewidth.cc000066400000000000000000000024141476450434500230250ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlitedb.ih" int SqliteDB::QueryResults::availableWidth() const { #if defined(_WIN32) || defined(__MINGW64__) // this is untested, I don't have windows CONSOLE_SCREEN_BUFFER_INFO csbi; int ret = GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); if (ret) return (csbi.dwSize.X - 1 < 40) ? 40 : csbi.dwSize.X - 1; return 80; #else // !windows struct winsize ts; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ts) != -1) return (ts.ws_col < 40) ? 40 : ts.ws_col; return 80; // some random default; #endif } signalbackup-tools-20250313-1/sqlitedb/copydb.cc000066400000000000000000000024641476450434500213320ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlitedb.ih" bool SqliteDB::copyDb(SqliteDB const &source, SqliteDB const &target) // static { sqlite3_backup *backup = sqlite3_backup_init(target.d_db, "main", source.d_db, "main"); if (!backup) { Logger::error("SQL: ", sqlite3_errmsg(target.d_db)); return false; } int rc = 0; if ((rc = sqlite3_backup_step(backup, -1)) != SQLITE_DONE) { Logger::error("SQL: ", sqlite3_errstr(rc)); return false; } if (sqlite3_backup_finish(backup) != SQLITE_OK) { Logger::error("SQL: Error finishing backup"); return false; } return true; } signalbackup-tools-20250313-1/sqlitedb/databasewriteversion.cc000066400000000000000000000036121476450434500242730ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlitedb.ih" void SqliteDB::setDatabaseWriteVersion() { if (d_data && d_data->second >= 99) { d_databasewriteversion = (d_data->first[96] << 24) | (d_data->first[97] << 16) | (d_data->first[98] << 8) | (d_data->first[99]); return; } if (!d_name.empty() && d_name != ":memory:") { std::ifstream databasefile(d_name, std::ios_base::in | std::ios_base::binary); if (databasefile.is_open() && databasefile.seekg(96)) { databasefile.read(reinterpret_cast(&d_databasewriteversion), 4); d_databasewriteversion = bepaald::swap_endian(d_databasewriteversion); return; } } } void SqliteDB::checkDatabaseWriteVersion() const { if (d_databasewriteversion > SQLITE_VERSION_NUMBER) { Logger::warning("Database was created with a newer version of SQLite3 than this program is using. If you"); Logger::warning_indent("see a 'malformed database schema' error, please update your SQLite3 version."); Logger::warning_indent("Database was written by version: ", d_databasewriteversion); Logger::warning_indent("This program is using version: ", SQLITE_VERSION_NUMBER); } } signalbackup-tools-20250313-1/sqlitedb/prettyprint.cc000066400000000000000000000404431476450434500224550ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlitedb.ih" void SqliteDB::QueryResults::prettyPrint(bool truncate, long long int requestedrow) const { if (rows() == 0 && columns() == 0) { Logger::message("(no results)"); return; } std::vector> contents; // add headers contents.resize(contents.size() + 1); for (unsigned int i = 0; i < d_headers.size(); ++i) contents.back().emplace_back(d_headers[i]); // set data long long int startrow = requestedrow == -1 ? 0 : requestedrow; long long int endrow = requestedrow == -1 ? rows() : requestedrow + 1; for (unsigned int i = startrow; i < endrow; ++i) { contents.resize(contents.size() + 1); for (unsigned int j = 0; j < columns(); ++j) { if (valueHasType(i, j)) contents.back().emplace_back(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) contents.back().emplace_back("(NULL)"); else if (valueHasType(i, j)) { contents.back().emplace_back(getValueAs(i, j)); std::string::size_type newline; if ((newline = contents.back().back().find('\n')) != std::string::npos) { contents.back().back().resize(newline); contents.back().back() += "[\\n...]"; } } else if (valueHasType, size_t>>(i, j)) contents.back().emplace_back(bepaald::bytesToHexString(getValueAs, size_t>>(i, j).first.get(), getValueAs, size_t>>(i, j).second)); else if (valueHasType(i, j)) contents.back().emplace_back(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) contents.back().emplace_back(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) contents.back().emplace_back(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) contents.back().emplace_back(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) contents.back().emplace_back(bepaald::toString(getValueAs(i, j))); else contents.back().emplace_back("(unhandled type)"); } } // calculate widths std::vector widths(contents[0].size(), 0); for (unsigned int col = 0; col < contents[0].size(); ++col) for (unsigned int row = 0; row < contents.size(); ++row) if (widths[col] < charCount(contents[row][col])) widths[col] = charCount(contents[row][col]); int totalw = std::accumulate(widths.begin(), widths.end(), 0) + 3 * columns() + 1; int availablewidth = truncate ? availableWidth() : std::numeric_limits::max(); //std::cout << " total width: " << totalw << std::endl; //std::cout << " available width: " << availablewidth << std::endl; if (totalw > availablewidth) { unsigned int fairwidthpercol = (availablewidth - 1) / contents[0].size() - 3; //std::cout << "cols: " << contents[0].size() << " size per col (adjusted for table space): " << fairwidthpercol << " LEFT: " << availableWidth - (contents[0].size() * fairwidthpercol + 3 * contents[0].size() + 1) << std::endl; int spaceleftbyshortcols = availablewidth - (contents[0].size() * fairwidthpercol + 3 * contents[0].size() + 1); std::vector oversizedcols; unsigned int widestcol = 0; unsigned int maxwidth = 0; // each column has availableWidth / nCols (- tableedges) available by fairness. // add to this all the space not needed by the columns that are // less wide than availableWidth / nCols anyway. for (unsigned int i = 0; i < widths.size(); ++i) { if (widths[i] > maxwidth) { maxwidth = widths[i]; widestcol = i; } if (widths[i] <= fairwidthpercol) { spaceleftbyshortcols += fairwidthpercol - widths[i]; //std::cout << "Column " << i << " has room to spare: " << " " << widths[i] << " (" << fairwidthpercol - widths[i] << ")" << std::endl; } else { oversizedcols.push_back(i); //std::cout << "Column " << i << " is oversized: " << " " << widths[i] << std::endl; } } unsigned int maxwidthpercol = std::max(static_cast(fairwidthpercol + spaceleftbyshortcols / oversizedcols.size()), 5); int leftforlongcols = spaceleftbyshortcols % oversizedcols.size(); //std::cout << L"Real max width per col: " << maxwidthpercol << L" (left " << leftforlongcols << L")" << std::endl; // maybe some oversized column are not oversized anymore... bool changed = true; while (changed) { changed = false; for (auto it = oversizedcols.begin(); it != oversizedcols.end(); ++it) { if (widths[*it] < maxwidthpercol) { //std::cout << "Column has room, needs " << widths[*it] << " available: " << maxwidthpercol << " adds " << maxwidthpercol - widths[*it] << " to pool of leftspace for " << oversizedcols.size() - 1 << " remaining oversized cols" << std::endl; changed = true; maxwidthpercol += (maxwidthpercol - widths[*it]) / (oversizedcols.size() - 1); oversizedcols.erase(it); break; } //else //{ // std::cout << "Column needs that extra space " << widths[*it] << " available: " << maxwidthpercol << std::endl; //} } //std::cout << "" << std::endl; } //std::cout << "REAL max width per col: " << maxwidthpercol << " (left " << leftforlongcols << ")" << std::endl; // update widths widths.clear(); widths.resize(contents[0].size()); for (unsigned int col = 0; col < contents[0].size(); ++col) for (unsigned int row = 0; row < contents.size(); ++row) { if (charCount(contents[row][col]) > maxwidthpercol + ((col == widestcol) ? leftforlongcols : 0)) { contents[row][col].resize(maxwidthpercol + ((col == widestcol) ? leftforlongcols : 0) - 5); // this might overcrop, because the max is set to charcount contents[row][col] += "[...]"; // while the resize is done at bytecount. We could crop at } // charcount, but some char take two columns of terminal, so if (widths[col] < charCount(contents[row][col])) // we might then undercrop, which is worse. widths[col] = charCount(contents[row][col]); } } //std::cout << std::string(availableWidth(), '*') << std::endl; //bool ansi = useEscapeCodes(); Logger::message(std::string(std::accumulate(widths.begin(), widths.end(), 0) + 2 * columns() + columns() + 1, '-')); for (unsigned int row = 0; row < contents.size(); ++row) { Logger::message_start(); unsigned int pos = 1; // for seeking horizontal position with ANSI escape codes, this starts counting at 1 for (unsigned int col = 0; col < contents[row].size(); ++col) { Logger::message_continue(std::left, "| ", std::setw(widths[col]), std::setfill(' '), contents[row][col], std::setw(0), " "); //if (ansi) // if we support control codes, make 'sure' the cursor is at the right position //{ pos += 2 + widths[col] + 1; // "| " + content + " " Logger::message_continue(Logger::ControlChar("\033[" + bepaald::toString(pos - 1) + "G ")); // prints a space right before where the next '|' will come //} } Logger::message_end("|"); // another bar under top row if (row == 0) Logger::message(std::string(std::accumulate(widths.begin(), widths.end(), 0) + 2 * columns() + columns() + 1, '-')); } Logger::message(std::string(std::accumulate(widths.begin(), widths.end(), 0) + 2 * columns() + columns() + 1, '-')); return; } // void SqliteDB::QueryResults::prettyPrint() const // { // if (rows() == 0 && columns() == 0) // { // std::cout << "(no results)" << std::endl; // return; // } // std::setlocale(LC_ALL, "en_US.utf8"); // std::freopen(nullptr, "a", stdout); // std::vector> contents; // // add headers // contents.resize(contents.size() + 1); // for (unsigned int i = 0; i < d_headers.size(); ++i) // contents.back().emplace_back(wideString(d_headers[i])); // // set data // for (unsigned int i = 0; i < rows(); ++i) // { // contents.resize(contents.size() + 1); // for (unsigned int j = 0; j < columns(); ++j) // { // if (valueHasType(i, j)) // { // /* // std::wstring tmp = wideString(getValueAs(i, j)); // if (tmp.length() != charCount(getValueAs(i, j))) // { // std::wcout << L"SIZES DIFFER:" << std::endl; // std::wcout << tmp << std::endl; // } // */ // contents.back().emplace_back(wideString(getValueAs(i, j))); // std::string::size_type newline = std::string::npos; // if ((newline = contents.back().back().find('\n')) != std::string::npos) // contents.back().back().resize(newline); // } // else if (valueHasType(i, j)) // { // contents.back().emplace_back(bepaald::toWString(getValueAs(i, j))); // } // else if (valueHasType(i, j)) // { // contents.back().emplace_back(bepaald::toWString(getValueAs(i, j))); // } // else if (valueHasType(i, j)) // { // contents.back().emplace_back(L"(NULL)"); // } // else if (valueHasType, size_t>>(i, j)) // { // contents.back().emplace_back(bepaald::bytesToHexWString(getValueAs, size_t>>(i, j).first.get(), // getValueAs, size_t>>(i, j).second)); // } // else // { // contents.back().emplace_back(L"(unhandled type)"); // } // } // } // // calculate widths // std::vector widths(contents[0].size(), 0); // for (unsigned int col = 0; col < contents[0].size(); ++col) // for (unsigned int row = 0; row < contents.size(); ++row) // if (widths[col] < contents[row][col].length()) // widths[col] = contents[row][col].length(); // int totalw = std::accumulate(widths.begin(), widths.end(), 0) + 3 * columns() + 1; // //std::wcout << L" total width: " << totalw << std::endl; // //std::wcout << L" available width: " << availableWidth() << std::endl; // if (totalw > availableWidth()) // { // unsigned int fairwidthpercol = (availableWidth() - 1) / contents[0].size() - 3; // //std::wcout << L"cols: " << contents[0].size() << L" size per col (adjusted for table space): " << fairwidthpercol << L" LEFT: " << availableWidth() - (contents[0].size() * fairwidthpercol + 3 * contents[0].size() + 1) << std::endl; // int spaceleftbyshortcols = availableWidth() - (contents[0].size() * fairwidthpercol + 3 * contents[0].size() + 1); // std::vector oversizedcols; // unsigned int widestcol = 0; // unsigned int maxwidth = 0; // // each column has availableWidth() / nCols (- tableedges) available by fairness. // // add to this all the space not needed by the columns that are // // less wide than availableWidth() / nCols anyway. // for (unsigned int i = 0; i < widths.size(); ++i) // { // if (widths[i] > maxwidth) // { // maxwidth = widths[i]; // widestcol = i; // } // if (widths[i] <= fairwidthpercol) // { // spaceleftbyshortcols += fairwidthpercol - widths[i]; // //std::wcout << L"Column " << i << L" has room to spare: " << L" " << widths[i] << L" (" << fairwidthpercol - widths[i] << L")" << std::endl; // } // else // { // oversizedcols.push_back(i); // //std::wcout << L"Column " << i << L" is oversized: " << L" " << widths[i] << std::endl; // } // } // unsigned int maxwidthpercol = std::max(static_cast(fairwidthpercol + spaceleftbyshortcols / oversizedcols.size()), 5); // int leftforlongcols = spaceleftbyshortcols % oversizedcols.size(); // //std::wcout << L"Real max width per col: " << maxwidthpercol << L" (left " << leftforlongcols << L")" << std::endl; // // maybe some oversized column are not oversized anymore... // bool changed = true; // while (changed) // { // changed = false; // for (auto it = oversizedcols.begin(); it != oversizedcols.end(); ++it) // { // if (widths[*it] < maxwidthpercol) // { // //std::wcout << L"Column has room, needs " << widths[*it] << L" available: " << maxwidthpercol << L" adds " << maxwidthpercol - widths[*it] << L" to pool of leftspace for " << oversizedcols.size() - 1 << L" remaining oversized cols" << std::endl; // changed = true; // maxwidthpercol += (maxwidthpercol - widths[*it]) / (oversizedcols.size() - 1); // oversizedcols.erase(it); // break; // } // //else // //{ // // std::wcout << L"Column needs that extra space " << widths[*it] << L" available: " << maxwidthpercol << std::endl; // //} // } // //std::wcout << L"" << std::endl; // } // //std::wcout << L"REAL max width per col: " << maxwidthpercol << L" (left " << leftforlongcols << L")" << std::endl; // // update widths // widths.clear(); // widths.resize(contents[0].size()); // for (unsigned int col = 0; col < contents[0].size(); ++col) // for (unsigned int row = 0; row < contents.size(); ++row) // { // if (contents[row][col].length() > maxwidthpercol + ((col == widestcol) ? leftforlongcols : 0)) // { // contents[row][col].resize(maxwidthpercol + ((col == widestcol) ? leftforlongcols : 0) - 5); // contents[row][col] += L"[...]"; // } // if (widths[col] < contents[row][col].length()) // { // widths[col] = contents[row][col].length(); // } // } // } // //std::wcout << std::wstring(availableWidth(), L'*') << std::endl; // bool ansi = useEscapeCodes(); // std::wcout << std::wstring(std::accumulate(widths.begin(), widths.end(), 0) + 2 * columns() + columns() + 1, L'-') << std::endl; // for (unsigned int row = 0; row < contents.size(); ++row) // { // std::wcout.setf(std::ios_base::left); // unsigned int pos = 1; // for seeking horizontal position with ANSI escape codes, this starts counting at 1 // for (unsigned int col = 0; col < contents[row].size(); ++col) // { // std::wcout << L"| " << std::setw(widths[col]) << std::setfill(L' ') << contents[row][col] << std::setw(0) << L" "; // if (ansi) // { // pos += 2 + widths[col] + 1; // "| " + content + " " // std::wcout << L"\033[" << pos - 1 << L"G "; // prints a space right before where the next '|' will come // } // } // std::wcout << L"|" << std::endl; // // another bar under top row // if (row == 0) // std::wcout << std::wstring(std::accumulate(widths.begin(), widths.end(), 0) + 2 * columns() + columns() + 1, L'-') << std::endl; // } // std::wcout << std::wstring(std::accumulate(widths.begin(), widths.end(), 0) + 2 * columns() + columns() + 1, L'-') << std::endl; // std::freopen(nullptr, "a", stdout); // return; // } signalbackup-tools-20250313-1/sqlitedb/print.cc000066400000000000000000000056211476450434500212040ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlitedb.ih" void SqliteDB::QueryResults::print(long long int row, bool printheader) const { if (rows() == 0 && columns() == 0) { Logger::message("(no results)"); return; } if (printheader) { Logger::message_start(); for (unsigned int i = 0; i < d_headers.size(); ++i) Logger::message_continue(d_headers[i], ((i < d_headers.size() - 1) ? "|" : "")); Logger::message_end(); } long long int startrow = row == -1 ? 0 : row; long long int endrow = row == -1 ? rows() : row + 1; for (unsigned int i = startrow; i < endrow; ++i) { Logger::message_start(); for (unsigned int j = 0; j < columns(); ++j) { if (valueHasType(i, j)) Logger::message_continue(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message_continue("(NULL)"); else if (valueHasType(i, j)) Logger::message_continue(getValueAs(i, j)); else if (valueHasType, size_t>>(i, j)) Logger::message_continue(bepaald::bytesToHexString(getValueAs, size_t>>(i, j).first.get(), getValueAs, size_t>>(i, j).second)); else if (valueHasType(i, j)) Logger::message_continue(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message_continue(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message_continue(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message_continue(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message_continue(bepaald::toString(getValueAs(i, j))); else Logger::message_continue("(unhandled type)"); if (j < columns() - 1) Logger::message_continue("|"); } Logger::message_end(); } } signalbackup-tools-20250313-1/sqlitedb/printlinemode.cc000066400000000000000000000065211476450434500227210ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlitedb.ih" void SqliteDB::QueryResults::printLineMode(long long int row) const { if (rows() == 0 && columns() == 0) { Logger::message("(no results)"); return; } // get longest header unsigned int maxheader = 0; for (auto const &h : d_headers) if (h.size() > maxheader) maxheader = h.size(); // print long long int startrow = row == -1 ? 0 : row; long long int endrow = row == -1 ? rows() : row + 1; for (unsigned int i = startrow; i < endrow; ++i) { Logger::message(" === Row ", i + 1, "/", rows(), " ==="); for (unsigned int j = 0; j < columns(); ++j) { if (valueHasType(i, j)) Logger::message(std::setfill(' '), std::setw(maxheader), std::right, d_headers[j], " : ", bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message(std::setfill(' '), std::setw(maxheader), std::right, d_headers[j], " : ", "(NULL)"); else if (valueHasType(i, j)) Logger::message(std::setfill(' '), std::setw(maxheader), std::right, d_headers[j], " : ", getValueAs(i, j)); else if (valueHasType, size_t>>(i, j)) Logger::message(std::setfill(' '), std::setw(maxheader), std::right, d_headers[j], " : ", bepaald::bytesToHexString(getValueAs, size_t>>(i, j).first.get(), getValueAs, size_t>>(i, j).second)); else if (valueHasType(i, j)) Logger::message(std::setfill(' '), std::setw(maxheader), std::right, d_headers[j], " : ", bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message(std::setfill(' '), std::setw(maxheader), std::right, d_headers[j], " : ", bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message(std::setfill(' '), std::setw(maxheader), std::right, d_headers[j], " : ", bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message(std::setfill(' '), std::setw(maxheader), std::right, d_headers[j], " : ", bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message(std::setfill(' '), std::setw(maxheader), std::right, d_headers[j], " : ", bepaald::toString(getValueAs(i, j))); else Logger::message("(unhandled type)"); } } } signalbackup-tools-20250313-1/sqlitedb/printsingleline.cc000066400000000000000000000052201476450434500232510ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlitedb.ih" void SqliteDB::QueryResults::printSingleLine(long long int row) const { if (rows() == 0 && columns() == 0) { Logger::message("(no results)"); return; } //Logger::message long long int startrow = row == -1 ? 0 : row; long long int endrow = row == -1 ? rows() : row + 1; for (unsigned int i = startrow; i < endrow; ++i) { for (unsigned int j = 0; j < columns(); ++j) { if (j > 0 || i > 0) Logger::message_continue(','); if (valueHasType(i, j)) Logger::message_continue(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message_continue("(NULL)"); else if (valueHasType(i, j)) Logger::message_continue(getValueAs(i, j)); else if (valueHasType, size_t>>(i, j)) Logger::message_continue(bepaald::bytesToHexString(getValueAs, size_t>>(i, j).first.get(), getValueAs, size_t>>(i, j).second)); else if (valueHasType(i, j)) Logger::message_continue(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message_continue(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message_continue(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message_continue(bepaald::toString(getValueAs(i, j))); else if (valueHasType(i, j)) Logger::message_continue(bepaald::toString(getValueAs(i, j))); else Logger::message_continue("(unhandled type)"); } } Logger::message_end(); } signalbackup-tools-20250313-1/sqlitedb/removecolumn.cc000066400000000000000000000020671476450434500225640ustar00rootroot00000000000000/* Copyright (C) 2020-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlitedb.ih" bool SqliteDB::QueryResults::removeColumn(unsigned int idx) { if (idx >= d_headers.size()) return false; for (auto const &v : d_values) if (idx >= v.size()) return false; d_headers.erase(d_headers.begin() + idx); for (auto &v : d_values) v.erase(v.begin() + idx); return true; } signalbackup-tools-20250313-1/sqlitedb/renamecolumn.cc000066400000000000000000000016611476450434500225350ustar00rootroot00000000000000/* Copyright (C) 2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlitedb.ih" bool SqliteDB::QueryResults::renameColumn(unsigned int idx, std::string const &name) { if (idx >= d_headers.size()) return false; d_headers[idx] = name; return true; } signalbackup-tools-20250313-1/sqlitedb/sqlitedb.h000066400000000000000000001171511476450434500215230ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef SQLITEDB_H_ #define SQLITEDB_H_ #include #include #include #include #include #include #include #if __cpp_lib_ranges >= 201911L #include #endif #include "../common_be.h" #include "../memfiledb/memfiledb.h" #include "../logger/logger.h" class SqliteDB { public: class QueryResults { std::vector d_headers; std::vector> d_values; public: inline void emplaceHeader(std::string &&h); inline std::vector const &headers() const; inline std::string const &header(size_t idx) const; inline bool hasColumn(std::string const &h) const; inline void emplaceValue(size_t row, std::any &&a); inline std::any value(size_t row, std::string const &header) const; template inline T getValueAs(size_t row, std::string const &header) const; inline std::any const &value(size_t row, size_t idx) const; inline std::vector const &row(size_t row) const; template inline bool valueHasType(size_t row, size_t idx) const; template inline bool valueHasType(size_t row, std::string const &header) const; inline bool isNull(size_t row, size_t idx) const; inline bool isNull(size_t row, std::string const &header) const; template inline T getValueAs(size_t row, size_t idx) const; inline bool empty() const; inline size_t rows() const; inline size_t columns() const; inline void clear(); void printSingleLine(long long int row = -1) const; void printLineMode(long long int row = -1) const; void prettyPrint(bool truncate, long long int row = -1) const; void print(long long int row = -1, bool printheader = true) const; std::string valueAsString(size_t row, size_t column) const; std::string valueAsString(size_t row, std::string const &header) const; long long int valueAsInt(size_t row, size_t column, long long int def = -1) const; long long int valueAsInt(size_t row, std::string const &header, long long int def = -1) const; inline std::string operator()(size_t row, std::string const &header) const; inline std::string operator()(std::string const &header) const; template inline bool contains(T const &value) const; bool removeColumn(unsigned int idx); bool renameColumn(unsigned int idx, std::string const &name); inline bool removeRow(unsigned int idx); inline QueryResults getRow(unsigned int idx); private: inline int idxOfHeader(std::string const &header) const; int availableWidth() const; inline uint64_t charCount(std::string const &utf8) const; }; public: struct StaticTextParam { char *ptr; uint64_t size; StaticTextParam(char *p, uint64_t s) : ptr(p), size(s) {} }; private: sqlite3 *d_db; sqlite3_vfs *d_vfs; mutable sqlite3_stmt *d_stmt; // cache a (prepared) statement for reuse in subsequent transactions mutable char const *d_error_tail; std::string d_name; // non-owning pointer! std::pair *d_data; uint32_t d_databasewriteversion; bool d_readonly; bool d_ok; mutable std::map d_tables; // cache results of containsTable/tableContainsColumn mutable std::map> d_columns; mutable char d_previous_schema_version[11]; // 11 = maximum chars in string representing 4 byte number (+ '\0') protected: inline explicit SqliteDB(); inline explicit SqliteDB(std::string const &name, bool readonly = true); inline explicit SqliteDB(std::pair *data); inline SqliteDB(SqliteDB const &other); inline SqliteDB &operator=(SqliteDB const &other); inline ~SqliteDB(); public: inline bool ok() const; inline bool saveToFile(std::string const &filename) const; inline bool exec(std::string const &q, QueryResults *results = nullptr, bool verbose = false) const; inline bool exec(std::string const &q, std::any const ¶m, QueryResults *results = nullptr, bool verbose = false) const; #if __cpp_lib_ranges >= 201911L template requires std::ranges::input_range && std::is_same>::value inline bool exec(std::string const &q, R &¶ms, QueryResults *results = nullptr, bool verbose = false) const; #endif inline bool exec(std::string const &q, std::vector const ¶ms, QueryResults *results = nullptr, bool verbose = false) const; template inline T getSingleResultAs(std::string const &q, T defaultval) const; template inline T getSingleResultAs(std::string const &q, std::any const ¶m, T defaultval) const; template inline T getSingleResultAs(std::string const &q, std::vector const ¶ms, T defaultval) const; inline bool print(std::string const &q) const; inline bool print(std::string const &q, std::any const ¶m) const; inline bool print(std::string const &q, std::vector const ¶ms) const; inline bool prettyPrint(bool truncate, std::string const &q) const; inline bool prettyPrint(bool truncate, std::string const &q, std::any const ¶m) const; inline bool prettyPrint(bool truncate, std::string const &q, std::vector const ¶ms) const; inline bool printLineMode(std::string const &q) const; inline bool printLineMode(std::string const &q, std::any const ¶m) const; inline bool printLineMode(std::string const &q, std::vector const ¶ms) const; inline bool printSingleLine(std::string const &q) const; inline bool printSingleLine(std::string const &q, std::any const ¶m) const; inline bool printSingleLine(std::string const &q, std::vector const ¶ms) const; static bool copyDb(SqliteDB const &source, SqliteDB const &target); inline int changed() const; inline long long int lastId() const; inline bool containsTable(std::string const &tablename) const; inline bool tableContainsColumn(std::string const &tablename, std::string const &columnname) const; template inline bool tableContainsColumn(std::string const &tablename, std::string const &columnname, columnnames... list) const; inline void clearTableCache() const; inline void freeMemory(); void checkDatabaseWriteVersion() const; private: inline bool initFromFile(); inline bool initFromMemory(); inline void destroy(); inline int execParamFiller(int count, std::string const ¶m) const; inline int execParamFiller(int count, char const *param) const; inline int execParamFiller(int count, unsigned char const *param) const; inline int execParamFiller(int count, int param) const; inline int execParamFiller(int count, unsigned int param) const; inline int execParamFiller(int count, long param) const; inline int execParamFiller(int count, unsigned long param) const; inline int execParamFiller(int count, long long int param) const; inline int execParamFiller(int count, unsigned long long int param) const; inline int execParamFiller(int count, double param) const; inline int execParamFiller(int count, std::pair, size_t> const ¶m) const; inline int execParamFiller(int count, std::pair const ¶m) const; inline int execParamFiller(int count, StaticTextParam const ¶m) const; inline int execParamFiller(int count, std::nullptr_t param) const; template inline bool isType(std::any const &a) const; inline bool schemaVersionChanged() const; void setDatabaseWriteVersion(); inline bool registerCustoms() const; static inline void tokencount(sqlite3_context *context, int argc, sqlite3_value **argv); static inline void token(sqlite3_context *context, int argc, sqlite3_value **argv); static inline void jsonlong(sqlite3_context *context, int argc, sqlite3_value **argv); }; inline SqliteDB::SqliteDB() : SqliteDB(":memory:") {} inline SqliteDB::SqliteDB(std::string const &name, bool readonly) : d_db(nullptr), d_vfs(nullptr), d_stmt(nullptr), d_error_tail(nullptr), d_name(name), d_data(nullptr), d_databasewriteversion(0), d_readonly(readonly), d_ok(false), d_previous_schema_version{} { d_ok = initFromFile(); } inline SqliteDB::SqliteDB(std::pair *data) : d_db(nullptr), d_vfs(MemFileDB::sqlite3_memfilevfs(data)), d_stmt(nullptr), d_error_tail(nullptr), d_data(data), d_databasewriteversion(0), d_readonly(true), d_ok(false), d_previous_schema_version{} { d_ok = initFromMemory(); } inline SqliteDB::SqliteDB(SqliteDB const &other) : SqliteDB(":memory:") { if (d_ok) d_ok = copyDb(other, *this); } inline SqliteDB &SqliteDB::operator=(SqliteDB const &other) { if (this != &other) { // destroy this if its already an existing thing destroy(); // create d_db = nullptr; d_vfs = nullptr; d_stmt = nullptr; d_error_tail = nullptr; d_name = ":memory:"; d_data = nullptr; d_databasewriteversion = other.d_databasewriteversion; d_readonly = other.d_readonly; d_ok = initFromFile(); std::strncpy(d_previous_schema_version, other.d_previous_schema_version, 11); if (d_ok) d_ok = copyDb(other, *this); } return *this; } inline SqliteDB::~SqliteDB() { destroy(); } inline bool SqliteDB::initFromFile() { bool initok = false; if (d_name != ":memory:" && d_readonly) initok = (sqlite3_open_v2(d_name.c_str(), &d_db, SQLITE_OPEN_READONLY, nullptr) == SQLITE_OK); else initok = (sqlite3_open(d_name.c_str(), &d_db) == SQLITE_OK); if (!initok) return false; setDatabaseWriteVersion(); return registerCustoms(); } inline bool SqliteDB::initFromMemory() { bool initok = false; if (sqlite3_vfs_register(d_vfs, 0) == SQLITE_OK) initok = (sqlite3_open_v2(MemFileDB::vfsName(), &d_db, SQLITE_OPEN_READONLY, MemFileDB::vfsName()) == SQLITE_OK); if (!initok) return false; setDatabaseWriteVersion(); return registerCustoms(); } inline void SqliteDB::destroy() { if (d_stmt) sqlite3_finalize(d_stmt); if (d_vfs) sqlite3_vfs_unregister(d_vfs); if (d_db) sqlite3_close(d_db); } inline bool SqliteDB::ok() const { return d_ok; } inline bool SqliteDB::saveToFile(std::string const &filename) const { SqliteDB database(filename, false /*readonly*/); if (!SqliteDB::copyDb(*this, database)) { Logger::error("Failed to export sqlite database"); return false; } Logger::message("Saved database to file '", filename, "'"); return true; } inline bool SqliteDB::exec(std::string const &q, QueryResults *results, bool verbose) const { return exec(q, std::vector(), results, verbose); } inline bool SqliteDB::exec(std::string const &q, std::any const ¶m, QueryResults *results, bool verbose) const { return exec(q, std::vector{param}, results, verbose); } #if __cpp_lib_ranges >= 201911L template requires std::ranges::input_range && std::is_same>::value inline bool SqliteDB::exec(std::string const &q, R &¶ms, QueryResults *results, bool verbose) const #else inline bool SqliteDB::exec(std::string const &q, std::vector const ¶ms, QueryResults *results, bool verbose) const #endif { if (verbose) [[unlikely]] Logger::message("Running query: \"", q, "\""); // if we have no prepared statement OR // if this query is not the prepared one -> we need to reprepare it... if (!d_stmt || std::string_view(sqlite3_sql(d_stmt)) != q) { // destroy the old one (NOTE "Invoking sqlite3_finalize() on a NULL pointer is a harmless no-op.") sqlite3_finalize(d_stmt); // create new statement if (sqlite3_prepare_v2(d_db, q.c_str(), -1, &d_stmt, &d_error_tail) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_prepare_v2(): ", sqlite3_errmsg(d_db)); //// old way: just print the error //Logger::error_indent("\"", q, "\""); //// newer way: mark the point _around_ the error posistion // long long int error_pos = std::distance(q.c_str(), d_error_tail); // long long int error_start = std::max(0ll, error_pos - 2); // long long int error_end = std::min(error_pos + 2, static_cast(q.size())); // Logger::error_indent("-> Query: \"", // q.substr(0, error_start), // Logger::Control::BOLD, // q.substr(error_start, error_end - error_start), // Logger::Control::NORMAL, // q.substr(error_end), // "\""); // attempt to mark the token that sqlite choked on long long int error_pos = std::distance(q.c_str(), d_error_tail); long long int error_start = error_pos; // find the token where the error starts... while (error_start > 0 && ((q[error_start - 1] >= 'a' && q[error_start - 1] <= 'z') || (q[error_start - 1] >= 'A' && q[error_start - 1] <= 'Z') || (q[error_start - 1] >= '0' && q[error_start - 1] <= '9'))) --error_start; Logger::error_indent("-> Query: \"", q.substr(0, error_start), Logger::Control::BOLD, q.substr(error_start, error_pos - error_start), Logger::Control::NORMAL, q.substr(error_pos)); return false; } } else // reuse existing prepared statement { if (sqlite3_reset(d_stmt) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_reset(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } if (static_cast(params.size()) != sqlite3_bind_parameter_count(d_stmt)) [[unlikely]] { if (sqlite3_bind_parameter_count(d_stmt) < static_cast(params.size())) Logger::warning("Too few placeholders in query!"); else if (sqlite3_bind_parameter_count(d_stmt) > static_cast(params.size())) Logger::warning("Too many placeholders in query!"); Logger::warning_indent("-> Query: \"", q, "\" (parameters: ", params.size(), ", placeholders: ", sqlite3_bind_parameter_count(d_stmt), ")"); } #if __cplusplus > 201703L for (int i = 0; auto const &p : params) #else int i = 0; for (auto const &p : params) #endif { // order empirically determined if (isType(p)) { if (execParamFiller(i + 1, std::any_cast(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType(p)) { if (execParamFiller(i + 1, nullptr) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType(p)) { if (execParamFiller(i + 1, std::any_cast(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType, size_t>>(p)) { if (execParamFiller(i + 1, std::any_cast, size_t>>(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType(p)) { if (execParamFiller(i + 1, std::any_cast(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType(p)) { if (execParamFiller(i + 1, std::any_cast(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType(p)) { if (execParamFiller(i + 1, std::any_cast(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType(p)) { if (execParamFiller(i + 1, std::any_cast(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType(p)) { if (execParamFiller(i + 1, std::any_cast(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType(p)) { if (execParamFiller(i + 1, std::any_cast(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType(p)) { if (execParamFiller(i + 1, std::any_cast(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType(p)) { if (execParamFiller(i + 1, std::any_cast(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType>(p)) { if (execParamFiller(i + 1, std::any_cast>(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else if (isType(p)) { if (execParamFiller(i + 1, std::any_cast(p)) != SQLITE_OK) [[unlikely]] { Logger::error("During sqlite3_bind_*(): ", sqlite3_errmsg(d_db)); Logger::error_indent("-> Query: \"", q, "\""); return false; } } else { Logger::error("Unhandled parameter type ", p.type().name()); Logger::error_indent("-> Query: \"", q, "\""); return false; } ++i; } if (results) results->clear(); int rc; int row = 0; while ((rc = sqlite3_step(d_stmt)) == SQLITE_ROW) { if (!results) continue; // if headers aren't set, set them if (results->columns() == 0) for (int c = 0; c < sqlite3_column_count(d_stmt); ++c) results->emplaceHeader(sqlite3_column_name(d_stmt, c)); // set values for (int c = 0; c < sqlite3_column_count(d_stmt); ++c) { // order empirically determined if (sqlite3_column_type(d_stmt, c) == SQLITE_INTEGER) results->emplaceValue(row, sqlite3_column_int64(d_stmt, c)); else if (sqlite3_column_type(d_stmt, c) == SQLITE_NULL) results->emplaceValue(row, nullptr); else if (sqlite3_column_type(d_stmt, c) == SQLITE_TEXT) results->emplaceValue(row, std::string(reinterpret_cast(sqlite3_column_text(d_stmt, c)))); else if (sqlite3_column_type(d_stmt, c) == SQLITE_BLOB) { size_t blobsize = sqlite3_column_bytes(d_stmt, c); std::shared_ptr blob(new unsigned char[blobsize]); if (blobsize) // if 0, sqlite3_column_blob() is nullptr, which is UB for memcpy std::memcpy(blob.get(), reinterpret_cast(sqlite3_column_blob(d_stmt, c)), blobsize); results->emplaceValue(row, std::make_pair(blob, blobsize)); } else if (sqlite3_column_type(d_stmt, c) == SQLITE_FLOAT) results->emplaceValue(row, sqlite3_column_double(d_stmt, c)); } ++row; } if (rc != SQLITE_DONE) { Logger::error("After sqlite3_step(): ", sqlite3_errmsg(d_db)); char *expanded_query = sqlite3_expanded_sql(d_stmt); if (expanded_query) { Logger::error_indent("-> Query: \"", expanded_query, "\""); sqlite3_free(expanded_query); } return false; } if (schemaVersionChanged()) [[unlikely]] clearTableCache(); return true; } #if __cpp_lib_ranges >= 201911L inline bool SqliteDB::exec(std::string const &q, std::vector const ¶ms, QueryResults *results, bool verbose) const { return exec(q, std::views::all(params), results, verbose); } #endif template inline T SqliteDB::getSingleResultAs(std::string const &q, T defaultval) const { return getSingleResultAs(q, std::vector(), defaultval); } template inline T SqliteDB::getSingleResultAs(std::string const &q, std::any const ¶m, T defaultval) const { return getSingleResultAs(q, std::vector{param}, defaultval); } template inline T SqliteDB::getSingleResultAs(std::string const &q, std::vector const ¶ms, T defaultval) const { QueryResults tmp; if (!exec(q, params, &tmp)) return defaultval; if (tmp.rows() != 1 || tmp.columns() != 1 || !tmp.valueHasType(0, 0)) { //if (tmp.rows() && tmp.columns()) // std::cout << "Type: " << tmp.value(0, 0).type().name() << " Requested type: " << typeid(T).name() << std::endl; return defaultval; } return tmp.getValueAs(0, 0); } inline bool SqliteDB::print(std::string const &q) const { return print(q, std::vector()); } inline bool SqliteDB::print(std::string const &q, std::any const ¶m) const { return print(q, std::vector{param}); } inline bool SqliteDB::print(std::string const &q, std::vector const ¶ms) const { QueryResults results; bool ret = exec(q, params, &results); results.print(); return ret; } inline bool SqliteDB::prettyPrint(bool truncate, std::string const &q) const { return prettyPrint(truncate, q, std::vector()); } inline bool SqliteDB::prettyPrint(bool truncate, std::string const &q, std::any const ¶m) const { return prettyPrint(truncate, q, std::vector{param}); } inline bool SqliteDB::prettyPrint(bool truncate, std::string const &q, std::vector const ¶ms) const { QueryResults results; bool ret = exec(q, params, &results); results.prettyPrint(truncate); return ret; } inline bool SqliteDB::printLineMode(std::string const &q) const { return printLineMode(q, std::vector()); } inline bool SqliteDB::printLineMode(std::string const &q, std::any const ¶m) const { return printLineMode(q, std::vector{param}); } inline bool SqliteDB::printLineMode(std::string const &q, std::vector const ¶ms) const { QueryResults results; bool ret = exec(q, params, &results); results.printLineMode(); return ret; } inline bool SqliteDB::printSingleLine(std::string const &q) const { return printSingleLine(q, std::vector()); } inline bool SqliteDB::printSingleLine(std::string const &q, std::any const ¶m) const { return printSingleLine(q, std::vector{param}); } inline bool SqliteDB::printSingleLine(std::string const &q, std::vector const ¶ms) const { QueryResults results; bool ret = exec(q, params, &results); results.printSingleLine(); return ret; } template inline bool SqliteDB::isType(std::any const &a) const { return (a.type() == typeid(T)); } inline int SqliteDB::execParamFiller(int count, std::string const ¶m) const { //std::cout << "Binding STRING at " << count << ": " << param.c_str() << std::endl; return sqlite3_bind_text(d_stmt, count, param.c_str(), -1, SQLITE_TRANSIENT); } inline int SqliteDB::execParamFiller(int count, char const *param) const { //std::cout << "Binding CHAR CONST * at " << count << ": " << param << std::endl; return sqlite3_bind_text(d_stmt, count, param, -1, SQLITE_STATIC);//TRANSIENT); } inline int SqliteDB::execParamFiller(int count, unsigned char const *param) const { //std::cout << "Binding UNSIGNED CHAR CONST * at " << count << std::endl; return sqlite3_bind_text(d_stmt, count, reinterpret_cast(param), -1, SQLITE_STATIC);//TRANSIENT); } inline int SqliteDB::execParamFiller(int count, std::pair, size_t> const ¶m) const { //std::cout << "Binding BLOB at " << count << std::endl; return sqlite3_bind_blob(d_stmt, count, reinterpret_cast(param.first.get()), param.second, SQLITE_STATIC);//TRANSIENT); } inline int SqliteDB::execParamFiller(int count, std::pair const ¶m) const { //std::cout << "Binding BLOB at " << count << std::endl; return sqlite3_bind_blob(d_stmt, count, reinterpret_cast(param.first), param.second, SQLITE_STATIC);//TRANSIENT); } inline int SqliteDB::execParamFiller(int count, StaticTextParam const ¶m) const { //std::cout << "Binding STATIC TEXT at " << count << std::endl; return sqlite3_bind_text(d_stmt, count, param.ptr, param.size, SQLITE_STATIC); } inline int SqliteDB::execParamFiller(int count, int param) const { //std::cout << "Binding long long int at " << count << ": " << param << std::endl; return sqlite3_bind_int64(d_stmt, count, param); } inline int SqliteDB::execParamFiller(int count, unsigned int param) const { //std::cout << "Binding long long int at " << count << ": " << param << std::endl; return sqlite3_bind_int64(d_stmt, count, param); } inline int SqliteDB::execParamFiller(int count, long param) const { //std::cout << "Binding long long int at " << count << ": " << param << std::endl; return sqlite3_bind_int64(d_stmt, count, param); } inline int SqliteDB::execParamFiller(int count, unsigned long param) const { //std::cout << "Binding long long int at " << count << ": " << param << std::endl; return sqlite3_bind_int64(d_stmt, count, param); } inline int SqliteDB::execParamFiller(int count, long long int param) const { //std::cout << "Binding long long int at " << count << ": " << param << std::endl; return sqlite3_bind_int64(d_stmt, count, param); } inline int SqliteDB::execParamFiller(int count, unsigned long long int param) const { //std::cout << "Binding long long int at " << count << ": " << param << std::endl; return sqlite3_bind_int64(d_stmt, count, param); } inline int SqliteDB::execParamFiller(int count, std::nullptr_t) const { //std::cout << "Binding NULL at " << count << std::endl; return sqlite3_bind_null(d_stmt, count); } inline int SqliteDB::execParamFiller(int count, double param) const { //std::cout << "Binding DOUBLE at " << count << ": " << param << std::endl; return sqlite3_bind_double(d_stmt, count, param); } inline int SqliteDB::changed() const { return sqlite3_changes(d_db); } inline long long int SqliteDB::lastId() const { return sqlite3_last_insert_rowid(d_db); } inline bool SqliteDB::containsTable(std::string const &tablename) const { if (auto it = d_tables.find(tablename); it != d_tables.end()) return it->second; QueryResults tmp; if (exec("SELECT DISTINCT tbl_name FROM sqlite_master WHERE type = 'table' AND tbl_name = '" + tablename + "'", &tmp) && tmp.rows() > 0) { d_tables.emplace(tablename, true); return true; } d_tables.emplace(tablename, false); return false; } inline bool SqliteDB::tableContainsColumn(std::string const &tablename, std::string const &columnname) const { auto it1 = d_columns.find(tablename); if (it1 != d_columns.end()) if (auto it2 = it1->second.find(columnname); it2 != it1->second.end()) return it2->second; QueryResults tmp; if (exec("SELECT 1 FROM PRAGMA_TABLE_XINFO('" + tablename + "') WHERE name == '" + columnname + "'", &tmp) && tmp.rows() > 0) { if (it1 != d_columns.end()) it1->second.emplace(columnname, true); else d_columns[tablename].emplace(columnname, true); return true; } if (it1 != d_columns.end()) it1->second.emplace(columnname, false); else d_columns[tablename].emplace(columnname, false); return false; } template inline bool SqliteDB::tableContainsColumn(std::string const &tablename, std::string const &columnname, columnnames... list) const { return tableContainsColumn(tablename, columnname) && tableContainsColumn(tablename, list...); } inline void SqliteDB::clearTableCache() const { d_tables.clear(); d_columns.clear(); } inline void SqliteDB::freeMemory() { sqlite3_db_release_memory(d_db); } inline void SqliteDB::QueryResults::emplaceHeader(std::string &&h) { d_headers.emplace_back(h); } inline std::string const &SqliteDB::QueryResults::header(size_t idx) const { return d_headers[idx]; } inline std::vector const &SqliteDB::QueryResults::headers() const { return d_headers; } inline bool SqliteDB::QueryResults::hasColumn(std::string const &h) const { return bepaald::contains(d_headers, h); } inline void SqliteDB::QueryResults::emplaceValue(size_t row, std::any &&a) { if (d_values.size() < row + 1) d_values.resize(row + 1); d_values[row].emplace_back(a); } inline std::any const &SqliteDB::QueryResults::value(size_t row, size_t idx) const { return d_values[row][idx]; } inline int SqliteDB::QueryResults::idxOfHeader(std::string const &header) const { for (unsigned int i = 0; i < d_headers.size(); ++i) if (d_headers[i] == header) return i; [[unlikely]] return -1; } inline std::any SqliteDB::QueryResults::value(size_t row, std::string const &header) const { int i = idxOfHeader(header); if (i == -1) [[unlikely]] { Logger::warning("Column `", header, "' not found in query results"); return std::any{nullptr}; } return d_values[row][i]; } template inline T SqliteDB::QueryResults::getValueAs(size_t row, std::string const &header) const { int i = idxOfHeader(header); if (i == -1) [[unlikely]] { Logger::warning("Column `", header, "' not found in query results"); return T{}; } if (d_values[row][i].type() != typeid(T)) [[unlikely]] { Logger::message("Getting value of field '", header, "' (idx ", i, "). Value as string: ", valueAsString(row, i)); Logger::message("Type: ", d_values[row][i].type().name(), " Requested type: ", typeid(T).name()); //return T{}; } return std::any_cast(d_values[row][i]); } template inline bool SqliteDB::QueryResults::valueHasType(size_t row, std::string const &header) const { int i = idxOfHeader(header); if (i == -1) [[unlikely]] { Logger::warning("Column `", header, "' not found in query results"); return false; } return (d_values[row][i].type() == typeid(T)); } template inline bool SqliteDB::QueryResults::valueHasType(size_t row, size_t idx) const { return (d_values[row][idx].type() == typeid(T)); } inline bool SqliteDB::QueryResults::isNull(size_t row, size_t idx) const { return valueHasType(row, idx); } inline bool SqliteDB::QueryResults::isNull(size_t row, std::string const &header) const { return valueHasType(row, header); } template inline T SqliteDB::QueryResults::getValueAs(size_t row, size_t idx) const { return std::any_cast(d_values[row][idx]); } inline bool SqliteDB::QueryResults::empty() const { return d_values.empty(); } inline size_t SqliteDB::QueryResults::rows() const { return d_values.size(); } inline size_t SqliteDB::QueryResults::columns() const { return d_headers.size(); } inline void SqliteDB::QueryResults::clear() { d_headers.clear(); d_values.clear(); } inline std::string SqliteDB::QueryResults::operator()(size_t row, std::string const &header) const { return valueAsString(row, header); } inline std::string SqliteDB::QueryResults::operator()(std::string const &header) const { return valueAsString(0, header); } template inline bool SqliteDB::QueryResults::contains(T const &value) const { for (unsigned int i = 0; i < d_values.size(); ++i) for (unsigned int j = 0; j < d_values[i].size(); ++j) if (d_values[i][j].type() == typeid(T)) if (std::any_cast(d_values[i][j]) == value) return true; return false; } inline std::vector const &SqliteDB::QueryResults::row(size_t row) const { return d_values[row]; } /* If you know that the data is UTF-8, then you just have to check the high bit: 0xxxxxxx = single-byte ASCII character 1xxxxxxx = part of multi-byte character Or, if you need to distinguish lead/trail bytes: 10xxxxxx = 2nd, 3rd, or 4th byte of multi-byte character 110xxxxx = 1st byte of 2-byte character 1110xxxx = 1st byte of 3-byte character 11110xxx = 1st byte of 4-byte character */ inline uint64_t SqliteDB::QueryResults::charCount(std::string const &utf8) const { uint64_t ret = utf8.length(); for (unsigned int i = 0; i < utf8.size(); ++i) if ((utf8[i] & 0b11111000) == 0b11110000) [[unlikely]] ret -= 3; else if ((utf8[i] & 0b11100000) == 0b11000000) [[unlikely]] --ret; else if ((utf8[i] & 0b11110000) == 0b11100000) [[unlikely]] ret -= 2; return ret; } inline bool SqliteDB::QueryResults::removeRow(unsigned int idx) { if (idx >= d_values.size()) return false; d_values.erase(d_values.begin() + idx); return true; } inline SqliteDB::QueryResults SqliteDB::QueryResults::getRow(unsigned int idx) { QueryResults tmp; tmp.d_headers = d_headers; tmp.d_values.push_back(d_values[idx]); return tmp; } inline bool SqliteDB::schemaVersionChanged() const { std::pair sv_data = {false, d_previous_schema_version}; sqlite3_exec(d_db, "SELECT schema_version FROM PRAGMA_SCHEMA_VERSION;", [](void *sv, int /*count*/, char **data, char **) { // assert(count == 1); // compare the new schema_version (data[0]) with the old one (passed through sv_data.second) if (strncmp(reinterpret_cast *>(sv)->second, data[0], 11) != 0) [[unlikely]] { // if not equal, set changed and copy the new version reinterpret_cast *>(sv)->first = true; std::strncpy(reinterpret_cast *>(sv)->second, data[0], 11); } return 0; }, &sv_data, nullptr); return sv_data.first; } inline bool SqliteDB::registerCustoms() const { return sqlite3_create_function(d_db, "TOKENCOUNT", -1, SQLITE_UTF8, nullptr, &tokencount, nullptr, nullptr) == SQLITE_OK && sqlite3_create_function(d_db, "TOKEN", -1, SQLITE_UTF8, nullptr, &token, nullptr, nullptr) == SQLITE_OK && sqlite3_create_function(d_db, "JSONLONG", -1, SQLITE_UTF8, nullptr, &jsonlong, nullptr, nullptr) == SQLITE_OK; } inline void SqliteDB::tokencount(sqlite3_context *context, int argc, sqlite3_value **argv) //static { if (argc == 0) { sqlite3_result_int(context, 0); return; } if (sqlite3_value_type(argv[0]) != SQLITE_TEXT) { sqlite3_result_int(context, 0); return; } unsigned char const *text = sqlite3_value_text(argv[0]); if (!text || !text[0]) { sqlite3_result_int(context, 0); return; } char delim = ' '; if (argc > 1 && argv[1] && sqlite3_value_text(argv[1])[0]) delim = (sqlite3_value_text(argv[1]))[0]; int startpos = 0; int count = 1; while (text[startpos]) { if (text[startpos] == delim) { ++count; while (text[startpos] == delim) ++startpos; } else ++startpos; } sqlite3_result_int(context, count); } inline void SqliteDB::token(sqlite3_context *context, int argc, sqlite3_value **argv) // static { if (argc > 1) { if (sqlite3_value_type(argv[0]) != SQLITE_TEXT) { sqlite3_result_null(context); return; } // get params unsigned char const *text = sqlite3_value_text(argv[0]); if (!text) return; unsigned int idx = sqlite3_value_int(argv[1]); char delim = ' '; if (argc > 2 && argv[2] && sqlite3_value_text(argv[2])[0]) delim = (sqlite3_value_text(argv[2]))[0]; //std::cout << "DELIM: '" << delim << "' (" << (static_cast(delim) & 0xff) << ")" << std::endl; // find the requested token unsigned int startpos = 0; unsigned int endpos = 0; unsigned int count = 0; while (text[startpos]) { if (count == idx) // look for end delim { endpos = startpos; break; } if (text[startpos] == delim) { ++count; while (text[startpos] == delim) ++startpos; } else ++startpos; } while (text[endpos] && text[endpos] != delim) ++endpos; //std::cout << " TEXT: " << text << std::endl; //std::cout << "RANGE: " << std::string(startpos, ' ') << "^" << std::string(endpos < startpos ? 0 : endpos - startpos, ' ') << "^" < startpos) { int len = endpos - startpos; char *result = new char[len]; std::memcpy(result, text + startpos, len); sqlite3_result_text(context, result, len, SQLITE_TRANSIENT); bepaald::destroyPtr(&result, &len); return; } } sqlite3_result_null(context); } inline void SqliteDB::jsonlong(sqlite3_context *context, int argc, sqlite3_value **argv) // static { if (argc == 1) [[likely]] { // value is already a number probably if (sqlite3_value_type(argv[0]) != SQLITE_TEXT) [[likely]] { sqlite3_result_value(context, argv[0]); return; } // we have a string, check if it's '{low: N, high: M, unsigned}' unsigned char const *text = sqlite3_value_text(argv[0]); if (!text) return; // not sure what we're doing here... SqliteDB::QueryResults res; SqliteDB sqldb(":memory:"); if (sqldb.exec("SELECT " "IIF(json_valid(?1), json_extract(?1, '$.low'), NULL) AS low, " "IIF(json_valid(?1), json_extract(?1, '$.high'), NULL) AS high, " "IIF(json_valid(?1), json_extract(?1, '$.unsigned'), NULL) AS unsigned", text, &res) && res.rows() == 1 && (!res.isNull(0, "low") || !res.isNull(0, "high"))) { long long int jl = 0; if (!res.isNull(0, "high")) jl |= (res.getValueAs(0, "high") << 32); if (!res.isNull(0, "low")) jl |= (res.getValueAs(0, "low") & 0xFFFFFFFF); sqlite3_result_int64(context, jl); return; } //return copy of found string, it is not a json long object... sqlite3_result_value(context, argv[0]); return; } //std::cout << "No results, returning null (" << argc << ")" << std::endl; sqlite3_result_null(context); } #endif signalbackup-tools-20250313-1/sqlitedb/sqlitedb.ih000066400000000000000000000016231476450434500216700ustar00rootroot00000000000000/* Copyright (C) 2019-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlitedb.h" #include #include #include #include #include #include #include "../base64/base64.h" signalbackup-tools-20250313-1/sqlitedb/valueasint.cc000066400000000000000000000037571476450434500222330ustar00rootroot00000000000000/* Copyright (C) 2023-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlitedb.ih" long long int SqliteDB::QueryResults::valueAsInt(size_t row, size_t column, long long int def) const { // order empirically determined if (valueHasType(row, column)) return getValueAs(row, column); if (valueHasType(row, column)) return def; if (valueHasType(row, column)) return bepaald::toNumber(getValueAs(row, column), def); if (valueHasType, size_t>>(row, column)) return def; if (valueHasType(row, column)) return getValueAs(row, column); if (valueHasType(row, column)) return getValueAs(row, column); if (valueHasType(row, column)) return getValueAs(row, column); if (valueHasType(row, column)) return def; else [[unlikely]] return def; } long long int SqliteDB::QueryResults::valueAsInt(size_t row, std::string const &header, long long int def) const { int i = idxOfHeader(header); if (i == -1) [[unlikely]] { Logger::warning("Column `", header, "' not found in query results"); return def; } return valueAsInt(row, i, def); } signalbackup-tools-20250313-1/sqlitedb/valueasstring.cc000066400000000000000000000044671476450434500227460ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlitedb.ih" std::string SqliteDB::QueryResults::valueAsString(size_t row, size_t column) const { // order empirically determined if (valueHasType(row, column)) return std::string(); if (valueHasType(row, column)) return getValueAs(row, column); if (valueHasType(row, column)) return bepaald::toString(getValueAs(row, column)); if (valueHasType, size_t>>(row, column)) return Base64::bytesToBase64String(getValueAs, size_t>>(row, column).first.get(), getValueAs, size_t>>(row, column).second); if (valueHasType(row, column)) return bepaald::toString(getValueAs(row, column)); if (valueHasType(row, column)) return bepaald::toString(getValueAs(row, column)); if (valueHasType(row, column)) return bepaald::toString(getValueAs(row, column)); if (valueHasType(row, column)) return bepaald::toString(getValueAs(row, column)); else [[unlikely]] return "(unhandled type)"; } std::string SqliteDB::QueryResults::valueAsString(size_t row, std::string const &header) const { int i = idxOfHeader(header); if (i == -1) [[unlikely]] { Logger::warning("Column `", header, "' not found in query results"); return "(column not found)"; } return valueAsString(row, i); } signalbackup-tools-20250313-1/sqlstatementframe/000077500000000000000000000000001476450434500214655ustar00rootroot00000000000000signalbackup-tools-20250313-1/sqlstatementframe/buildstatement.cc000066400000000000000000000062031476450434500250210ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlstatementframe.ih" #include "../common_bytes.h" void SqlStatementFrame::buildStatement() { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::STATEMENT) { d_statement = bepaald::bytesToString(std::get<1>(p), std::get<2>(p)); if (d_parameterdata.size() == 0) // only a statement was given, no parameters to insert... return; } // check if number of parameters equals number of '?' if (std::count(d_statement.begin(), d_statement.end(), '?') != static_cast(d_parameterdata.size())) [[unlikely]] { Logger::error("Bad substitution count: "); Logger::error_indent("Statement: ", d_statement, " parameters: ", d_parameterdata.size()); d_statement.clear(); return; } std::string::size_type pos = 0; for (auto const &p : d_parameterdata) { pos = d_statement.find('?', pos); if (pos == std::string::npos) [[unlikely]] { DEBUGOUT("Fail to find '?'"); d_statement.clear(); return; } switch (std::get<0>(p)) { case PARAMETER_FIELD::INT: { d_statement.replace(pos, 1, std::to_string(static_cast(bytesToUint64(std::get<1>(p), std::get<2>(p))))); break; } case PARAMETER_FIELD::NULLPARAMETER: { d_statement.replace(pos, 1, "NULL"); break; } case PARAMETER_FIELD::STRING: { std::string rep = bepaald::bytesToString(std::get<1>(p), std::get<2>(p)); std::string::size_type pos2 = 0; while ((pos2 = rep.find('\'', pos2)) != std::string::npos) { rep.replace(pos2, 1, "''"); pos2 += 2; } rep = '\'' + rep + '\''; d_statement.replace(pos, 1, rep); pos += rep.length(); break; } case PARAMETER_FIELD::BLOB: { d_statement.replace(pos, 1, "X'" + bepaald::bytesToHexString(std::get<1>(p), std::get<2>(p), true) + '\''); break; } case PARAMETER_FIELD::DOUBLE: { std::stringstream ss; ss.imbue(std::locale(std::locale(), new Period)); // make sure we get periods as decimal indicators ss << std::defaultfloat << std::setprecision(17) << *reinterpret_cast(std::get<1>(p)); d_statement.replace(pos, 1, ss.str()); break; } [[unlikely]] default: Logger::error("Unknown parameter type in SqlStatementFrame (", std::get<0>(p), ")."); } } } signalbackup-tools-20250313-1/sqlstatementframe/sqlstatementframe.h000066400000000000000000000576161476450434500254140ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef SQLSTATEMENTFRAME_H_ #define SQLSTATEMENTFRAME_H_ #include #include #include #include #include #include "../common_be.h" #include "../common_bytes.h" #include "../backupframe/backupframe.h" struct Period final : std::numpunct { char do_decimal_point() const override { return '.'; } // make sure std::to_string always uses period for }; // decimal point, regardless of locale class SqlStatementFrame : public BackupFrame { public: enum PARAMETER_FIELD { STRING = 1, // string INT = 2, // uint64 DOUBLE = 3, // double BLOB = 4, // bytes NULLPARAMETER = 5 // bool }; enum FIELD { STATEMENT = 1, // string PARAMETERS = 2 // PARAMETER_FIELD (repeated) }; private: static Registrar s_registrar; std::vector> d_parameterdata; // PARAMETER_FIELD, bytes, size std::string d_statement; public: inline SqlStatementFrame(); inline SqlStatementFrame(unsigned char const *data, size_t length, uint64_t count = 0); inline SqlStatementFrame(SqlStatementFrame &&other); inline SqlStatementFrame &operator=(SqlStatementFrame &&other); inline SqlStatementFrame(SqlStatementFrame const &other); inline SqlStatementFrame &operator=(SqlStatementFrame const &other); inline virtual ~SqlStatementFrame() override; inline virtual SqlStatementFrame *clone() const override; inline virtual SqlStatementFrame *move_clone() override; inline virtual FRAMETYPE frameType() const override; inline static BackupFrame *create(unsigned char const *data, size_t length, uint64_t count = 0); inline virtual void printInfo() const override; inline void printInfo(std::vector const ¶mternames) const; inline std::string const &statement(); inline std::pair getData() const override; inline void setStatementField(std::string const &val); inline void addStringParameter(std::string const &val); inline void addBlobParameter(std::pair, size_t> const &val); inline void addIntParameter(int64_t val); inline void addNullParameter(); inline void addDoubleParameter(double val); inline void addParameterField(PARAMETER_FIELD field, std::string const &val); inline std::string bindStatement() const; inline std::vector parameters() const; // inline void setParameter(unsigned int idx, unsigned char *data, uint32_t length); // inline void getParameter(unsigned int idx) const; // inline std::string getParameterAsString(unsigned int idx) const; // inline uint64_t getParameterAsUint64(unsigned int idx) const; inline virtual bool validate(uint64_t) const override; private: void buildStatement(); inline uint64_t dataSize() const override; }; inline SqlStatementFrame::SqlStatementFrame() : BackupFrame(-1) {} inline SqlStatementFrame::SqlStatementFrame(unsigned char const *data, size_t length, uint64_t count) : BackupFrame(data, length, count) { //std::cout << "CREATING SQLSTATEMENTFRAME" << std::endl; for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::PARAMETERS) { //std::cout << "INITIALIZING PARAMETERS: " << std::get<2>(p) << " bytes" << std::endl; if (!init(std::get<1>(p), std::get<2>(p), &d_parameterdata)) [[unlikely]] { d_ok = false; break; } } } inline SqlStatementFrame::SqlStatementFrame(SqlStatementFrame &&other) : BackupFrame(std::move(other)), d_parameterdata(std::move(other.d_parameterdata)) { other.d_parameterdata.clear(); } inline SqlStatementFrame &SqlStatementFrame::operator=(SqlStatementFrame &&other) { if (this != &other) { // properly delete any data this is holding for (unsigned int i = 0; i < d_parameterdata.size(); ++i) if (std::get<1>(d_parameterdata[i])) delete[] std::get<1>(d_parameterdata[i]); d_parameterdata.clear(); BackupFrame::operator=(std::move(other)); d_parameterdata = std::move(other.d_parameterdata); other.d_parameterdata.clear(); } return *this; } inline SqlStatementFrame::SqlStatementFrame(SqlStatementFrame const &other) : BackupFrame(other), d_statement(other.d_statement) { for (unsigned int i = 0; i < other.d_parameterdata.size(); ++i) { unsigned char *datacpy = nullptr; if (std::get<1>(other.d_parameterdata[i])) { datacpy = new unsigned char[std::get<2>(other.d_parameterdata[i])]; std::memcpy(datacpy, std::get<1>(other.d_parameterdata[i]), std::get<2>(other.d_parameterdata[i])); } d_parameterdata.emplace_back(std::make_tuple(std::get<0>(other.d_parameterdata[i]), datacpy, std::get<2>(other.d_parameterdata[i]))); } } inline SqlStatementFrame &SqlStatementFrame::operator=(SqlStatementFrame const &other) { if (this != &other) { BackupFrame::operator=(other); d_statement = other.d_statement; for (unsigned int i = 0; i < other.d_parameterdata.size(); ++i) { unsigned char *datacpy = nullptr; if (std::get<1>(other.d_parameterdata[i])) { datacpy = new unsigned char[std::get<2>(other.d_parameterdata[i])]; std::memcpy(datacpy, std::get<1>(other.d_parameterdata[i]), std::get<2>(other.d_parameterdata[i])); } d_parameterdata.emplace_back(std::make_tuple(std::get<0>(other.d_parameterdata[i]), datacpy, std::get<2>(other.d_parameterdata[i]))); } } return *this; } inline SqlStatementFrame::~SqlStatementFrame() { //std::cout << "DESTROYING SQLSTATEMENTFRAME" << std::endl; for (unsigned int i = 0; i < d_parameterdata.size(); ++i) if (std::get<1>(d_parameterdata[i])) delete[] std::get<1>(d_parameterdata[i]); d_parameterdata.clear(); } inline SqlStatementFrame *SqlStatementFrame::clone() const { return new SqlStatementFrame(*this); } inline SqlStatementFrame *SqlStatementFrame::move_clone() { return new SqlStatementFrame(std::move(*this)); } inline BackupFrame::FRAMETYPE SqlStatementFrame::frameType() const // virtual override { return FRAMETYPE::SQLSTATEMENT; } inline BackupFrame *SqlStatementFrame::create(unsigned char const *data, size_t length, uint64_t count) // static { return new SqlStatementFrame(data, length, count); } inline void SqlStatementFrame::printInfo() const { //DEBUGOUT("TYPE: SQLSTATEMENTFRAME"); Logger::message("Frame number: ", d_count); Logger::message(" Size: ", d_constructedsize); Logger::message(" Type: SQLSTATEMENT"); unsigned int param_ctr = 0; for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::STATEMENT) { Logger::message(" - (statement: \"", bepaald::bytesToString(std::get<1>(p), std::get<2>(p)), "\" (", std::get<2>(p), " bytes)"); break; // ONLY ONE FIELD::STATEMENT } for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::PARAMETERS) { if (param_ctr < d_parameterdata.size()) { switch (std::get<0>(d_parameterdata[param_ctr])) { case PARAMETER_FIELD::INT: Logger::message(" - (uint64 parameter): \"", bytesToUint64(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr])), "\""); break; case PARAMETER_FIELD::NULLPARAMETER: Logger::message(" - (bool parameter) : \"", std::boolalpha, (bytesToUint64(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr])) ? true : false), "\" (value: \"", bytesToUint64(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr])), "\")"); break; case PARAMETER_FIELD::STRING: Logger::message(" - (string parameter): \"", bepaald::bytesToString(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr])), "\""); break; case PARAMETER_FIELD::BLOB: Logger::message(" - (binary parameter): \"", bepaald::bytesToHexString(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr])), "\""); break; case PARAMETER_FIELD::DOUBLE: Logger::message(" - (double parameter): \"", bepaald::toString(*reinterpret_cast(std::get<1>(d_parameterdata[param_ctr]))), "\" ", bepaald::bytesToHexString(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr]))); break; } ++param_ctr; } } } } inline void SqlStatementFrame::printInfo(std::vector const ¶meternames) const { if (d_parameterdata.size() != parameternames.size()) { Logger::message(d_parameterdata.size()); Logger::message(parameternames.size()); return printInfo(); } //DEBUGOUT("TYPE: SQLSTATEMENTFRAME"); Logger::message("Frame number: ", d_count); Logger::message(" Type: SQLSTATEMENT"); unsigned int param_ctr = 0; for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::STATEMENT) { Logger::message(" - (statement: \"", bepaald::bytesToString(std::get<1>(p), std::get<2>(p)), "\" (", std::get<2>(p), " bytes)"); break; // only one FIELD::STATEMENT } for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::PARAMETERS) { if (param_ctr < d_parameterdata.size()) { switch (std::get<0>(d_parameterdata[param_ctr])) { case PARAMETER_FIELD::INT: Logger::message(" - ", parameternames[param_ctr], " (uint64 parameter): \"", bytesToUint64(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr])), "\""); break; case PARAMETER_FIELD::NULLPARAMETER: Logger::message(" - ", parameternames[param_ctr], " (bool parameter): \"", std::boolalpha, (bytesToUint64(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr])) ? true : false), "\" (value: \"", bytesToUint64(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr])), "\")"); break; case PARAMETER_FIELD::STRING: Logger::message(" - ", parameternames[param_ctr], " (string parameter): \"", bepaald::bytesToString(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr])), "\""); break; case PARAMETER_FIELD::BLOB: Logger::message(" - ", parameternames[param_ctr], " (binary parameter): \"", bepaald::bytesToHexString(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr])), "\""); break; case PARAMETER_FIELD::DOUBLE: Logger::message(" - ", parameternames[param_ctr], " (double parameter): \"", *reinterpret_cast(std::get<1>(d_parameterdata[param_ctr])), "\" ", bepaald::bytesToHexString(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr]))); break; } ++param_ctr; } } } } std::string const &SqlStatementFrame::statement() { if (d_statement.empty()) buildStatement(); return d_statement; } inline uint64_t SqlStatementFrame::dataSize() const { uint64_t size = 0; for (auto const &fd : d_framedata) { if (std::get<0>(fd) == FIELD::STATEMENT) { uint64_t statementsize = std::get<2>(fd); // length of actual data // length of length size += varIntSize(statementsize); size += statementsize + 1; // plus one for fieldtype + wiretype } } for (auto const &pd : d_parameterdata) { switch (std::get<0>(pd)) { case PARAMETER_FIELD::INT: { uint64_t value = bytesToUint64(std::get<1>(pd), std::get<2>(pd)); size += varIntSize(value); size += 1; // for fieldtype + wiretype size += varIntSize(varIntSize(value) + 1); // to write size of parameter field into break; } case PARAMETER_FIELD::NULLPARAMETER: { uint64_t value = bytesToUint64(std::get<1>(pd), std::get<2>(pd)); size += varIntSize(value); size += 1; // for fieldtype + wiretype size += varIntSize(varIntSize(value) + 1); // to write size of parameter field into break; } case PARAMETER_FIELD::STRING: { uint64_t stringsize = std::get<2>(pd); size += varIntSize(stringsize); size += stringsize + 1; // +1 for fieldtype + wiretype size += varIntSize(stringsize + 2); // to write size of parameter field into break; } case PARAMETER_FIELD::BLOB: { uint64_t stringsize = std::get<2>(pd); size += varIntSize(stringsize); size += stringsize + 1; size += varIntSize(stringsize + 2); // to write size of parameter field into break; } case PARAMETER_FIELD::DOUBLE: { size += 9; // fixed64 + 1 for field and wiretype? size += varIntSize(9 + 1); // for parameter_field + type break; } } size += 1; // for fieldtype + wiretype? } // for size of this entire frame. size += varIntSize(size); return ++size; // for frametype and wiretype } inline std::pair SqlStatementFrame::getData() const { uint64_t size = dataSize(); unsigned char *data = new unsigned char[size]; uint64_t datapos = 0; datapos += setFieldAndWire(FRAMETYPE::SQLSTATEMENT, WIRETYPE::LENGTHDELIM, data + datapos); datapos += setFrameSize(size, data + datapos); unsigned int param_ctr = 0; for (auto const &fd : d_framedata) if (std::get<0>(fd) == FIELD::STATEMENT) { datapos += putLengthDelimType(fd, data + datapos); break; // only one FIELD::STATEMENT } for (auto const &fd : d_framedata) { if (std::get<0>(fd) == FIELD::PARAMETERS) { if (param_ctr < d_parameterdata.size()) { switch (std::get<0>(d_parameterdata[param_ctr])) { case PARAMETER_FIELD::INT: case PARAMETER_FIELD::NULLPARAMETER: { uint64_t value = bytesToUint64(std::get<1>(d_parameterdata[param_ctr]), std::get<2>(d_parameterdata[param_ctr])); datapos += setFieldAndWire(FIELD::PARAMETERS, WIRETYPE::LENGTHDELIM, data + datapos); datapos += putVarInt(varIntSize(value) + 1, data + datapos); datapos += putVarIntType(d_parameterdata[param_ctr], data + datapos); break; } case PARAMETER_FIELD::STRING: case PARAMETER_FIELD::BLOB: { datapos += setFieldAndWire(FIELD::PARAMETERS, WIRETYPE::LENGTHDELIM, data + datapos); // size of string field+wire number of bytes for length of string // v v v v datapos += putVarInt(std::get<2>(d_parameterdata[param_ctr]) + 1 + varIntSize(std::get<2>(d_parameterdata[param_ctr])), data + datapos); datapos += putLengthDelimType(d_parameterdata[param_ctr], data + datapos); break; } case PARAMETER_FIELD::DOUBLE: { datapos += setFieldAndWire(FIELD::PARAMETERS, WIRETYPE::LENGTHDELIM, data + datapos); datapos += putVarInt(8 + 1, data + datapos); datapos += putFixed64Type(d_parameterdata[param_ctr], data + datapos); break; } } } ++param_ctr; } } //std::cout << "2 DID " << param_ctr << " PARAMETERS!" << std::endl; //std::cout << " " << bepaald::bytesToHexString(data, size) << std::endl; return {data, size}; } inline void SqlStatementFrame::setStatementField(std::string const &val) { unsigned char *temp = new unsigned char[val.length()]; std::memcpy(temp, val.c_str(), val.length()); d_framedata.emplace_back(std::make_tuple(static_cast(FIELD::STATEMENT), temp, val.length())); } inline void SqlStatementFrame::addStringParameter(std::string const &val) { //std::cout << "Adding string parameter: " << val << std::endl; unsigned char *temp = new unsigned char[val.length()]; std::memcpy(temp, val.c_str(), val.length()); d_parameterdata.emplace_back(std::make_tuple(PARAMETER_FIELD::STRING, temp, val.length())); d_framedata.emplace_back(std::make_tuple(FIELD::PARAMETERS, nullptr, 0)); } inline void SqlStatementFrame::addBlobParameter(std::pair, size_t> const &val) { unsigned char *temp = new unsigned char[val.second]; std::memcpy(temp, val.first.get(), val.second); d_parameterdata.emplace_back(std::make_tuple(PARAMETER_FIELD::BLOB, temp, val.second)); d_framedata.emplace_back(std::make_tuple(FIELD::PARAMETERS, nullptr, 0)); } inline void SqlStatementFrame::addIntParameter(int64_t val) { val = bepaald::swap_endian(val); unsigned char *temp = new unsigned char[sizeof(val)]; std::memcpy(temp, reinterpret_cast(&val), sizeof(val)); d_parameterdata.emplace_back(std::make_tuple(PARAMETER_FIELD::INT, temp, sizeof(val))); d_framedata.emplace_back(std::make_tuple(FIELD::PARAMETERS, nullptr, 0)); } inline void SqlStatementFrame::addNullParameter() { int64_t num = 1; int64_t val = bepaald::swap_endian(num); unsigned char *temp = new unsigned char[sizeof(val)]; std::memcpy(temp, reinterpret_cast(&val), sizeof(val)); d_parameterdata.emplace_back(std::make_tuple(PARAMETER_FIELD::NULLPARAMETER, temp, sizeof(val))); d_framedata.emplace_back(std::make_tuple(FIELD::PARAMETERS, nullptr, 0)); } inline void SqlStatementFrame::addDoubleParameter(double val) { unsigned char *temp = new unsigned char[sizeof(val)]; std::memcpy(temp, reinterpret_cast(&val), sizeof(val)); d_parameterdata.emplace_back(std::make_tuple(PARAMETER_FIELD::DOUBLE, temp, sizeof(val))); d_framedata.emplace_back(std::make_tuple(FIELD::PARAMETERS, nullptr, 0)); } inline void SqlStatementFrame::addParameterField(PARAMETER_FIELD field, std::string const &val) { switch (field) { case PARAMETER_FIELD::INT: case PARAMETER_FIELD::NULLPARAMETER: { uint64_t num = std::stoull(val); num = bepaald::swap_endian(num); unsigned char *temp = new unsigned char[sizeof(num)]; std::memcpy(temp, reinterpret_cast(&num), sizeof(num)); d_parameterdata.emplace_back(std::make_tuple(field, temp, sizeof(num))); d_framedata.emplace_back(std::make_tuple(FIELD::PARAMETERS, nullptr, 0)); break; } case PARAMETER_FIELD::STRING: case PARAMETER_FIELD::BLOB: { unsigned char *temp = new unsigned char[val.length()]; std::memcpy(temp, val.c_str(), val.length()); d_parameterdata.emplace_back(std::make_tuple(field, temp, val.length())); d_framedata.emplace_back(std::make_tuple(FIELD::PARAMETERS, nullptr, 0)); break; } case PARAMETER_FIELD::DOUBLE: { double num = std::stod(val); unsigned char *temp = new unsigned char[sizeof(num)]; std::memcpy(temp, reinterpret_cast(&num), sizeof(num)); d_parameterdata.emplace_back(std::make_tuple(field, temp, sizeof(num))); d_framedata.emplace_back(std::make_tuple(FIELD::PARAMETERS, nullptr, 0)); break; } } } // inline void SqlStatementFrame::setParameter(unsigned int idx, unsigned char *data, uint32_t length) // { // if (std::get<1>(d_parameterdata[idx])) // { // delete[] std::get<1>(d_parameterdata[idx]); // std::get<1>(d_parameterdata[idx]) = new unsigned char[length]; // std::memcpy(std::get<1>(d_parameterdata[idx]), data, length); // std::get<2>(d_parameterdata[idx]) = length; // } // } // inline void SqlStatementFrame::getParameter(unsigned int idx) const // { // if (std::get<1>(d_parameterdata[idx])) // { // std::cout << bepaald::bytesToString(std::get<1>(d_parameterdata[idx]), std::get<2>(d_parameterdata[idx])) << std::endl; // } // else // std::cout << "nullptr" << std::endl; // } // inline std::string SqlStatementFrame::getParameterAsString(unsigned int idx) const // { // if (std::get<1>(d_parameterdata[idx])) // return bepaald::bytesToString(std::get<1>(d_parameterdata[idx]), std::get<2>(d_parameterdata[idx])); // return ""; // } // inline uint64_t SqlStatementFrame::getParameterAsUint64(unsigned int idx) const // { // if (std::get<1>(d_parameterdata[idx])) // return bytesToUint64(std::get<1>(d_parameterdata[idx]), std::get<2>(d_parameterdata[idx])); // return 0; // } inline std::string SqlStatementFrame::bindStatement() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::STATEMENT) return bepaald::bytesToString(std::get<1>(p), std::get<2>(p)); return std::string(); } inline std::vector SqlStatementFrame::parameters() const { std::vector parameters; for (auto const &p : d_parameterdata) { // this switch is ordered by occurrence switch (std::get<0>(p)) { case PARAMETER_FIELD::INT: { parameters.emplace_back(static_cast(bytesToUint64(std::get<1>(p), std::get<2>(p)))); break; } case PARAMETER_FIELD::NULLPARAMETER: { //parameters.emplace_back(static_cast(bytesToUint64(std::get<1>(p), std::get<2>(p)))); parameters.emplace_back(nullptr); break; } case PARAMETER_FIELD::STRING: { //if () //std::cout << "Returning string parameter: " << bepaald::bytesToString(std::get<1>(p), std::get<2>(p)) << std::endl; parameters.emplace_back(bepaald::bytesToString(std::get<1>(p), std::get<2>(p))); /* std::string rep = bepaald::bytesToString(std::get<1>(p), std::get<2>(p)); std::string::size_type pos2 = 0; while ((pos2 = rep.find('\'', pos2)) != std::string::npos) { rep.replace(pos2, 1, "''"); pos2 += 2; } rep = '\'' + rep + '\''; d_statement.replace(pos, 1, rep); pos += rep.length(); */ break; } case PARAMETER_FIELD::BLOB: { std::pair, size_t> data{new unsigned char[std::get<2>(p)], std::get<2>(p)}; std::memcpy(data.first.get(), std::get<1>(p), std::get<2>(p)); parameters.emplace_back(std::move(data)); break; } case PARAMETER_FIELD::DOUBLE: { parameters.emplace_back(*reinterpret_cast(std::get<1>(p))); break; } } } return parameters; } inline bool SqlStatementFrame::validate(uint64_t) const { if (d_framedata.empty()) return false; bool foundstatement = false; for (auto const &p : d_framedata) { if (std::get<0>(p) != FIELD::STATEMENT && std::get<0>(p) != FIELD::PARAMETERS) return false; if (std::get<0>(p) == FIELD::STATEMENT) foundstatement = true; } for (auto const &pd : d_parameterdata) { if (std::get<0>(pd) != PARAMETER_FIELD::STRING && std::get<0>(pd) != PARAMETER_FIELD::INT && std::get<0>(pd) != PARAMETER_FIELD::DOUBLE && std::get<0>(pd) != PARAMETER_FIELD::BLOB && std::get<0>(pd) != PARAMETER_FIELD::NULLPARAMETER) return false; } return foundstatement; } #endif signalbackup-tools-20250313-1/sqlstatementframe/sqlstatementframe.ih000066400000000000000000000014371476450434500255530ustar00rootroot00000000000000/* Copyright (C) 2019-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlstatementframe.h" #include signalbackup-tools-20250313-1/sqlstatementframe/statics.cc000066400000000000000000000015741476450434500234550ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "sqlstatementframe.ih" SqlStatementFrame::Registrar SqlStatementFrame::s_registrar(FRAMETYPE::SQLSTATEMENT, SqlStatementFrame::create); signalbackup-tools-20250313-1/stickerframe/000077500000000000000000000000001476450434500204055ustar00rootroot00000000000000signalbackup-tools-20250313-1/stickerframe/statics.cc000066400000000000000000000015431476450434500223710ustar00rootroot00000000000000/* Copyright (C) 2019-2024 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "stickerframe.ih" StickerFrame::Registrar StickerFrame::s_registrar(FRAMETYPE::STICKER, StickerFrame::create); signalbackup-tools-20250313-1/stickerframe/stickerframe.h000066400000000000000000000204751476450434500232450ustar00rootroot00000000000000/* Copyright (C) 2019-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #ifndef STICKERFRAME_H_ #define STICKERFRAME_H_ #include #include #include "../framewithattachment/framewithattachment.h" #include "../attachmentmetadata/attachmentmetadata.h" #include "../common_be.h" #include "../common_bytes.h" class StickerFrame : public FrameWithAttachment { enum FIELD { INVALID = 0, ROWID = 1, // uint64 LENGTH = 2, // uint32 }; static Registrar s_registrar; std::optional d_mimetype; public: inline explicit StickerFrame(uint64_t count = 0); inline StickerFrame(unsigned char const *data, size_t length, uint64_t count = 0); inline StickerFrame(StickerFrame &&other) = default; inline StickerFrame &operator=(StickerFrame &&other) = default; inline StickerFrame(StickerFrame const &other) = default; inline StickerFrame &operator=(StickerFrame const &other) = default; inline virtual ~StickerFrame() override = default; inline virtual StickerFrame *clone() const override; inline virtual StickerFrame *move_clone() override; inline virtual FRAMETYPE frameType() const override; inline static BackupFrame *create(unsigned char const *data, size_t length, uint64_t count = 0); inline virtual void printInfo() const override; inline virtual uint32_t attachmentSize() const override; inline uint32_t length() const; inline uint64_t rowId() const; inline void setRowId(uint64_t rid); inline virtual std::pair getData() const override; inline virtual bool validate(uint64_t available) const override; inline std::string getHumanData() const override; inline unsigned int getField(std::string_view const &str) const; inline std::optional mimetype() const; inline unsigned char *attachmentData(bool *badmac = nullptr, bool verbose = false); private: inline uint64_t dataSize() const override; }; inline StickerFrame::StickerFrame(uint64_t count) : FrameWithAttachment(count) {} inline StickerFrame::StickerFrame(unsigned char const *data, size_t length, uint64_t count) : FrameWithAttachment(data, length, count) {} inline StickerFrame *StickerFrame::clone() const { return new StickerFrame(*this); } inline StickerFrame *StickerFrame::move_clone() { return new StickerFrame(std::move(*this)); } inline BackupFrame::FRAMETYPE StickerFrame::frameType() const // virtual override { return FRAMETYPE::STICKER; } inline BackupFrame *StickerFrame::create(unsigned char const *data, size_t length, uint64_t count) // static { return new StickerFrame(data, length, count); } inline void StickerFrame::printInfo() const // virtual override { Logger::message("Frame number: ", d_count); Logger::message(" Size: ", d_constructedsize); Logger::message(" Type: STICKER"); for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::ROWID) Logger::message(" - row id : ", bytesToUint64(std::get<1>(p), std::get<2>(p)), " (", std::get<2>(p), " bytes)"); else if (std::get<0>(p) == FIELD::LENGTH) Logger::message(" - length : ", bytesToUint32(std::get<1>(p), std::get<2>(p)), " (", std::get<2>(p), " bytes)"); } } inline uint32_t StickerFrame::length() const { if (!d_attachmentdata_size) for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::LENGTH) return bytesToUint32(std::get<1>(p), std::get<2>(p)); return d_attachmentdata_size; } inline uint32_t StickerFrame::attachmentSize() const // virtual override { return length(); } inline uint64_t StickerFrame::rowId() const { for (auto const &p : d_framedata) if (std::get<0>(p) == FIELD::ROWID) return bytesToUint64(std::get<1>(p), std::get<2>(p)); return 0; } inline void StickerFrame::setRowId(uint64_t rid) { for (auto &p : d_framedata) if (std::get<0>(p) == FIELD::ROWID) { if (sizeof(rid) != std::get<2>(p)) [[unlikely]] { //std::cout << " ************ DAMN! ********** " << std::endl; return; } uint64_t val = bepaald::swap_endian(rid); std::memcpy(std::get<1>(p), reinterpret_cast(&val), sizeof(val)); return; } } inline uint64_t StickerFrame::dataSize() const { uint64_t size = 0; for (auto const &fd : d_framedata) { switch (std::get<0>(fd)) { case FIELD::ROWID: { uint64_t value = bytesToUint64(std::get<1>(fd), std::get<2>(fd)); size += varIntSize(value); size += 1; // +1 for fieldtype + wiretype break; } case FIELD::LENGTH: { uint32_t value = bytesToUint32(std::get<1>(fd), std::get<2>(fd)); size += varIntSize(value); size += 1; // for fieldtype + wiretype break; } } } // for size of this entire frame. size += varIntSize(size); return ++size; // for frametype and wiretype } inline std::pair StickerFrame::getData() const { uint64_t size = dataSize(); unsigned char *data = new unsigned char[size]; uint64_t datapos = 0; datapos += setFieldAndWire(FRAMETYPE::STICKER, WIRETYPE::LENGTHDELIM, data + datapos); datapos += setFrameSize(size, data + datapos); for (auto const &fd : d_framedata) { switch (std::get<0>(fd)) { case FIELD::ROWID: datapos += putVarIntType(fd, data + datapos); break; case FIELD::LENGTH: datapos += putVarIntType(fd, data + datapos); break; } } return {data, size}; } inline bool StickerFrame::validate(uint64_t available) const { if (d_framedata.empty()) return false; int foundrowid = 0; int rowid_fieldsize = 0; int foundlength = 0; int length_fieldsize = 0; unsigned int length = 0; for (auto const &p : d_framedata) { if (std::get<0>(p) != FIELD::ROWID && std::get<0>(p) != FIELD::LENGTH) return false; if (std::get<0>(p) == FIELD::ROWID) { ++foundrowid; rowid_fieldsize += std::get<2>(p); } else if (std::get<0>(p) == FIELD::LENGTH) { ++foundlength; length += bytesToUint32(std::get<1>(p), std::get<2>(p)); length_fieldsize += std::get<2>(p); } } return foundlength == 1 && foundrowid == 1 && length_fieldsize <= 8 && rowid_fieldsize <= 8 && length <= available && length < 1 * 1024 * 1024; // If size is more than 1MB, it's not right... From // https://support.signal.org/hc/en-us/articles/360031836512-Stickers : // "Each sticker has a size limit of 300kb" } inline std::string StickerFrame::getHumanData() const { std::string data; for (auto const &p : d_framedata) { if (std::get<0>(p) == FIELD::ROWID) data += "ROWID:uint64:" + bepaald::toString(bytesToUint64(std::get<1>(p), std::get<2>(p))) + "\n"; else if (std::get<0>(p) == FIELD::LENGTH) data += "LENGTH:uint32:" + bepaald::toString(bytesToUint32(std::get<1>(p), std::get<2>(p))) + "\n"; } return data; } inline unsigned int StickerFrame::getField(std::string_view const &str) const { if (str == "ROWID") return FIELD::ROWID; if (str == "LENGTH") return FIELD::LENGTH; return FIELD::INVALID; } inline std::optional StickerFrame::mimetype() const { return d_mimetype; } inline unsigned char *StickerFrame::attachmentData(bool *badmac, bool verbose) { unsigned char *data = FrameWithAttachment::attachmentData(badmac, verbose); if (data && !d_mimetype) // try to get mimetype { AttachmentMetadata amd = AttachmentMetadata::getAttachmentMetaData(std::string(), data, d_attachmentdata_size, true/*skiphash*/); if (!amd.filetype.empty()) d_mimetype = amd.filetype; } return data; } #endif signalbackup-tools-20250313-1/stickerframe/stickerframe.ih000066400000000000000000000014041476450434500234050ustar00rootroot00000000000000/* Copyright (C) 2019-2023 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "stickerframe.h" signalbackup-tools-20250313-1/xmldocument/000077500000000000000000000000001476450434500202655ustar00rootroot00000000000000signalbackup-tools-20250313-1/xmldocument/xmldocument.cc000066400000000000000000000657161476450434500231520ustar00rootroot00000000000000/* Copyright (C) 2024-2025 Selwin van Dijk This file is part of signalbackup-tools. signalbackup-tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. signalbackup-tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with signalbackup-tools. If not, see . */ #include "xmldocument.h" #include #include #include #define CASE_NUMBER case'0':case'1':case'2':case'3':case'4':case'5':case'6':case'7':case'8':case'9' #define CASE_ALPHA_LOWER case'a':case'b':case'c':case'd':case'e':case'f':case'g':case'h':case'i':case'j':case'k':case'l':case'm':case'n':case'o':case'p':case'q':case'r':case's':case't':case'u':case'v':case'w':case'x':case'y':case'z' #define CASE_ALPHA_UPPER case'A':case'B':case'C':case'D':case'E':case'F':case'G':case'H':case'I':case'J':case'K':case'L':case'M':case'N':case'O':case'P':case'Q':case'R':case'S':case'T':case'U':case'V':case'W':case'X':case'Y':case'Z' #define CASE_ALPHA CASE_ALPHA_UPPER:CASE_ALPHA_LOWER #define CASE_ALPHANUM CASE_ALPHA:CASE_NUMBER XmlDocument::XmlDocument(std::string const &filename) : d_currentnode(&d_rootnode), d_ok(false) { std::ifstream file(filename, std::ios_base::in | std::ios_base::binary); if (!file.is_open()) [[unlikely]] { Logger::error("Failed to open XML file for reading: '", filename, "'"); return; } unsigned int constexpr buffer_size = 16 * 1024; //std::unique_ptr buffer(new char[buffer_size]); char buffer[buffer_size]; unsigned long long int filepos; // save file position of (very) large values unsigned int available; State state = INITIAL; State state_before_comment = INITIAL; std::string attribute_name_tmp; std::string attribute_value_tmp; long long int attribute_pos = -1; long long int attribute_size = 0; std::string closing_tag_tmp; bool has_root_element = false; while (true) { filepos = file.tellg(); file.read(buffer, buffer_size); available = file.gcount(); for (unsigned int i = 0; i < available; ++i) { switch (state) // order determined for SignalPlaintextBackup.xml { case ATTRIBUTE_VALUE_DOUBLE: // v { // ' '&'(except in entity)) char *closing_quote = static_cast(std::memchr(buffer + i, '"', available - i)); if (closing_quote != nullptr) { attribute_size = attribute_size + (closing_quote - (buffer + i)); attribute_value_tmp.append(buffer + i, attribute_size < Node::s_maxsize ? attribute_size - attribute_value_tmp.size() : 0); // if (attribute_name_tmp == "data") // { // std::cout << " - attribute: '" << attribute_name_tmp << "'='" << attribute_value_tmp << "'" << std::endl; // std::cout << "filepos : " << filepos << std::endl; // //std::cout << "i : " << newbufferpos << std::endl; // std::cout << "att_pos : " << attribute_pos << std::endl; // std::cout << "att_size: " << attribute_size << std::endl; // std::cout << "attribute value ends at pos: " << (filepos + i + (closing_quote - (buffer.get() + i))) << " SIZE: " << attribute_size << std::endl; // } Node::StringOrRef attributevalue = { (attribute_size < Node::s_maxsize) ? std::move(attribute_value_tmp) : std::string(), (attribute_size < Node::s_maxsize) ? std::string() : filename, (attribute_size < Node::s_maxsize) ? -1 : attribute_pos, attribute_size }; // if (attribute_name_tmp == "data") // { // std::cout << attributevalue.file << std::endl; // std::cout << attributevalue.value << std::endl; // std::cout << attributevalue.size << std::endl; // std::cout << attributevalue.pos << std::endl; // } d_currentnode->d_attributes[attribute_name_tmp] = std::move(attributevalue); attribute_value_tmp.clear(); attribute_name_tmp.clear(); attribute_pos = -1; i += (closing_quote - (buffer + i)); //std::cout << "State: ATTRIBUTE_VALUE_DOUBLE -> ELEMENT_AFTER_TAGNAME" << std::endl; state = ELEMENT_AFTER_TAGNAME; } else { attribute_size += available - i; attribute_value_tmp.append(buffer + i, attribute_value_tmp.size() < Node::s_maxsize ? available - i : 0); i = available; } /**** END NEW METHOD ****/ /**** OLD METHOD switch (buffer[i]) { [[unlikely]] case '"': { attribute_size = (filepos + i) - attribute_pos; // std::cout << " - attribute: '" << attribute_name_tmp << "'='" << attribute_value_tmp << "'" << std::endl; // std::cout << "filepos : " << filepos << std::endl; // std::cout << "i : " << i << std::endl; // std::cout << "att_pos : " << attribute_pos << std::endl; // std::cout << "att_size: " << attribute_size << std::endl; // std::cout << "attribute value ends at pos: " << (filepos + i) << " SIZE: " << attribute_size << std::endl; // { // if (attribute_size > 0) // { // std::ifstream tmp(filename, std::ios_base::in | std::ios_base::binary); // tmp.seekg(attribute_pos); // std::unique_ptr v(new char[attribute_size]); // tmp.read(v.get(), attribute_size); // std::cout << " *** " << std::string(v.get(), attribute_size) << std::endl; // } // } Node::StringOrRef attributevalue = { (attribute_size < Node::s_maxsize) ? std::move(attribute_value_tmp) : std::string(), (attribute_size < Node::s_maxsize) ? std::string() : filename, (attribute_size < Node::s_maxsize) ? -1 : attribute_pos, attribute_size }; d_currentnode->d_attributes[attribute_name_tmp] = std::move(attributevalue); attribute_value_tmp.clear(); attribute_name_tmp.clear(); attribute_pos = -1; //std::cout << "State: ATTRIBUTE_VALUE_DOUBLE -> ELEMENT_AFTER_TAGNAME" << std::endl; state = ELEMENT_AFTER_TAGNAME; break; } [[unlikely]] case '<': // ampersand is also not allowed as representing itself, but is used for entities (like &) [[unlikely]] case '>': { Logger::error("Illegal character in attribute value: '", buffer[i], "'"); return; } default: { attribute_value_tmp += buffer[i]; break; } } END OLD METHOD ****/ break; } case ATTRIBUTE_NAME: // v { // ELEMENT_SELFCLOSING_END " << std::endl; state = ELEMENT_SELFCLOSING_END; break; } case '>': // tag closed, but node open... { //std::cout << "State: ELEMENT_AFTER_TAGNAME -> ELEMENT_VALUE" << std::endl; state = ELEMENT_VALUE; break; } CASE_ALPHA: case '_': { //std::cout << "State: ELEMENT_AFTER_TAG_NAME -> ATTRIBUTE_NAME" << std::endl; state = ATTRIBUTE_NAME; attribute_name_tmp += buffer[i]; break; } case ' ': //ignore more spaces.. case '\n': case '\t': case '\r': { break; } [[unlikely]] default: { Logger::error("Illegal character in ELEMENT_AFTER_TAGNAME? ('", buffer[i], "')"); return; } } break; } case ATTRIBUTE_WAIT_QUOTE: // v { // ATTRIBUTE_VALUE_DOUBLE " << std::endl; state = ATTRIBUTE_VALUE_DOUBLE; break; } case '\'': { //std::cout << "State: ATTRIBUTE_WAIT_QUOTE -> ATTRIBUTE_VALUE_SINGLE" << std::endl; state = ATTRIBUTE_VALUE_SINGLE; break; } case ' ': // ignoring whitespace around '=' in attributename="attributevalue" case '\n': case '\t': case '\r': { break; } [[unlikely]] default: { Logger::error("Illegal character waiting for attribute value (must be quotation mark): '", buffer[i], "'"); return; } } break; } case ELEMENT_TAG: // v { // d_name += buffer[i]; break; } case ' ': case '\n': case '\t': case '\r': { //std::cout << "State: ELEMENT_TAG -> ELEMENT_AFTER_TAGNAME (" << d_currentnode->d_name << ")" << std::endl; state = ELEMENT_AFTER_TAGNAME; break; } case '>': // tag closed, but node open... { state = ELEMENT_VALUE; break; } [[unlikely]] default: { Logger::error("Illegal character in ELEMENT_TAG ('", buffer[i], "')"); return; } } break; } case ELEMENT_VALUE: // v { // ... switch (buffer[i]) { case '<': { //std::cout << "State: ELEMENT_VALUE -> PROLOG_ELEMENT_DTD_COMMENT" << std::endl; state = PROLOG_ELEMENT_DTD_COMMENT; // technically, prolog and dtd can not happen at this point... state_before_comment = ELEMENT_VALUE; if (d_currentnode->is_text_node && !d_currentnode->is_closed) { d_currentnode->is_closed = true; if (d_currentnode->d_value.find_first_not_of(" \n\t\r") == std::string::npos) { d_currentnode = d_currentnode->d_parent; d_currentnode->d_children.pop_back(); // delete the empty text node } else d_currentnode = d_currentnode->d_parent; } break; } //case 'something'/// illegal characters... default: { //std::cout << "Got char in element value: '" << buffer[i] << "'" << std::endl; //std::cout << "Current node exists: " << (d_currentnode ? "yes" : "no") << std::endl; if (!d_currentnode->is_text_node || d_currentnode->is_closed) { d_currentnode->d_children.emplace_back(Node(d_currentnode)); d_currentnode = &d_currentnode->d_children.back(); d_currentnode->is_text_node = true; } d_currentnode->d_value += buffer[i]; } break; } break; } case ELEMENT_SELFCLOSING_END: // v { // ': { // NODE CLOSED! //std::cout << " - Got node (closed!): '" << d_currentnode->d_name << "'" << std::endl; d_currentnode->is_closed = true; // I dont think the below is possible on a self closing tag, it cant have ever entered ELEMENT_VALUE //if (d_currentnode->d_value.find_first_not_of(" \n\t\r") == std::string::npos) // d_currentnode->d_value.clear(); d_currentnode = d_currentnode->d_parent; //std::cout << "State: ELEMENT_SELFCLOSING_END -> INITIAL" << std::endl; state = ELEMENT_VALUE; // not sure about this... break; } [[unlikely]] default: { Logger::error("Illegal character sequence in ELEMENT_SELFCLOSING_END? ('/", buffer[i], "')"); return; } } break; } case PROLOG_ELEMENT_DTD_COMMENT: // v { // <.... switch (buffer[i]) { CASE_ALPHA: case '_': { //std::cout << "State: PROLOG_ELEMENT_DTD_COMMENT -> ELEMENT_TAG" << std::endl; state = ELEMENT_TAG; if (has_root_element) { if (d_currentnode->is_closed) { d_currentnode->d_parent->d_children.emplace_back(Node(d_currentnode)); d_currentnode = &d_currentnode->d_parent->d_children.back(); } else { d_currentnode->d_children.emplace_back(Node(d_currentnode)); d_currentnode = &d_currentnode->d_children.back(); } } else { has_root_element = true; } d_currentnode->d_name += buffer[i]; break; } case '?': { //std::cout << "State: PROLOG_ELEMENT_DTD_COMMENT -> PROLOG" << std::endl; state = PROLOG; break; } case '!': { //std::cout << "State: PROLOG_ELEMENT_DTD_COMMENT -> DTD_COMMENT" << std::endl; state = DTD_COMMENT; break; } case '/': { //std::cout << "State: PROLOG_ELEMENT_DTD_COMMENT -> CLOSING TAG" << std::endl; state = ELEMENT_CLOSING_TAG_START; break; } [[unlikely]] default: { Logger::error("Illegal char in PROLOG_ELEMENT_DTD_COMMENT state: '", buffer[i], "'"); return; } } break; } // in the initial case, we expect only PROLOG, DTD, or ELEMENT (the root element) case INITIAL: { switch (buffer[i]) { case '<': { state = PROLOG_ELEMENT_DTD_COMMENT; state_before_comment = INITIAL; //std::cout << "State: INITIAL -> PROLOG_ELEMENT_DTD_COMMENT" << std::endl; break; } case ' ': case '\n': case '\t': case '\r': { break; } [[unlikely]] default: { Logger::error("Illegal char in INITIAL state: '", buffer[i], "'"); return; } } break; } case ELEMENT_CLOSING_TAG_START: // v { // ': { // check name, close node! if (closing_tag_tmp != d_currentnode->d_name) { Logger::error("Non matching closing tag (open: '", d_currentnode->d_name, "' close: '", closing_tag_tmp, "')"); return; } //std::cout << " - Got node (closed!): '" << d_currentnode->d_name << "'" << std::endl; d_currentnode->is_closed = true; if (d_currentnode->is_text_node && d_currentnode->d_value.find_first_not_of(" \n\t\r") == std::string::npos) { d_currentnode = d_currentnode->d_parent; d_currentnode->d_children.pop_back(); // delete the empty text node } else d_currentnode = d_currentnode->d_parent; if (d_currentnode == nullptr) [[unlikely]] // we just closed the root node, we should be done state = FINISHED; // no data can follow, but maybe a newline (or similar) else state = ELEMENT_VALUE; // prolog and dtd are impossible i think break; } [[unlikely]] default: { Logger::error("Illegal character in ELEMENT_CLOSING_TAG: '", buffer[i], "'"); return; } } break; } case ATTRIBUTE_VALUE_SINGLE: // v { // d_attributes[attribute_name_tmp] = std::move(attributevalue); attribute_value_tmp.clear(); attribute_name_tmp.clear(); attribute_pos = -1; i += (closing_quote - (buffer + i)); state = ELEMENT_AFTER_TAGNAME; } else { attribute_size += available - i; attribute_value_tmp.append(buffer + i, attribute_value_tmp.size() < Node::s_maxsize ? available - i : 0); i = available; } /**** END NEW METHOD ****/ break; } case PROLOG: // in the prolog, we only wait for '?>' to close it { switch (buffer[i]) { case '?': { //std::cout << "State: PROLOG -> PROLOG_READ_QUESTIONMARK" << std::endl; state = PROLOG_READ_QUESTIONMARK; break; } default: { d_prolog += buffer[i]; break; } } break; } case PROLOG_READ_QUESTIONMARK: { switch (buffer[i]) { case '>': // the prolog has finished { //std::cout << " - prolog: '" << d_prolog << "'" << std::endl; //std::cout << "State: PROLOG_READ_QUESTIONMARK -> INITIAL" << std::endl; state = INITIAL; break; } default: // the '?' did not signal the end of the prolog { d_prolog += '?'; d_prolog += buffer[i]; //std::cout << "State: PROLOG_READ_QUESTIONMARK -> PROLOG" << std::endl; state = PROLOG; } } break; } case DTD_COMMENT: // v { // COMMENT_FIRST_OPEN_HYPHEN" << std::endl; state = COMMENT_FIRST_OPEN_HYPHEN; break; } case 'D': // Lets assume 'DOCTYPE' { //std::cout << "State: DTD_COMMENT -> DTD" << std::endl; state = DTD; break; } } break; } case COMMENT_FIRST_OPEN_HYPHEN: // v { // COMMENT" << std::endl; state = COMMENT; break; } else { Logger::error("Illegal character sequence: ' COMMENT_FIRST_CLOSE_HYPHEN" << std::endl; state = COMMENT_FIRST_CLOSE_HYPHEN; break; } default: break; } break; } case COMMENT_FIRST_CLOSE_HYPHEN: // v { //