qTox  Version: nightly | Commit: bc751c8e1cac455f9690654fcfe0f560d2d7dfdd
history.cpp
Go to the documentation of this file.
1 /*
2  Copyright © 2015-2019 by The qTox Project Contributors
3 
4  This file is part of qTox, a Qt-based graphical interface for Tox.
5 
6  qTox is libre software: you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation, either version 3 of the License, or
9  (at your option) any later version.
10 
11  qTox is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  GNU General Public License for more details.
15 
16  You should have received a copy of the GNU General Public License
17  along with qTox. If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include <QDebug>
21 #include <cassert>
22 
23 #include "history.h"
24 #include "profile.h"
25 #include "settings.h"
26 #include "db/rawdatabase.h"
27 #include "src/core/toxpk.h"
28 
29 namespace {
30 static constexpr int SCHEMA_VERSION = 9;
31 
32 bool createCurrentSchema(RawDatabase& db)
33 {
34  QVector<RawDatabase::Query> queries;
35  queries += RawDatabase::Query(QStringLiteral(
36  "CREATE TABLE peers (id INTEGER PRIMARY KEY, "
37  "public_key TEXT NOT NULL UNIQUE);"
38  "CREATE TABLE aliases (id INTEGER PRIMARY KEY, "
39  "owner INTEGER, "
40  "display_name BLOB NOT NULL, "
41  "UNIQUE(owner, display_name), "
42  "FOREIGN KEY (owner) REFERENCES peers(id));"
43  "CREATE TABLE history "
44  "(id INTEGER PRIMARY KEY, "
45  "message_type CHAR(1) NOT NULL DEFAULT 'T' CHECK (message_type in ('T','F','S')), "
46  "timestamp INTEGER NOT NULL, "
47  "chat_id INTEGER NOT NULL, "
48  // Message subtypes want to reference the following as a foreign key. Foreign keys must be
49  // guaranteed to be unique. Since an ID is already unique, id + message type is also unique
50  "UNIQUE (id, message_type), "
51  "FOREIGN KEY (chat_id) REFERENCES peers(id)); "
52  "CREATE TABLE text_messages "
53  "(id INTEGER PRIMARY KEY, "
54  "message_type CHAR(1) NOT NULL CHECK (message_type = 'T'), "
55  "sender_alias INTEGER NOT NULL, "
56  // even though technically a message can be null for file transfer, we've opted
57  // to just insert an empty string when there's no content, this moderately simplifies
58  // implementation as currently our database doesn't have support for optional fields.
59  // We would either have to insert "?" or "null" based on if message exists and then
60  // ensure that our blob vector always has the right number of fields. Better to just
61  // leave this as NOT NULL for now.
62  "message BLOB NOT NULL, "
63  "FOREIGN KEY (id, message_type) REFERENCES history(id, message_type), "
64  "FOREIGN KEY (sender_alias) REFERENCES aliases(id)); "
65  "CREATE TABLE file_transfers "
66  "(id INTEGER PRIMARY KEY, "
67  "message_type CHAR(1) NOT NULL CHECK (message_type = 'F'), "
68  "sender_alias INTEGER NOT NULL, "
69  "file_restart_id BLOB NOT NULL, "
70  "file_name BLOB NOT NULL, "
71  "file_path BLOB NOT NULL, "
72  "file_hash BLOB NOT NULL, "
73  "file_size INTEGER NOT NULL, "
74  "direction INTEGER NOT NULL, "
75  "file_state INTEGER NOT NULL, "
76  "FOREIGN KEY (id, message_type) REFERENCES history(id, message_type), "
77  "FOREIGN KEY (sender_alias) REFERENCES aliases(id)); "
78  "CREATE TABLE system_messages "
79  "(id INTEGER PRIMARY KEY, "
80  "message_type CHAR(1) NOT NULL CHECK (message_type = 'S'), "
81  "system_message_type INTEGER NOT NULL, "
82  "arg1 BLOB, "
83  "arg2 BLOB, "
84  "arg3 BLOB, "
85  "arg4 BLOB, "
86  "FOREIGN KEY (id, message_type) REFERENCES history(id, message_type)); "
87  "CREATE TABLE faux_offline_pending (id INTEGER PRIMARY KEY, "
88  "required_extensions INTEGER NOT NULL DEFAULT 0, "
89  "FOREIGN KEY (id) REFERENCES history(id));"
90  "CREATE TABLE broken_messages (id INTEGER PRIMARY KEY, "
91  "reason INTEGER NOT NULL DEFAULT 0, "
92  "FOREIGN KEY (id) REFERENCES history(id));"));
93  // sqlite doesn't support including the index as part of the CREATE TABLE statement, so add a second query
94  queries += RawDatabase::Query(
95  "CREATE INDEX chat_id_idx on history (chat_id);");
96  queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = %1;").arg(SCHEMA_VERSION));
97  return db.execNow(queries);
98 }
99 
100 bool isNewDb(std::shared_ptr<RawDatabase>& db, bool& success)
101 {
102  bool newDb;
103  if (!db->execNow(RawDatabase::Query("SELECT COUNT(*) FROM sqlite_master;",
104  [&](const QVector<QVariant>& row) {
105  newDb = row[0].toLongLong() == 0;
106  }))) {
107  db.reset();
108  success = false;
109  return false;
110  }
111  success = true;
112  return newDb;
113 }
114 
115 bool dbSchema0to1(RawDatabase& db)
116 {
117  QVector<RawDatabase::Query> queries;
118  queries += RawDatabase::Query(QStringLiteral("CREATE TABLE file_transfers "
119  "(id INTEGER PRIMARY KEY, "
120  "chat_id INTEGER NOT NULL, "
121  "file_restart_id BLOB NOT NULL, "
122  "file_name BLOB NOT NULL, "
123  "file_path BLOB NOT NULL, "
124  "file_hash BLOB NOT NULL, "
125  "file_size INTEGER NOT NULL, "
126  "direction INTEGER NOT NULL, "
127  "file_state INTEGER NOT NULL);"));
128  queries += RawDatabase::Query(QStringLiteral("ALTER TABLE history ADD file_id INTEGER;"));
129  queries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 1;"));
130  return db.execNow(queries);
131 }
132 
133 bool dbSchema1to2(RawDatabase& db)
134 {
135  // Any faux_offline_pending message, in a chat that has newer delivered
136  // message is decided to be broken. It must be moved from
137  // faux_offline_pending to broken_messages
138 
139  // the last non-pending message in each chat
140  QString lastDeliveredQuery =
141  QString("SELECT chat_id, MAX(history.id) FROM "
142  "history JOIN peers chat ON chat_id = chat.id "
143  "LEFT JOIN faux_offline_pending ON history.id = faux_offline_pending.id "
144  "WHERE faux_offline_pending.id IS NULL "
145  "GROUP BY chat_id;");
146 
147  QVector<RawDatabase::Query> upgradeQueries;
148  upgradeQueries += RawDatabase::Query(QStringLiteral("CREATE TABLE broken_messages "
149  "(id INTEGER PRIMARY KEY);"));
150 
151  auto rowCallback = [&upgradeQueries](const QVector<QVariant>& row) {
152  auto chatId = row[0].toLongLong();
153  auto lastDeliveredHistoryId = row[1].toLongLong();
154 
155  upgradeQueries += QString("INSERT INTO broken_messages "
156  "SELECT faux_offline_pending.id FROM "
157  "history JOIN faux_offline_pending "
158  "ON faux_offline_pending.id = history.id "
159  "WHERE history.chat_id=%1 "
160  "AND history.id < %2;")
161  .arg(chatId)
162  .arg(lastDeliveredHistoryId);
163  };
164  // note this doesn't modify the db, just generate new queries, so is safe
165  // to run outside of our upgrade transaction
166  if (!db.execNow({lastDeliveredQuery, rowCallback})) {
167  return false;
168  }
169 
170  upgradeQueries += QString("DELETE FROM faux_offline_pending "
171  "WHERE id in ("
172  "SELECT id FROM broken_messages);");
173 
174  upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 2;"));
175 
176  return db.execNow(upgradeQueries);
177 }
178 
179 bool dbSchema2to3(RawDatabase& db)
180 {
181  // Any faux_offline_pending message with the content "/me " are action
182  // messages that qTox previously let a user enter, but that will cause an
183  // action type message to be sent to toxcore, with 0 length, which will
184  // always fail. They must be be moved from faux_offline_pending to broken_messages
185  // to avoid qTox from erroring trying to send them on every connect
186 
187  const QString emptyActionMessageString = "/me ";
188 
189  QVector<RawDatabase::Query> upgradeQueries;
190  upgradeQueries += RawDatabase::Query{QString("INSERT INTO broken_messages "
191  "SELECT faux_offline_pending.id FROM "
192  "history JOIN faux_offline_pending "
193  "ON faux_offline_pending.id = history.id "
194  "WHERE history.message = ?;"),
195  {emptyActionMessageString.toUtf8()}};
196 
197  upgradeQueries += QString("DELETE FROM faux_offline_pending "
198  "WHERE id in ("
199  "SELECT id FROM broken_messages);");
200 
201  upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 3;"));
202 
203  return db.execNow(upgradeQueries);
204 }
205 
206 bool dbSchema3to4(RawDatabase& db)
207 {
208  QVector<RawDatabase::Query> upgradeQueries;
209  upgradeQueries += RawDatabase::Query{QString(
210  "CREATE INDEX chat_id_idx on history (chat_id);")};
211 
212  upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 4;"));
213 
214  return db.execNow(upgradeQueries);
215 }
216 
217 void addForeignKeyToAlias(QVector<RawDatabase::Query>& queries)
218 {
219  queries += RawDatabase::Query(QStringLiteral(
220  "CREATE TABLE aliases_new (id INTEGER PRIMARY KEY, owner INTEGER, "
221  "display_name BLOB NOT NULL, UNIQUE(owner, display_name), "
222  "FOREIGN KEY (owner) REFERENCES peers(id));"));
223  queries += RawDatabase::Query(QStringLiteral(
224  "INSERT INTO aliases_new (id, owner, display_name) "
225  "SELECT id, owner, display_name "
226  "FROM aliases;"));
227  queries += RawDatabase::Query(QStringLiteral("DROP TABLE aliases;"));
228  queries += RawDatabase::Query(QStringLiteral("ALTER TABLE aliases_new RENAME TO aliases;"));
229 }
230 
231 void addForeignKeyToHistory(QVector<RawDatabase::Query>& queries)
232 {
233  queries += RawDatabase::Query(QStringLiteral(
234  "CREATE TABLE history_new "
235  "(id INTEGER PRIMARY KEY, "
236  "timestamp INTEGER NOT NULL, "
237  "chat_id INTEGER NOT NULL, "
238  "sender_alias INTEGER NOT NULL, "
239  "message BLOB NOT NULL, "
240  "file_id INTEGER, "
241  "FOREIGN KEY (file_id) REFERENCES file_transfers(id), "
242  "FOREIGN KEY (chat_id) REFERENCES peers(id), "
243  "FOREIGN KEY (sender_alias) REFERENCES aliases(id));"));
244  queries += RawDatabase::Query(QStringLiteral(
245  "INSERT INTO history_new (id, timestamp, chat_id, sender_alias, message, file_id) "
246  "SELECT id, timestamp, chat_id, sender_alias, message, file_id "
247  "FROM history;"));
248  queries += RawDatabase::Query(QStringLiteral("DROP TABLE history;"));
249  queries += RawDatabase::Query(QStringLiteral("ALTER TABLE history_new RENAME TO history;"));
250 }
251 
252 void addForeignKeyToFauxOfflinePending(QVector<RawDatabase::Query>& queries)
253 {
254  queries += RawDatabase::Query(QStringLiteral(
255  "CREATE TABLE new_faux_offline_pending (id INTEGER PRIMARY KEY, "
256  "FOREIGN KEY (id) REFERENCES history(id));"));
257  queries += RawDatabase::Query(QStringLiteral(
258  "INSERT INTO new_faux_offline_pending (id) "
259  "SELECT id "
260  "FROM faux_offline_pending;"));
261  queries += RawDatabase::Query(QStringLiteral("DROP TABLE faux_offline_pending;"));
262  queries += RawDatabase::Query(QStringLiteral("ALTER TABLE new_faux_offline_pending RENAME TO faux_offline_pending;"));
263 }
264 
265 void addForeignKeyToBrokenMessages(QVector<RawDatabase::Query>& queries)
266 {
267  queries += RawDatabase::Query(QStringLiteral(
268  "CREATE TABLE new_broken_messages (id INTEGER PRIMARY KEY, "
269  "FOREIGN KEY (id) REFERENCES history(id));"));
270  queries += RawDatabase::Query(QStringLiteral(
271  "INSERT INTO new_broken_messages (id) "
272  "SELECT id "
273  "FROM broken_messages;"));
274  queries += RawDatabase::Query(QStringLiteral("DROP TABLE broken_messages;"));
275  queries += RawDatabase::Query(QStringLiteral("ALTER TABLE new_broken_messages RENAME TO broken_messages;"));
276 }
277 
278 bool dbSchema4to5(RawDatabase& db)
279 {
280  // add foreign key contrains to database tables. sqlite doesn't support advanced alter table commands, so instead we
281  // need to copy data to new tables with the foreign key contraints: http://www.sqlitetutorial.net/sqlite-alter-table/
282  QVector<RawDatabase::Query> upgradeQueries;
283  addForeignKeyToAlias(upgradeQueries);
284  addForeignKeyToHistory(upgradeQueries);
285  addForeignKeyToFauxOfflinePending(upgradeQueries);
286  addForeignKeyToBrokenMessages(upgradeQueries);
287  upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 5;"));
288  auto transactionPass = db.execNow(upgradeQueries);
289  if (transactionPass) {
290  db.execNow("VACUUM;"); // after copying all the tables and deleting the old ones, our db file is half empty.
291  }
292  return transactionPass;
293 }
294 
295 bool dbSchema5to6(RawDatabase& db)
296 {
297  QVector<RawDatabase::Query> upgradeQueries;
298 
299  upgradeQueries += RawDatabase::Query{QString("ALTER TABLE faux_offline_pending "
300  "ADD COLUMN required_extensions INTEGER NOT NULL "
301  "DEFAULT 0;")};
302 
303  upgradeQueries += RawDatabase::Query{QString("ALTER TABLE broken_messages "
304  "ADD COLUMN reason INTEGER NOT NULL "
305  "DEFAULT 0;")};
306 
307  upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 6;"));
308  return db.execNow(upgradeQueries);
309 }
310 
311 bool dbSchema6to7(RawDatabase& db)
312 {
313  QVector<RawDatabase::Query> upgradeQueries;
314 
315  // Cannot add UNIQUE(id, message_type) to history table without creating a new one. Create a new history table
316  upgradeQueries += RawDatabase::Query(
317  "CREATE TABLE history_new (id INTEGER PRIMARY KEY, message_type CHAR(1) NOT NULL DEFAULT "
318  "'T' CHECK (message_type in ('T','F','S')), timestamp INTEGER NOT NULL, chat_id INTEGER "
319  "NOT NULL, UNIQUE (id, message_type), FOREIGN KEY (chat_id) REFERENCES peers(id))");
320 
321  // Create new text_messages table. We will split messages out of history and insert them into this new table
322  upgradeQueries += RawDatabase::Query(
323  "CREATE TABLE text_messages (id INTEGER PRIMARY KEY, message_type CHAR(1) NOT NULL CHECK "
324  "(message_type = 'T'), sender_alias INTEGER NOT NULL, message BLOB NOT NULL, FOREIGN KEY "
325  "(id, message_type) REFERENCES history_new(id, message_type), FOREIGN KEY (sender_alias) "
326  "REFERENCES aliases(id))");
327 
328  // Cannot add a FOREIGN KEY to the file_transfers table without creating a new one. Create a new file_transfers table
329  upgradeQueries += RawDatabase::Query(
330  "CREATE TABLE file_transfers_new (id INTEGER PRIMARY KEY, message_type CHAR(1) NOT NULL "
331  "CHECK (message_type = 'F'), sender_alias INTEGER NOT NULL, file_restart_id BLOB NOT NULL, "
332  "file_name BLOB NOT NULL, file_path BLOB NOT NULL, file_hash BLOB NOT NULL, file_size "
333  "INTEGER NOT NULL, direction INTEGER NOT NULL, file_state INTEGER NOT NULL, FOREIGN KEY "
334  "(id, message_type) REFERENCES history_new(id, message_type), FOREIGN KEY (sender_alias) "
335  "REFERENCES aliases(id))");
336 
337  upgradeQueries +=
338  RawDatabase::Query("INSERT INTO history_new SELECT id, 'T' AS message_type, timestamp, "
339  "chat_id FROM history WHERE history.file_id IS NULL");
340 
341  upgradeQueries +=
342  RawDatabase::Query("INSERT INTO text_messages SELECT id, 'T' AS message_type, "
343  "sender_alias, message FROM history WHERE history.file_id IS NULL");
344 
345  upgradeQueries +=
346  RawDatabase::Query("INSERT INTO history_new SELECT id, 'F' AS message_type, timestamp, "
347  "chat_id FROM history WHERE history.file_id IS NOT NULL");
348 
349  upgradeQueries += RawDatabase::Query(
350  "INSERT INTO file_transfers_new (id, message_type, sender_alias, file_restart_id, "
351  "file_name, file_path, file_hash, file_size, direction, file_state) SELECT history.id, 'F' "
352  "as message_type, history.sender_alias, file_transfers.file_restart_id, "
353  "file_transfers.file_name, file_transfers.file_path, file_transfers.file_hash, "
354  "file_transfers.file_size, file_transfers.direction, file_transfers.file_state FROM "
355  "history INNER JOIN file_transfers on history.file_id = file_transfers.id WHERE "
356  "history.file_id IS NOT NULL");
357 
358  upgradeQueries += RawDatabase::Query(
359  "CREATE TABLE system_messages (id INTEGER PRIMARY KEY, message_type CHAR(1) NOT NULL CHECK "
360  "(message_type = 'S'), system_message_type INTEGER NOT NULL, arg1 BLOB, arg2 BLOB, arg3 BLOB, arg4 BLOB, "
361  "FOREIGN KEY (id, message_type) REFERENCES history_new(id, message_type))");
362 
363  // faux_offline_pending needs to be re-created to reference the new history table
364  upgradeQueries += RawDatabase::Query(
365  "CREATE TABLE faux_offline_pending_new (id INTEGER PRIMARY KEY, required_extensions "
366  "INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (id) REFERENCES history_new(id))");
367  upgradeQueries += RawDatabase::Query("INSERT INTO faux_offline_pending_new SELECT id, "
368  "required_extensions FROM faux_offline_pending");
369  upgradeQueries += RawDatabase::Query("DROP TABLE faux_offline_pending");
370  upgradeQueries +=
371  RawDatabase::Query("ALTER TABLE faux_offline_pending_new RENAME TO faux_offline_pending");
372 
373  // broken_messages needs to be re-created to reference the new history table
374  upgradeQueries += RawDatabase::Query(
375  "CREATE TABLE broken_messages_new (id INTEGER PRIMARY KEY, reason INTEGER NOT NULL DEFAULT "
376  "0, FOREIGN KEY (id) REFERENCES history_new(id))");
377  upgradeQueries += RawDatabase::Query(
378  "INSERT INTO broken_messages_new SELECT id, reason FROM broken_messages");
379  upgradeQueries += RawDatabase::Query("DROP TABLE broken_messages");
380  upgradeQueries +=
381  RawDatabase::Query("ALTER TABLE broken_messages_new RENAME TO broken_messages");
382 
383  // Everything referencing old history should now be gone
384  upgradeQueries += RawDatabase::Query("DROP TABLE history");
385  upgradeQueries += RawDatabase::Query("ALTER TABLE history_new RENAME TO history");
386 
387  // Drop file transfers late since history depends on it
388  upgradeQueries += RawDatabase::Query("DROP TABLE file_transfers");
389  upgradeQueries += RawDatabase::Query("ALTER TABLE file_transfers_new RENAME TO file_transfers");
390 
391  upgradeQueries += RawDatabase::Query("CREATE INDEX chat_id_idx on history (chat_id);");
392 
393  upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 7;"));
394 
395  return db.execNow(upgradeQueries);
396 }
397 
398 bool dbSchema7to8(RawDatabase& db)
399 {
400  // Dummy upgrade. This upgrade does not change the schema, however on
401  // version 7 if qtox saw a system message it would assert and crash. This
402  // upgrade ensures that old versions of qtox do not try to load the new
403  // database
404 
405  QVector<RawDatabase::Query> upgradeQueries;
406  upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 8;"));
407 
408  return db.execNow(upgradeQueries);
409 }
410 
411 struct BadEntry {
412  BadEntry(int64_t row, QString toxId) :
413  row{row},
414  toxId{toxId} {}
415  RowId row;
416  QString toxId;
417 };
418 
419 std::vector<BadEntry> getInvalidPeers(RawDatabase& db)
420 {
421  std::vector<BadEntry> badPeerIds;
422  db.execNow(RawDatabase::Query("SELECT id, public_key FROM peers WHERE LENGTH(public_key) != 64", [&](const QVector<QVariant>& row) {
423  badPeerIds.emplace_back(BadEntry{row[0].toInt(), row[1].toString()});
424  }));
425  return badPeerIds;
426 }
427 
428 RowId getValidPeerRow(RawDatabase& db, const ToxPk& friendPk)
429 {
430  bool validPeerExists{false};
431  RowId validPeerRow;
432  db.execNow(RawDatabase::Query(QStringLiteral("SELECT id FROM peers WHERE public_key='%1';")
433  .arg(friendPk.toString()), [&](const QVector<QVariant>& row) {
434  validPeerRow = RowId{row[0].toLongLong()};
435  validPeerExists = true;
436  }));
437  if (validPeerExists) {
438  return validPeerRow;
439  }
440 
441  db.execNow(RawDatabase::Query(("SELECT id FROM peers ORDER BY id DESC LIMIT 1;"), [&](const QVector<QVariant>& row) {
442  int64_t maxPeerId = row[0].toInt();
443  validPeerRow = RowId{maxPeerId + 1};
444  }));
445  db.execNow(RawDatabase::Query(QStringLiteral("INSERT INTO peers (id, public_key) VALUES (%1, '%2');").arg(validPeerRow.get()).arg(friendPk.toString())));
446  return validPeerRow;
447 }
448 
449 struct DuplicateAlias {
450  DuplicateAlias(RowId goodAliasRow, std::vector<RowId> badAliasRows) :
451  goodAliasRow{goodAliasRow},
452  badAliasRows{badAliasRows} {}
453  DuplicateAlias() {};
454  RowId goodAliasRow{-1};
455  std::vector<RowId> badAliasRows;
456 };
457 
458 DuplicateAlias getDuplicateAliasRows(RawDatabase& db, RowId goodPeerRow, RowId badPeerRow)
459 {
460  std::vector<RowId> badAliasRows;
461  RowId goodAliasRow;
462  bool hasGoodEntry{false};
463  db.execNow(RawDatabase::Query(QStringLiteral("SELECT good.id, bad.id FROM aliases good INNER JOIN aliases bad ON good.display_name=bad.display_name WHERE good.owner=%1 AND bad.owner=%2;").arg(goodPeerRow.get()).arg(badPeerRow.get()),
464  [&](const QVector<QVariant>& row) {
465  hasGoodEntry = true;
466  goodAliasRow = RowId{row[0].toInt()};
467  badAliasRows.emplace_back(RowId{row[1].toLongLong()});
468  }));
469 
470  if (hasGoodEntry) {
471  return {goodAliasRow, badAliasRows};
472  } else {
473  return {};
474  }
475 }
476 
477 void mergeAndDeleteAlias(QVector<RawDatabase::Query>& upgradeQueries, RowId goodAlias, std::vector<RowId> badAliases)
478 {
479  for (const auto badAliasId : badAliases) {
480  upgradeQueries += RawDatabase::Query(QStringLiteral("UPDATE text_messages SET sender_alias = %1 WHERE sender_alias = %2;").arg(goodAlias.get()).arg(badAliasId.get()));
481  upgradeQueries += RawDatabase::Query(QStringLiteral("UPDATE file_transfers SET sender_alias = %1 WHERE sender_alias = %2;").arg(goodAlias.get()).arg(badAliasId.get()));
482  upgradeQueries += RawDatabase::Query(QStringLiteral("DELETE FROM aliases WHERE id = %1;").arg(badAliasId.get()));
483  }
484 }
485 
486 void mergeAndDeletePeer(QVector<RawDatabase::Query>& upgradeQueries, RowId goodPeerId, RowId badPeerId)
487 {
488  upgradeQueries += RawDatabase::Query(QStringLiteral("UPDATE aliases SET owner = %1 WHERE owner = %2").arg(goodPeerId.get()).arg(badPeerId.get()));
489  upgradeQueries += RawDatabase::Query(QStringLiteral("UPDATE history SET chat_id = %1 WHERE chat_id = %2;").arg(goodPeerId.get()).arg(badPeerId.get()));
490  upgradeQueries += RawDatabase::Query(QStringLiteral("DELETE FROM peers WHERE id = %1").arg(badPeerId.get()));
491 }
492 
493 void mergeDuplicatePeers(QVector<RawDatabase::Query>& upgradeQueries, RawDatabase& db, std::vector<BadEntry> badPeers)
494 {
495  for (const auto& badPeer : badPeers) {
496  const RowId goodPeerId = getValidPeerRow(db, ToxPk{badPeer.toxId.left(64)});
497  const auto aliasDuplicates = getDuplicateAliasRows(db, goodPeerId, badPeer.row);
498  mergeAndDeleteAlias(upgradeQueries, aliasDuplicates.goodAliasRow, aliasDuplicates.badAliasRows);
499  mergeAndDeletePeer(upgradeQueries, goodPeerId, badPeer.row);
500  }
501 }
502 
503 bool dbSchema8to9(RawDatabase& db)
504 {
505  // not technically a schema update, but still a database version update based on healing invalid user data
506  // we added ourself in the peers table by ToxId isntead of ToxPk. Heal this over-length entry.
507  QVector<RawDatabase::Query> upgradeQueries;
508  const auto badPeers = getInvalidPeers(db);
509  mergeDuplicatePeers(upgradeQueries, db, badPeers);
510  upgradeQueries += RawDatabase::Query(QStringLiteral("PRAGMA user_version = 9;"));
511  return db.execNow(upgradeQueries);
512 }
513 
519 bool dbSchemaUpgrade(std::shared_ptr<RawDatabase>& db)
520 {
521  // If we're a new dB we can just make a new one and call it a day
522  bool success = false;
523  const bool newDb = isNewDb(db, success);
524  if (!success) {
525  qCritical() << "Failed to create current db schema";
526  return false;
527  }
528 
529  if (newDb) {
530  if (!createCurrentSchema(*db)) {
531  qCritical() << "Failed to create current db schema";
532  return false;
533  }
534  qDebug() << "Database created at schema version" << SCHEMA_VERSION;
535  return true;
536  }
537 
538  // Otherwise we have to do upgrades from our current version to the latest version
539 
540  int64_t databaseSchemaVersion;
541 
542  if (!db->execNow(RawDatabase::Query("PRAGMA user_version", [&](const QVector<QVariant>& row) {
543  databaseSchemaVersion = row[0].toLongLong();
544  }))) {
545  qCritical() << "History failed to read user_version";
546  return false;
547  }
548 
549  if (databaseSchemaVersion > SCHEMA_VERSION) {
550  qWarning().nospace() << "Database version (" << databaseSchemaVersion
551  << ") is newer than we currently support (" << SCHEMA_VERSION
552  << "). Please upgrade qTox";
553  // We don't know what future versions have done, we have to disable db access until we re-upgrade
554  return false;
555  } else if (databaseSchemaVersion == SCHEMA_VERSION) {
556  // No work to do
557  return true;
558  }
559 
560  using DbSchemaUpgradeFn = bool (*)(RawDatabase&);
561  std::vector<DbSchemaUpgradeFn> upgradeFns = {dbSchema0to1, dbSchema1to2, dbSchema2to3,
562  dbSchema3to4, dbSchema4to5, dbSchema5to6,
563  dbSchema6to7, dbSchema7to8, dbSchema8to9};
564 
565  assert(databaseSchemaVersion < static_cast<int>(upgradeFns.size()));
566  assert(upgradeFns.size() == SCHEMA_VERSION);
567 
568  for (int64_t i = databaseSchemaVersion; i < static_cast<int>(upgradeFns.size()); ++i) {
569  auto const newDbVersion = i + 1;
570  if (!upgradeFns[i](*db)) {
571  qCritical() << "Failed to upgrade db to schema version " << newDbVersion << " aborting";
572  return false;
573  }
574  qDebug() << "Database upgraded incrementally to schema version " << newDbVersion;
575  }
576 
577  qInfo() << "Database upgrade finished (databaseSchemaVersion" << databaseSchemaVersion << "->"
578  << SCHEMA_VERSION << ")";
579  return true;
580 }
581 
582 MessageState getMessageState(bool isPending, bool isBroken)
583 {
584  assert(!(isPending && isBroken));
585  MessageState messageState;
586 
587  if (isPending) {
588  messageState = MessageState::pending;
589  } else if (isBroken) {
590  messageState = MessageState::broken;
591  } else {
592  messageState = MessageState::complete;
593  }
594  return messageState;
595 }
596 
597 QString generatePeerIdString(ToxPk const& pk)
598 {
599  return QString("(SELECT id FROM peers WHERE public_key = '%1')").arg(pk.toString());
600 
601 }
602 
603 RawDatabase::Query generateEnsurePkInPeers(ToxPk const& pk)
604 {
605  return RawDatabase::Query{QStringLiteral("INSERT OR IGNORE INTO peers (public_key) "
606  "VALUES ('%1')").arg(pk.toString())};
607 }
608 
609 RawDatabase::Query generateUpdateAlias(ToxPk const& pk, QString const& dispName)
610 {
611  return RawDatabase::Query(
612  QString("INSERT OR IGNORE INTO aliases (owner, display_name) VALUES (%1, ?);").arg(generatePeerIdString(pk)),
613  {dispName.toUtf8()});
614 }
615 
616 RawDatabase::Query generateHistoryTableInsertion(char type, const QDateTime& time, const ToxPk& friendPk)
617 {
618  return RawDatabase::Query(QString("INSERT INTO history (message_type, timestamp, chat_id) "
619  "VALUES ('%1', %2, %3);")
620  .arg(type)
621  .arg(time.toMSecsSinceEpoch())
622  .arg(generatePeerIdString(friendPk)));
623 }
624 
635 QVector<RawDatabase::Query>
636 generateNewTextMessageQueries(const ToxPk& friendPk, const QString& message, const ToxPk& sender,
637  const QDateTime& time, bool isDelivered, ExtensionSet extensionSet,
638  QString dispName, std::function<void(RowId)> insertIdCallback)
639 {
640  QVector<RawDatabase::Query> queries;
641 
642  queries += generateEnsurePkInPeers(friendPk);
643  queries += generateEnsurePkInPeers(sender);
644  queries += generateUpdateAlias(sender, dispName);
645  queries += generateHistoryTableInsertion('T', time, friendPk);
646 
647  queries += RawDatabase::Query(
648  QString("INSERT INTO text_messages (id, message_type, sender_alias, message) "
649  "VALUES ( "
650  " last_insert_rowid(), "
651  " 'T', "
652  " (SELECT id FROM aliases WHERE owner=%1 and display_name=?), "
653  " ?"
654  ");")
655  .arg(generatePeerIdString(sender)),
656  {dispName.toUtf8(), message.toUtf8()}, insertIdCallback);
657 
658  if (!isDelivered) {
659  queries += RawDatabase::Query{
660  QString("INSERT INTO faux_offline_pending (id, required_extensions) VALUES ("
661  " last_insert_rowid(), %1"
662  ");")
663  .arg(extensionSet.to_ulong())};
664  }
665 
666  return queries;
667 }
668 
669 QVector<RawDatabase::Query> generateNewSystemMessageQueries(const ToxPk& friendPk,
670  const SystemMessage& systemMessage)
671 {
672  QVector<RawDatabase::Query> queries;
673 
674  queries += generateEnsurePkInPeers(friendPk);
675  queries += generateHistoryTableInsertion('S', systemMessage.timestamp, friendPk);
676 
677  QVector<QByteArray> blobs;
678  std::transform(systemMessage.args.begin(), systemMessage.args.end(), std::back_inserter(blobs),
679  [](const QString& s) { return s.toUtf8(); });
680 
681  queries += RawDatabase::Query(QString("INSERT INTO system_messages (id, message_type, "
682  "system_message_type, arg1, arg2, arg3, arg4)"
683  "VALUES (last_insert_rowid(), 'S', %1, ?, ?, ?, ?)")
684  .arg(static_cast<int>(systemMessage.messageType)),
685  blobs);
686 
687  return queries;
688 }
689 } // namespace
690 
701 {
702  static int id = qRegisterMetaType<FileDbInsertionData>();
703  (void)id;
704 }
705 
710 History::History(std::shared_ptr<RawDatabase> db_)
711  : db(db_)
712 {
713  if (!isValid()) {
714  qWarning() << "Database not open, init failed";
715  return;
716  }
717 
718  // foreign key support is not enabled by default, so needs to be enabled on every connection
719  // support was added in sqlite 3.6.19, which is qTox's minimum supported version
720  db->execNow(
721  "PRAGMA foreign_keys = ON;");
722 
723  const auto upgradeSucceeded = dbSchemaUpgrade(db);
724 
725  // dbSchemaUpgrade may have put us in an invalid state
726  if (!upgradeSucceeded) {
727  db.reset();
728  return;
729  }
730 
731  connect(this, &History::fileInserted, this, &History::onFileInserted);
732 }
733 
735 {
736  if (!isValid()) {
737  return;
738  }
739 
740  // We could have execLater requests pending with a lambda attached,
741  // so clear the pending transactions first
742  db->sync();
743 }
744 
750 {
751  return db && db->isOpen();
752 }
753 
759 bool History::historyExists(const ToxPk& friendPk)
760 {
761  if (historyAccessBlocked()) {
762  return false;
763  }
764 
765  return !getMessagesForFriend(friendPk, 0, 1).empty();
766 }
767 
772 {
773  if (!isValid()) {
774  return;
775  }
776 
777  db->execNow("DELETE FROM faux_offline_pending;"
778  "DELETE FROM broken_messages;"
779  "DELETE FROM text_messages;"
780  "DELETE FROM file_transfers;"
781  "DELETE FROM system_messages;"
782  "DELETE FROM history;"
783  "DELETE FROM aliases;"
784  "DELETE FROM peers;"
785  "VACUUM;");
786 }
787 
792 void History::removeFriendHistory(const ToxPk& friendPk)
793 {
794  if (!isValid()) {
795  return;
796  }
797 
798  QString queryText = QString("DELETE FROM faux_offline_pending "
799  "WHERE faux_offline_pending.id IN ( "
800  " SELECT faux_offline_pending.id FROM faux_offline_pending "
801  " LEFT JOIN history ON faux_offline_pending.id = history.id "
802  " WHERE chat_id=%1 "
803  "); "
804  "DELETE FROM broken_messages "
805  "WHERE broken_messages.id IN ( "
806  " SELECT broken_messages.id FROM broken_messages "
807  " LEFT JOIN history ON broken_messages.id = history.id "
808  " WHERE chat_id=%1 "
809  "); "
810  "DELETE FROM text_messages "
811  "WHERE id IN ("
812  " SELECT id from history "
813  " WHERE message_type = 'T' AND chat_id=%1);"
814  "DELETE FROM file_transfers "
815  "WHERE id IN ( "
816  " SELECT id from history "
817  " WHERE message_type = 'F' AND chat_id=%1);"
818  "DELETE FROM system_messages "
819  "WHERE id IN ( "
820  " SELECT id from history "
821  " WHERE message_type = 'S' AND chat_id=%1);"
822  "DELETE FROM history WHERE chat_id=%1; "
823  "DELETE FROM aliases WHERE owner=%1; "
824  "DELETE FROM peers WHERE id=%1; "
825  "VACUUM;")
826  .arg(generatePeerIdString(friendPk));
827 
828  if (!db->execNow(queryText)) {
829  qWarning() << "Failed to remove friend's history";
830  }
831 }
832 
833 void History::onFileInserted(RowId dbId, QString fileId)
834 {
835  auto& fileInfo = fileInfos[fileId];
836  if (fileInfo.finished) {
837  db->execLater(
838  generateFileFinished(dbId, fileInfo.success, fileInfo.filePath, fileInfo.fileHash));
839  fileInfos.remove(fileId);
840  } else {
841  fileInfo.finished = false;
842  fileInfo.fileId = dbId;
843  }
844 }
845 
846 QVector<RawDatabase::Query>
847 History::generateNewFileTransferQueries(const ToxPk& friendPk, const ToxPk& sender,
848  const QDateTime& time, const QString& dispName,
849  const FileDbInsertionData& insertionData)
850 {
851  QVector<RawDatabase::Query> queries;
852 
853  queries += generateEnsurePkInPeers(friendPk);
854  queries += generateEnsurePkInPeers(sender);
855  queries += generateUpdateAlias(sender, dispName);
856  queries += generateHistoryTableInsertion('F', time, friendPk);
857 
858  std::weak_ptr<History> weakThis = shared_from_this();
859  auto fileId = insertionData.fileId;
860 
861  queries +=
862  RawDatabase::Query(QString(
863  "INSERT INTO file_transfers "
864  " (id, message_type, sender_alias, "
865  " file_restart_id, file_name, file_path, "
866  " file_hash, file_size, direction, file_state) "
867  "VALUES ( "
868  " last_insert_rowid(), "
869  " 'F', "
870  " (SELECT id FROM aliases WHERE owner=%1 AND display_name=?), "
871  " ?, "
872  " ?, "
873  " ?, "
874  " ?, "
875  " %2, "
876  " %3, "
877  " %4 "
878  ");")
879  .arg(generatePeerIdString(sender))
880  .arg(insertionData.size)
881  .arg(insertionData.direction)
882  .arg(ToxFile::CANCELED),
883  {dispName.toUtf8(), insertionData.fileId.toUtf8(),
884  insertionData.fileName.toUtf8(), insertionData.filePath.toUtf8(),
885  QByteArray()},
886  [weakThis, fileId](RowId id) {
887  auto pThis = weakThis.lock();
888  if (pThis)
889  emit pThis->fileInserted(id, fileId);
890  });
891 
892  return queries;
893 }
894 
895 RawDatabase::Query History::generateFileFinished(RowId id, bool success, const QString& filePath,
896  const QByteArray& fileHash)
897 {
898  auto file_state = success ? ToxFile::FINISHED : ToxFile::CANCELED;
899  if (filePath.length()) {
900  return RawDatabase::Query(QStringLiteral("UPDATE file_transfers "
901  "SET file_state = %1, file_path = ?, file_hash = ?"
902  "WHERE id = %2")
903  .arg(file_state)
904  .arg(id.get()),
905  {filePath.toUtf8(), fileHash});
906  } else {
907  return RawDatabase::Query(QStringLiteral("UPDATE file_transfers "
908  "SET finished = %1 "
909  "WHERE id = %2")
910  .arg(file_state)
911  .arg(id.get()));
912  }
913 }
914 
915 void History::addNewFileMessage(const ToxPk& friendPk, const QString& fileId,
916  const QString& fileName, const QString& filePath, int64_t size,
917  const ToxPk& sender, const QDateTime& time, QString const& dispName)
918 {
919  if (historyAccessBlocked()) {
920  return;
921  }
922 
923  // This is an incredibly far from an optimal way of implementing this,
924  // but given the frequency that people are going to be initiating a file
925  // transfer we can probably live with it.
926 
927  // Since both inserting an alias for a user and inserting a file transfer
928  // will generate new ids, there is no good way to inject both new ids into the
929  // history query without refactoring our RawDatabase::Query and processor loops.
930 
931  // What we will do instead is chain callbacks to try to get reasonable behavior.
932  // We can call the generateNewMessageQueries() fn to insert a message with an empty
933  // message in it, and get the id with the callbck. Once we have the id we can ammend
934  // the data to have our newly inserted file_id as well
935 
936  ToxFile::FileDirection direction;
937  if (sender == friendPk) {
938  direction = ToxFile::RECEIVING;
939  } else {
940  direction = ToxFile::SENDING;
941  }
942 
943  std::weak_ptr<History> weakThis = shared_from_this();
944  FileDbInsertionData insertionData;
945  insertionData.friendPk = friendPk;
946  insertionData.fileId = fileId;
947  insertionData.fileName = fileName;
948  insertionData.filePath = filePath;
949  insertionData.size = size;
950  insertionData.direction = direction;
951 
952  auto queries = generateNewFileTransferQueries(friendPk, sender, time, dispName, insertionData);
953 
954  db->execLater(queries);
955 }
956 
957 void History::addNewSystemMessage(const ToxPk& friendPk, const SystemMessage& systemMessage)
958 {
959  if (historyAccessBlocked())
960  return;
961 
962  const auto queries = generateNewSystemMessageQueries(friendPk, systemMessage);
963 
964  db->execLater(queries);
965 }
966 
977 void History::addNewMessage(const ToxPk& friendPk, const QString& message, const ToxPk& sender,
978  const QDateTime& time, bool isDelivered, ExtensionSet extensionSet,
979  QString dispName, const std::function<void(RowId)>& insertIdCallback)
980 {
981  if (historyAccessBlocked()) {
982  return;
983  }
984 
985  db->execLater(generateNewTextMessageQueries(friendPk, message, sender, time, isDelivered,
986  extensionSet, dispName, insertIdCallback));
987 }
988 
989 void History::setFileFinished(const QString& fileId, bool success, const QString& filePath,
990  const QByteArray& fileHash)
991 {
992  if (historyAccessBlocked()) {
993  return;
994  }
995 
996  auto& fileInfo = fileInfos[fileId];
997  if (fileInfo.fileId.get() == -1) {
998  fileInfo.finished = true;
999  fileInfo.success = success;
1000  fileInfo.filePath = filePath;
1001  fileInfo.fileHash = fileHash;
1002  } else {
1003  db->execLater(generateFileFinished(fileInfo.fileId, success, filePath, fileHash));
1004  }
1005 
1006  fileInfos.remove(fileId);
1007 }
1008 
1010 {
1011  if (historyAccessBlocked()) {
1012  return 0;
1013  }
1014 
1015  return getNumMessagesForFriendBeforeDate(friendPk, QDateTime());
1016 }
1017 
1018 size_t History::getNumMessagesForFriendBeforeDate(const ToxPk& friendPk, const QDateTime& date)
1019 {
1020  if (historyAccessBlocked()) {
1021  return 0;
1022  }
1023 
1024  QString queryText = QString("SELECT COUNT(history.id) "
1025  "FROM history "
1026  "JOIN peers chat ON chat_id = chat.id "
1027  "WHERE chat.public_key='%1'")
1028  .arg(friendPk.toString());
1029 
1030  if (date.isNull()) {
1031  queryText += ";";
1032  } else {
1033  queryText += QString(" AND timestamp < %1;").arg(date.toMSecsSinceEpoch());
1034  }
1035 
1036  size_t numMessages = 0;
1037  auto rowCallback = [&numMessages](const QVector<QVariant>& row) {
1038  numMessages = row[0].toLongLong();
1039  };
1040 
1041  db->execNow({queryText, rowCallback});
1042 
1043  return numMessages;
1044 }
1045 
1047  size_t lastIdx)
1048 {
1049  if (historyAccessBlocked()) {
1050  return {};
1051  }
1052 
1053  QList<HistMessage> messages;
1054 
1055  // Don't forget to update the rowCallback if you change the selected columns!
1056  QString queryText =
1057  QString(
1058  "SELECT history.id, history.message_type, history.timestamp, faux_offline_pending.id, "
1059  " faux_offline_pending.required_extensions, broken_messages.id, text_messages.message, "
1060  " file_restart_id, file_name, file_path, file_size, file_transfers.direction, "
1061  " file_state, peers.public_key as sender_key, aliases.display_name, "
1062  " system_messages.system_message_type, system_messages.arg1, system_messages.arg2, "
1063  " system_messages.arg3, system_messages.arg4 "
1064  "FROM history "
1065  "LEFT JOIN text_messages ON history.id = text_messages.id "
1066  "LEFT JOIN file_transfers ON history.id = file_transfers.id "
1067  "LEFT JOIN system_messages ON system_messages.id == history.id "
1068  "LEFT JOIN aliases ON text_messages.sender_alias = aliases.id OR "
1069  "file_transfers.sender_alias = aliases.id "
1070  "LEFT JOIN peers ON aliases.owner = peers.id "
1071  "LEFT JOIN faux_offline_pending ON faux_offline_pending.id = history.id "
1072  "LEFT JOIN broken_messages ON broken_messages.id = history.id "
1073  "WHERE history.chat_id = %1 "
1074  "LIMIT %2 OFFSET %3;")
1075  .arg(generatePeerIdString(friendPk))
1076  .arg(lastIdx - firstIdx)
1077  .arg(firstIdx);
1078 
1079  auto rowCallback = [&friendPk, &messages](const QVector<QVariant>& row) {
1080  // If the select statement is changed please update these constants
1081  constexpr auto messageOffset = 6;
1082  constexpr auto fileOffset = 7;
1083  constexpr auto senderOffset = 13;
1084  constexpr auto systemOffset = 15;
1085 
1086  auto it = row.begin();
1087 
1088  const auto id = RowId{(*it++).toLongLong()};
1089  const auto messageType = (*it++).toString();
1090  const auto timestamp = QDateTime::fromMSecsSinceEpoch((*it++).toLongLong());
1091  const auto isPending = !(*it++).isNull();
1092  // If NULL this should just reutrn 0 which is an empty extension set, good enough for now
1093  const auto requiredExtensions = ExtensionSet((*it++).toLongLong());
1094  const auto isBroken = !(*it++).isNull();
1095  const auto messageState = getMessageState(isPending, isBroken);
1096 
1097  // Intentionally arrange query so message types are at the end so we don't have to think
1098  // about the iterator jumping around after handling the different types.
1099  assert(messageType.size() == 1);
1100  switch (messageType[0].toLatin1()) {
1101  case 'T': {
1102  it = std::next(row.begin(), messageOffset);
1103  assert(!it->isNull());
1104  const auto messageContent = (*it++).toString();
1105  it = std::next(row.begin(), senderOffset);
1106  const auto senderKey = (*it++).toString();
1107  const auto senderName = QString::fromUtf8((*it++).toByteArray().replace('\0', ""));
1108  messages += HistMessage(id, messageState, requiredExtensions, timestamp,
1109  friendPk.toString(), senderName, senderKey, messageContent);
1110  break;
1111  }
1112  case 'F': {
1113  it = std::next(row.begin(), fileOffset);
1114  assert(!it->isNull());
1115  const auto fileKind = TOX_FILE_KIND_DATA;
1116  const auto resumeFileId = (*it++).toString().toUtf8();
1117  const auto fileName = (*it++).toString();
1118  const auto filePath = (*it++).toString();
1119  const auto filesize = (*it++).toLongLong();
1120  const auto direction = static_cast<ToxFile::FileDirection>((*it++).toLongLong());
1121  const auto status = static_cast<ToxFile::FileStatus>((*it++).toLongLong());
1122 
1123  ToxFile file(0, 0, fileName, filePath, filesize, direction);
1124  file.fileKind = fileKind;
1125  file.resumeFileId = resumeFileId;
1126  file.status = status;
1127 
1128  it = std::next(row.begin(), senderOffset);
1129  const auto senderKey = (*it++).toString();
1130  const auto senderName = QString::fromUtf8((*it++).toByteArray().replace('\0', ""));
1131  messages += HistMessage(id, messageState, timestamp, friendPk.toString(), senderName,
1132  senderKey, file);
1133  break;
1134  }
1135  default:
1136  case 'S':
1137  it = std::next(row.begin(), systemOffset);
1138  assert(!it->isNull());
1139  SystemMessage systemMessage;
1140  systemMessage.messageType = static_cast<SystemMessageType>((*it++).toLongLong());
1141 
1142  auto argEnd = std::next(it, systemMessage.args.size());
1143  std::transform(it, argEnd, systemMessage.args.begin(), [](const QVariant& arg) {
1144  return QString::fromUtf8(arg.toByteArray().replace('\0', ""));
1145  });
1146  it = argEnd;
1147 
1148  messages += HistMessage(id, timestamp, friendPk.toString(), systemMessage);
1149  break;
1150  }
1151  };
1152 
1153  db->execNow({queryText, rowCallback});
1154 
1155  return messages;
1156 }
1157 
1159 {
1160  if (historyAccessBlocked()) {
1161  return {};
1162  }
1163 
1164  auto queryText =
1165  QString(
1166  "SELECT history.id, history.timestamp, faux_offline_pending.id, "
1167  " faux_offline_pending.required_extensions, broken_messages.id, text_messages.message, "
1168  " peers.public_key as sender_key, aliases.display_name "
1169  "FROM history "
1170  "JOIN text_messages ON history.id = text_messages.id "
1171  "JOIN aliases ON text_messages.sender_alias = aliases.id "
1172  "JOIN peers ON aliases.owner = peers.id "
1173  "JOIN faux_offline_pending ON faux_offline_pending.id = history.id "
1174  "LEFT JOIN broken_messages ON broken_messages.id = history.id "
1175  "WHERE history.chat_id = %1 AND history.message_type = 'T';")
1176  .arg(generatePeerIdString(friendPk));
1177 
1179  auto rowCallback = [&friendPk, &ret](const QVector<QVariant>& row) {
1180  auto it = row.begin();
1181  // dispName and message could have null bytes, QString::fromUtf8
1182  // truncates on null bytes so we strip them
1183  auto id = RowId{(*it++).toLongLong()};
1184  auto timestamp = QDateTime::fromMSecsSinceEpoch((*it++).toLongLong());
1185  auto isPending = !(*it++).isNull();
1186  auto extensionSet = ExtensionSet((*it++).toLongLong());
1187  auto isBroken = !(*it++).isNull();
1188  auto messageContent = (*it++).toString();
1189  auto senderKey = (*it++).toString();
1190  auto displayName = QString::fromUtf8((*it++).toByteArray().replace('\0', ""));
1191 
1192  MessageState messageState = getMessageState(isPending, isBroken);
1193 
1194  ret += {id, messageState, extensionSet, timestamp, friendPk.toString(),
1195  displayName, senderKey, messageContent};
1196  };
1197 
1198  db->execNow({queryText, rowCallback});
1199 
1200  return ret;
1201 }
1202 
1211 QDateTime History::getDateWhereFindPhrase(const ToxPk& friendPk, const QDateTime& from,
1212  QString phrase, const ParameterSearch& parameter)
1213 {
1214  if (historyAccessBlocked()) {
1215  return QDateTime();
1216  }
1217 
1218  QDateTime result;
1219  auto rowCallback = [&result](const QVector<QVariant>& row) {
1220  result = QDateTime::fromMSecsSinceEpoch(row[0].toLongLong());
1221  };
1222 
1223  phrase.replace("'", "''");
1224 
1225  QString message;
1226 
1227  switch (parameter.filter) {
1229  message = QStringLiteral("text_messages.message LIKE '%%1%'").arg(phrase);
1230  break;
1232  message = QStringLiteral("text_messages.message REGEXP '%1'")
1233  .arg(SearchExtraFunctions::generateFilterWordsOnly(phrase).toLower());
1234  break;
1236  message = QStringLiteral("REGEXPSENSITIVE(text_messages.message, '%1')")
1238  break;
1239  case FilterSearch::Regular:
1240  message = QStringLiteral("text_messages.message REGEXP '%1'").arg(phrase);
1241  break;
1243  message = QStringLiteral("REGEXPSENSITIVE(text_messages.message '%1')").arg(phrase);
1244  break;
1245  default:
1246  message = QStringLiteral("LOWER(text_messages.message) LIKE '%%1%'").arg(phrase.toLower());
1247  break;
1248  }
1249 
1250  QDateTime date = from;
1251 
1252  if (!date.isValid()) {
1253  date = QDateTime::currentDateTime();
1254  }
1255 
1256  if (parameter.period == PeriodSearch::AfterDate || parameter.period == PeriodSearch::BeforeDate) {
1257 
1258 #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
1259  date = parameter.date.startOfDay();
1260 #else
1261  date = QDateTime(parameter.date);
1262 #endif
1263  }
1264 
1265  QString period;
1266  switch (parameter.period) {
1268  period = QStringLiteral("ORDER BY timestamp ASC LIMIT 1;");
1269  break;
1271  period = QStringLiteral("AND timestamp > '%1' ORDER BY timestamp ASC LIMIT 1;")
1272  .arg(date.toMSecsSinceEpoch());
1273  break;
1275  period = QStringLiteral("AND timestamp < '%1' ORDER BY timestamp DESC LIMIT 1;")
1276  .arg(date.toMSecsSinceEpoch());
1277  break;
1278  default:
1279  period = QStringLiteral("AND timestamp < '%1' ORDER BY timestamp DESC LIMIT 1;")
1280  .arg(date.toMSecsSinceEpoch());
1281  break;
1282  }
1283 
1284  QString queryText =
1285  QStringLiteral("SELECT timestamp "
1286  "FROM history "
1287  "JOIN peers chat ON chat_id = chat.id "
1288  "JOIN text_messages ON history.id = text_messages.id "
1289  "WHERE chat.public_key='%1' "
1290  "AND %2 "
1291  "%3")
1292  .arg(friendPk.toString())
1293  .arg(message)
1294  .arg(period);
1295 
1296  db->execNow({queryText, rowCallback});
1297 
1298  return result;
1299 }
1300 
1314  const QDate& from,
1315  size_t maxNum)
1316 {
1317  if (historyAccessBlocked()) {
1318  return {};
1319  }
1320 
1321  auto friendPkString = friendPk.toString();
1322 
1323  // No guarantee that this is the most efficient way to do this...
1324  // We want to count messages that happened for a friend before a
1325  // certain date. We do this by re-joining our table a second time
1326  // but this time with the only filter being that our id is less than
1327  // the ID of the corresponding row in the table that is grouped by day
1328  auto countMessagesForFriend =
1329  QString("SELECT COUNT(*) - 1 " // Count - 1 corresponds to 0 indexed message id for friend
1330  "FROM history countHistory " // Import unfiltered table as countHistory
1331  "JOIN peers chat ON chat_id = chat.id " // link chat_id to chat.id
1332  "WHERE chat.public_key = '%1'" // filter this conversation
1333  "AND countHistory.id <= history.id") // and filter that our unfiltered table history id only has elements up to history.id
1334  .arg(friendPkString);
1335 
1336  auto limitString = (maxNum) ? QString("LIMIT %1").arg(maxNum) : QString("");
1337 
1338  auto queryString = QString("SELECT (%1), (timestamp / 1000 / 60 / 60 / 24) AS day "
1339  "FROM history "
1340  "JOIN peers chat ON chat_id = chat.id "
1341  "WHERE chat.public_key = '%2' "
1342  "AND timestamp >= %3 "
1343  "GROUP by day "
1344  "%4;")
1345  .arg(countMessagesForFriend)
1346  .arg(friendPkString)
1347 #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
1348  .arg(QDateTime(from.startOfDay()).toMSecsSinceEpoch())
1349 #else
1350  .arg(QDateTime(from).toMSecsSinceEpoch())
1351 #endif
1352  .arg(limitString);
1353 
1354  QList<DateIdx> dateIdxs;
1355  auto rowCallback = [&dateIdxs](const QVector<QVariant>& row) {
1356  DateIdx dateIdx;
1357  dateIdx.numMessagesIn = row[0].toLongLong();
1358  dateIdx.date =
1359  QDateTime::fromMSecsSinceEpoch(row[1].toLongLong() * 24 * 60 * 60 * 1000).date();
1360  dateIdxs.append(dateIdx);
1361  };
1362 
1363  db->execNow({queryString, rowCallback});
1364 
1365  return dateIdxs;
1366 }
1367 
1375 {
1376  if (historyAccessBlocked()) {
1377  return;
1378  }
1379 
1380  db->execLater(QString("DELETE FROM faux_offline_pending WHERE id=%1;").arg(messageId.get()));
1381 }
1382 
1388 {
1389  if (!Settings::getInstance().getEnableLogging()) {
1390  assert(false);
1391  qCritical() << "Blocked history access while history is disabled";
1392  return true;
1393  }
1394 
1395  if (!isValid()) {
1396  return true;
1397  }
1398 
1399  return false;
1400 }
1401 
1403 {
1404  if (!isValid()) {
1405  return;
1406  }
1407 
1408  QVector<RawDatabase::Query> queries;
1409  queries += RawDatabase::Query(QString("DELETE FROM faux_offline_pending WHERE id=%1;").arg(messageId.get()));
1410  queries += RawDatabase::Query(QString("INSERT INTO broken_messages (id, reason) "
1411  "VALUES (%1, %2);")
1412  .arg(messageId.get())
1413  .arg(static_cast<int64_t>(reason)));
1414 
1415  db->execLater(queries);
1416 }
profile.h
History::fileInserted
void fileInserted(RowId dbId, QString fileId)
History::setFileFinished
void setFileFinished(const QString &fileId, bool success, const QString &filePath, const QByteArray &fileHash)
Definition: history.cpp:989
RawDatabase::Query
Definition: rawdatabase.h:57
History::isValid
bool isValid()
Checks if the database was opened successfully.
Definition: history.cpp:749
FilterSearch::Register
@ Register
ParameterSearch::date
QDate date
Definition: searchtypes.h:50
history.h
History::~History
~History()
Definition: history.cpp:734
FileDbInsertionData::fileId
QString fileId
Definition: history.h:120
FilterSearch::RegisterAndWordsOnly
@ RegisterAndWordsOnly
settings.h
ParameterSearch::filter
FilterSearch filter
Definition: searchtypes.h:48
SystemMessageType
SystemMessageType
Definition: systemmessage.h:28
PeriodSearch::AfterDate
@ AfterDate
PeriodSearch::BeforeDate
@ BeforeDate
SystemMessage::timestamp
QDateTime timestamp
Definition: systemmessage.h:51
History::markAsBroken
void markAsBroken(RowId messageId, BrokenMessageReason reason)
Definition: history.cpp:1402
ParameterSearch
Definition: searchtypes.h:47
History::generateFileFinished
static RawDatabase::Query generateFileFinished(RowId fileId, bool success, const QString &filePath, const QByteArray &fileHash)
Definition: history.cpp:895
RowId
NamedType< int64_t, struct RowIdTag, Orderable > RowId
Definition: rawdatabase.h:49
History::eraseHistory
void eraseHistory()
Erases all the chat history from the database.
Definition: history.cpp:771
HistMessageContentType::file
@ file
ToxFile::FileStatus
FileStatus
Definition: toxfile.h:36
SystemMessage::args
Args args
Definition: systemmessage.h:52
FileDbInsertionData
Definition: history.h:114
History::removeFriendHistory
void removeFriendHistory(const ToxPk &friendPk)
Erases the chat history with one friend.
Definition: history.cpp:792
History::DateIdx::numMessagesIn
size_t numMessagesIn
Definition: history.h:185
SystemMessage
Definition: systemmessage.h:47
QList
Definition: friendlist.h:25
toxpk.h
History::historyExists
bool historyExists(const ToxPk &friendPk)
Checks if a friend has chat history.
Definition: history.cpp:759
History::historyAccessBlocked
bool historyAccessBlocked()
Determines if history access should be blocked.
Definition: history.cpp:1387
History::addNewSystemMessage
void addNewSystemMessage(const ToxPk &friendPk, const SystemMessage &systemMessage)
Definition: history.cpp:957
History::markAsDelivered
void markAsDelivered(RowId messageId)
Marks a message as delivered. Removing message from the faux-offline pending messages list.
Definition: history.cpp:1374
History::db
std::shared_ptr< RawDatabase > db
Definition: history.h:237
History::onFileInserted
void onFileInserted(RowId dbId, QString fileId)
Definition: history.cpp:833
ToxFile::RECEIVING
@ RECEIVING
Definition: toxfile.h:51
FileDbInsertionData::FileDbInsertionData
FileDbInsertionData()
Definition: history.cpp:700
FileDbInsertionData::size
int64_t size
Definition: history.h:123
History::getNumMessagesForFriendBeforeDate
size_t getNumMessagesForFriendBeforeDate(const ToxPk &friendPk, const QDateTime &date)
Definition: history.cpp:1018
History::getUndeliveredMessagesForFriend
QList< HistMessage > getUndeliveredMessagesForFriend(const ToxPk &friendPk)
Definition: history.cpp:1158
ToxFile::FileDirection
FileDirection
Definition: toxfile.h:48
HistMessageContentType::message
@ message
ParameterSearch::period
PeriodSearch period
Definition: searchtypes.h:49
ToxPk
This class represents a Tox Public Key, which is a part of Tox ID.
Definition: toxpk.h:26
PeriodSearch::WithTheFirst
@ WithTheFirst
History::getNumMessagesForFriendBeforeDateBoundaries
QList< DateIdx > getNumMessagesForFriendBeforeDateBoundaries(const ToxPk &friendPk, const QDate &from, size_t maxNum)
Gets date boundaries in conversation with friendPk. History doesn't model conversation indexes,...
Definition: history.cpp:1313
RawDatabase
Implements a low level RAII interface to a SQLCipher (SQlite3) database.
Definition: rawdatabase.h:52
ToxFile::CANCELED
@ CANCELED
Definition: toxfile.h:42
History::getMessagesForFriend
QList< HistMessage > getMessagesForFriend(const ToxPk &friendPk, size_t firstIdx, size_t lastIdx)
Definition: history.cpp:1046
ToxFile::FINISHED
@ FINISHED
Definition: toxfile.h:43
ToxFile::SENDING
@ SENDING
Definition: toxfile.h:50
History::DateIdx
Definition: history.h:182
SystemMessage::messageType
SystemMessageType messageType
Definition: systemmessage.h:50
History::History
History(std::shared_ptr< RawDatabase > db)
Prepares the database to work with the history.
Definition: history.cpp:710
BrokenMessageReason
BrokenMessageReason
Definition: brokenmessagereason.h:23
Settings::getInstance
static Settings & getInstance()
Returns the singleton instance.
Definition: settings.cpp:88
FilterSearch::WordsOnly
@ WordsOnly
ExtensionSet
std::bitset< ExtensionType::max > ExtensionSet
Definition: extension.h:32
History::fileInfos
QHash< QString, FileInfo > fileInfos
Definition: history.h:250
History::addNewFileMessage
void addNewFileMessage(const ToxPk &friendPk, const QString &fileId, const QString &fileName, const QString &filePath, int64_t size, const ToxPk &sender, const QDateTime &time, QString const &dispName)
Definition: history.cpp:915
History::getNumMessagesForFriend
size_t getNumMessagesForFriend(const ToxPk &friendPk)
Definition: history.cpp:1009
MessageState::complete
@ complete
FileDbInsertionData::filePath
QString filePath
Definition: history.h:122
MessageState
MessageState
Definition: history.h:128
History::addNewMessage
void addNewMessage(const ToxPk &friendPk, const QString &message, const ToxPk &sender, const QDateTime &time, bool isDelivered, ExtensionSet extensions, QString dispName, const std::function< void(RowId)> &insertIdCallback={})
Saves a chat message in the database.
Definition: history.cpp:977
FilterSearch::RegisterAndRegular
@ RegisterAndRegular
FileDbInsertionData::fileName
QString fileName
Definition: history.h:121
ContactId::toString
QString toString() const
Converts the ContactId to a uppercase hex string.
Definition: contactid.cpp:78
SearchExtraFunctions::generateFilterWordsOnly
static QString generateFilterWordsOnly(const QString &phrase)
generateFilterWordsOnly generate string for filter "Whole words only" for correct search phrase conta...
Definition: searchtypes.h:72
RawDatabase::execNow
bool execNow(const QString &statement)
Executes a SQL transaction synchronously.
Definition: rawdatabase.cpp:400
rawdatabase.h
ToxFile
Definition: toxfile.h:32
MessageState::broken
@ broken
FilterSearch::Regular
@ Regular
History::getDateWhereFindPhrase
QDateTime getDateWhereFindPhrase(const ToxPk &friendPk, const QDateTime &from, QString phrase, const ParameterSearch &parameter)
Search phrase in chat messages.
Definition: history.cpp:1211
FileDbInsertionData::direction
int direction
Definition: history.h:124
FileDbInsertionData::friendPk
ToxPk friendPk
Definition: history.h:119
History::generateNewFileTransferQueries
QVector< RawDatabase::Query > generateNewFileTransferQueries(const ToxPk &friendPk, const ToxPk &sender, const QDateTime &time, const QString &dispName, const FileDbInsertionData &insertionData)
Definition: history.cpp:847
History::HistMessage
Definition: history.h:139
MessageState::pending
@ pending
History::DateIdx::date
QDate date
Definition: history.h:184