-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathdino-chat-export.sh
executable file
·420 lines (337 loc) · 12.2 KB
/
dino-chat-export.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
#!/bin/sh
#―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Name: dino-chat-exporter
# Desc: Export all conversations from Dino (XMPP client)'s database into
# textual format
# Reqs: shell, sqlite3
# Lisc: GPLv3+
# Auth: jadedctrl <jadedctrl@posteo.at>
# 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)"
if test -z "$THEIR_AVATAR"; then
THEIR_AVATAR="files/their_avatar.png"
fi
if test -z "$YOUR_AVATAR"; then
YOUR_AVATAR="files/your_avatar.png"
fi
local files="$(sqlite "$DB_FILE" \
"SELECT path
FROM file_transfer
WHERE counterpart_id == $partner_id AND account_id == $account_id;")"
for file in $files; do
cp "$DINO_HOME/files/$file" "$output_dir/$file"
done
}
# 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"
# If this message has a file attached, print the file's relative path
# Uses two seperate output formats for files and for images
local body_query_part="
CASE
WHEN message.id == (
SELECT file_transfer.info
FROM file_transfer
WHERE file_transfer.info == message.id )
THEN ( SELECT
CASE
WHEN (file_transfer.path LIKE '%.jpg') OR (file_transfer.path LIKE '%.jpeg') OR (file_transfer.path LIKE '%.jpeg')
OR (file_transfer.path LIKE '%.png') OR (file_transfer.path LIKE '%.webm') OR (file_transfer.path LIKE '%.svg')
THEN PRINTF('$IMAGE_FORMAT', 'files/' || path)
ELSE PRINTF('$FILE_FORMAT', 'files/' || path, 'files/' || path)
END
FROM file_transfer
WHERE file_transfer.info == message.id )
ELSE message.body
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^$(echo "$body_query_part" | tr '\n' ' ' | tr -d '\t')^g"
}
# Prints a header/footer for message output, replacing useful variables
output_message_cap() {
local account_id="$1"
local partner_id="$2"
local message_cap="$3"
echo "$message_cap" \
| 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"
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"
}
# 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
# 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
if test -z "$FILE_FORMAT"; then
FILE_FORMAT="File uploaded: %s"
fi
if test -z "$IMAGE_FORMAT"; then
IMAGE_FORMAT="Image uploaded: %s"
fi
# STATE
# ———————————————————————————————————————————————————————————————————————————————
# How repulsive… very sorry about this =w="
THEIR_INFO=""
THEIR_AVATAR=""
YOUR_INFO=""
YOUR_JID_ID=""
YOUR_AVATAR=""
# INVOCATION
# ———————————————————————————————————————————————————————————————————————————————
usage() {
echo "usage: $(basename "$0") OUTPUT_DIRECTORY"
echo
echo "Exports all conversations and files from the Dino XMPP client into a plain-text format."
echo
echo ' $DINO_HOME'
echo ' Dino data directory (default: $XDG_DATA_HOME/Dino or ~/.local/share/Dino)'
echo ' $MESSAGE_HEADER'
echo ' Text preceding each message file, with basic substitutions. (e.g., "<html><body>…")'
echo ' Substitutions are THEIR_JID, YOUR_JID, THEIR_NICK, and YOUR_NICK.'
echo ' $MESSAGE_FOOTER'
echo ' Likewise, but is output to the end of each message file. (e.g., "</body></html>")'
echo ' $MESSAGE_FORMAT'
echo ' Template for message output, in a printf style (e.g., "[%s] <%s>: %s")'
echo ' $MESSAGE_SLOTS'
echo ' Comma-delimited arguments for $MESSAGE_FORMAT (e.g., "DATE,JID,BODY")'
echo ' Valid slots are AVATAR, BODY, DATE, and JID.'
echo ' $IMAGE_FORMAT'
echo ' Format for message-bodies containing an image. (e.g., "<img src="%s" />)'
echo ' Leave blank or as '%s' to simply print the image path.'
echo ' $FILE_FORMAT'
echo ' Likewise, but for every other sort of attached file.'
exit 2
}
OUTPUT="$1"
if test -z "$OUTPUT" -o "$1" = "--help" -o "$1" = "-h"; then
usage
fi
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
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
echo "Archiving $jid…"
archive_conversation_with_partner "$account" "$partner" "$partner_output"
done
done