commit eabc6e8ade354f2469bcef6311964ffdb34e2531 Author: Jaidyn Levesque Date: Fri Sep 30 09:58:54 2022 -0500 Init Pretty much everything's in order, other than: * File-transfers archiving * Video/voice call listing diff --git a/dino-chat-export.sh b/dino-chat-export.sh new file mode 100755 index 0000000..8f4a490 --- /dev/null +++ b/dino-chat-export.sh @@ -0,0 +1,348 @@ +#!/bin/sh +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― +# Name: dino-chat-exporter +# Desc: Export all conversations from Dino (XMPP client)'s database into +# textual format +# Reqs: shell, sqlite3 +# Date: 2022-10 +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― + + +sqlite() { + sqlite3 "$1" "$2" + if test "$?" -ne 0; then + >&2 printf "sqlite errored out! Let's try again in a moment…" + sleep 1 + + sqlite3 "$1" "$2" + if test "$?" -ne 0; then + >&2 printf "\t… well that didn't work. Oh, well.\n" + else + >&2 printf "\t… hey, that worked!\n" + fi + fi +} + + +# A list of all accounts, by internal ID +account_list() { + sqlite "$DB_FILE" \ + "SELECT id + FROM account;" +} + + +# A list of all counterpart/contact IDs for messages +conversation_partners() { + local account_id="$1" + + sqlite "$DB_FILE" \ + "SELECT DISTINCT counterpart_id + FROM message + WHERE account_id == $account_id;" +} + + +# Outputs valid file extension for given file +file_extension() { + local file="$1" + + # For some reason, `file` doesn't choose a file extension for HTML nor plaintext files? + if file --brief --mime "$file" | grep "text/html" > /dev/null; then + echo "html" + else + file --brief --extension "$file" \ + | cut --delimiter='/' --fields=1 \ + | sed 's%^???$%txt%' + fi +} + + +# Output the account no.'s jid_id (aka, accounts.id→jid.id) +# (We cache this in a global variable, so we're not making a million database queries) +account_jid_id() { + local account_id="$1" + + if test -z "$YOUR_JID_ID"; then + YOUR_JID_ID="$(sqlite "$DB_FILE" \ + "SELECT jid.id + FROM account, jid + WHERE account.id == $account_id + AND account.bare_jid == jid.bare_jid;")" + fi + echo "$YOUR_JID_ID" +} + + +# Output the account no.'s xmpp address and nick +# (We cache this in a global variable, so we're not making a million database queries) +account_jid_and_nick() { + local account_id="$1" + + if test -z "$YOUR_INFO"; then + YOUR_INFO="$(sqlite "$DB_FILE" \ + "SELECT FORMAT('%s' || char(10) || '%s', + bare_jid, + alias) + FROM account + WHERE id == $account_id;")" + fi + echo "$YOUR_INFO" +} + + +# Get a user's (based on jid.id) xmpp address and roster nickname +# (We cache this in a global variable, so we're not making a million database queries) +id_jid_and_nick() { + local internal_id="$1" + + if test -z "$THEIR_INFO"; then + local nick="$(sqlite "$DB_FILE" \ + "SELECT + CASE + WHEN roster.name IS NOT NULL + THEN roster.name + END + FROM roster, jid + WHERE roster.jid == jid.bare_jid AND jid.id == $internal_id;")" + + local jid="$(sqlite "$DB_FILE" \ + "SELECT bare_jid + FROM jid + WHERE jid.id == $internal_id;")" + + if test -z "$nick"; then + THEIR_INFO="$(printf '%s\n%s\n' "$jid" "$jid")" + else + THEIR_INFO="$(printf '%s\n%s\n' "$jid" "$nick")" + fi + fi + echo "$THEIR_INFO" +} + + +# Archives a full conversation with user (messages and files) +archive_conversation_with_partner() { + local account_id="$1" + local partner_id="$2" + local output_dir="$3" + + mkdir -p "$output_dir" + if test ! -d "$output_dir"; then + echo "$output_dir isn't a valid directory" + exit 2 + fi + + archive_files_with_partner "$account_id" "$partner_id" "$output_dir/files" + archive_messages_with_partner "$account_id" "$partner_id" "$output_dir/messages" +} + + +# Archives all messages between you and partner, according to a stem +archive_messages_with_partner() { + local account_id="$1" + local partner_id="$2" + local output_stem="$3" + + output_messages_with_partner "$account_id" "$partner_id" \ + > "$output_stem" + mv "$output_stem" "$output_stem.$(file_extension "$output_stem")" +} + + +# Archives all (currently known/downloaded) files and avatars between you and partner +archive_files_with_partner() { + local account_id="$1" + local partner_id="$2" + local output_dir="$3" + local IFS=" +" + mkdir -p "$output_dir" + if test ! -d "$output_dir"; then + echo "$output_dir isn't a valid directory" + return + fi + + THEIR_AVATAR="$(archive_avatars "$account_id" "$partner_id" "$output_dir/avatar" | head -1)" + YOUR_AVATAR="$(archive_avatars "$account_id" "$(account_jid_id "$account_id")" "$output_dir/your_avatar" | head -1)" +} + + +# Archive the avatars of a user, according to a stem +# ("./files/avatar" becomes "./files/avatar.png", "./files/avatar1.png"…) +archive_avatars() { + local account_id="$1" + local internal_id="$2" + local output_stem="$3" + + local i="" + for file in $(avatar_paths "$account_id" "$internal_id"); do + local output_path="$output_stem${i}.$(file_extension "$file")" + echo "$output_path" + + cp "$file" "$output_stem${i}.$(file_extension "$file")" + done +} + + +# For flexibility in formatting, we let the user define the selection order in a simplified manner +message_slots_to_selection() { + local slots="$1" + + local jid_query_part="CASE message.direction + WHEN 0 + THEN jid.bare_jid + ELSE ( select account.bare_jid from account where account.id == message.account_id ) + END" + + local avatar_query_part="CASE message.direction + WHEN 0 + THEN 'files/$(basename "$THEIR_AVATAR")' + ELSE 'files/$(basename "$YOUR_AVATAR")' + END" + + echo "$slots" \ + | sed "s^DATE^DATETIME(message.local_time, 'unixepoch', 'localtime')^g" \ + | sed "s^JID^$(echo "$jid_query_part" | tr '\n' ' ' | tr -d '\t')^g" \ + | sed "s^AVATAR^$(echo "$avatar_query_part" | tr '\n' ' ' | tr -d '\t')^g" \ + | sed 's^BODY^message.body^g' +} + + +# Prints a header/footer for message output, replacing useful variables +output_message_cap() { + printf "$MESSAGE_HEADER" \ + | sed 's%YOUR_JID%'"$(account_jid_and_nick "$account_id" | head -1)"'%g' \ + | sed 's%YOUR_NICK%'"$(account_jid_and_nick "$account_id" | tail -1)"'%g' \ + | sed 's%THEIR_JID%'"$(id_jid_and_nick "$partner_id" | head -1)"'%g' \ + | sed 's%THEIR_NICK%'"$(id_jid_and_nick "$partner_id" | tail -1)"'%g' +} + + +# Outputs all conversation's text with partner, as per $MESSAGE_FORMAT +output_messages_with_partner() { + local account_id="$1" + local partner_id="$2" + local output_dir="$3" # optional, only used to guess avatar paths + + output_message_cap "$account_id" "$partner_id" "$MESSAGE_HEADER" "$output_dir" + + sqlite "$DB_FILE" \ + "SELECT FORMAT('$MESSAGE_FORMAT', + $(message_slots_to_selection "$MESSAGE_SLOTS")) + FROM jid,message + WHERE message.account_id == '$account_id' + AND message.counterpart_id == $partner_id + AND jid.id == $partner_id + ORDER BY message.local_time ASC;" + + output_message_cap "$account_id" "$partner_id" "$MESSAGE_FOOTER" "$output_dir" +} + + +# Outputs existant avatar paths for the given user, by internal ID +avatar_paths() { + local account_id="$1" + local internal_id="$2" + local IFS=" +" + for file in $(potential_avatar_paths "$account_id" "$internal_id" | uniq); do + if test -e "$file"; then + echo "$file" + fi + done +} + + +# Outputs potential paths for a user's avatar, by internal ID +potential_avatar_paths() { + local account_id="$1" + local internal_id="$2" + + sqlite "$DB_FILE" \ + "SELECT '$DINO_HOME/avatars/' || hash + FROM contact_avatar + WHERE jid_id == '$internal_id' + AND account_id == '$account_id';" +} + + + +# USER ENVIRONMENT +# ——————————————————————————————————————————————————————————————————————————————— +# Where Dino's data lives +if test -z "$DINO_HOME"; then + DINO_HOME="$XDG_DATA_HOME/dino/" +fi +if test ! -e "$DINO_HOME"; then + DINO_HOME="$HOME/.local/share/dino/" +fi + +DB_FILE="$XDG_DATA_HOME/dino/dino.db" + +# The format for message output, with %s being substitued with it's corresponding +# place in $MESSAGE_SLOTS +if test -z "$MESSAGE_FORMAT"; then + MESSAGE_FORMAT="%s <%s> %s" +fi +MESSAGE_FORMAT='

%s %s %s

' + +# The slots used in $MESSAGE_FORMAT. +# May be DATE, JID, BODY, or AVATAR. Must be comma-delimited. +if test -z "$MESSAGE_SLOTS"; then + MESSAGE_SLOTS="DATE, JID, BODY" +fi +MESSAGE_SLOTS="AVATAR, DATE, JID, BODY" + +# A header printed before messages are output (could be "", for example) +MESSAGE_HEADER='\n\n\nConversation with THEIR_JID - YOUR_JID\n
'
+
+# A footer printed after messages are output (could be "", for example)
+MESSAGE_FOOTER=''
+
+
+
+# STATE
+# ———————————————————————————————————————————————————————————————————————————————
+# How repulsive… very sorry about this =w="
+THEIR_INFO=""
+THEIR_AVATAR=""
+YOUR_INFO=""
+YOUR_JID_ID=""
+YOUR_AVATAR=""
+
+
+
+# INVOCATION
+# ———————————————————————————————————————————————————————————————————————————————
+OUTPUT="/tmp/export/"
+
+for account in $(account_list); do
+	# Reset state (repopulated by account_jid_and_nick; account_jid_id; archive_files…)
+	YOUR_INFO=""; YOUR_JID_ID=""; YOUR_AVATAR=""
+
+	jid="$(account_jid_and_nick "$account" | head -1)"
+	nick="$(account_jid_and_nick "$account" | tail -1)"
+
+	account_output="$OUTPUT/$jid/"
+	if test -n "$nick" -a ! "$nick" = "$jid"; then
+		account_output="$OUTPUT/$nick ($jid)/"
+	fi
+
+	echo "$account"
+
+	for partner in $(conversation_partners "$account"); do
+		# Reset state (repopulated by id_jid_and_nick; archive_files_with…)
+		THEIR_INFO=""; THEIR_AVATAR=""
+
+		jid="$(id_jid_and_nick "$partner" | head -1)"
+		nick="$(id_jid_and_nick "$partner" | tail -1)"
+
+		partner_output="$account_output/$jid/"
+		if test -n "$nick" -a ! "$nick" = "$jid"; then
+			partner_output="$account_output/$nick ($jid)/"
+		fi
+
+		archive_conversation_with_partner "$account" "$partner" "$partner_output"
+	done
+done