429 lines
12 KiB
Dart
429 lines
12 KiB
Dart
import 'package:get_it/get_it.dart';
|
|
import 'package:moxxyv2/service/database/constants.dart';
|
|
import 'package:moxxyv2/service/xmpp_state.dart';
|
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
extension MaybeGet<K, V> on Map<K, V> {
|
|
V? maybeGet(K? key) {
|
|
if (key == null) return null;
|
|
|
|
return this[key];
|
|
}
|
|
}
|
|
|
|
Future<void> upgradeFromV45ToV46(Database db) async {
|
|
// Migrate everything to the tuple of (account JID, <old pk>)
|
|
// Things we do not migrate to this scheme:
|
|
// - Stickers: Technically, makes no sense
|
|
// - File metadata: We want to aggresively cache, so we keep it
|
|
|
|
// Get the account JID
|
|
final rawJid = await db.query(
|
|
xmppStateTable,
|
|
where: 'key = ?',
|
|
whereArgs: ['jid'],
|
|
limit: 1,
|
|
);
|
|
|
|
// [migrateRows] indicates whether we can move the data to the new JID-annotated format.
|
|
// It's false if we don't have a "logged in" JID. If we have one, it's true and we can
|
|
// move data.
|
|
final migrateRows = rawJid.isNotEmpty;
|
|
final accountJid = migrateRows ? rawJid.first['value']! as String : null;
|
|
|
|
// Store the account JID in the secure storage.
|
|
if (migrateRows) {
|
|
await GetIt.I.get<XmppStateService>().setAccountJid(accountJid!);
|
|
}
|
|
|
|
// Migrate the XMPP state
|
|
await db.execute(
|
|
'''
|
|
CREATE TABLE ${xmppStateTable}_new (
|
|
key TEXT NOT NULL,
|
|
accountJid TEXT NOT NULL,
|
|
value TEXT,
|
|
PRIMARY KEY (key, accountJid)
|
|
)''',
|
|
);
|
|
if (migrateRows) {
|
|
for (final statePair in await db.query(xmppStateTable)) {
|
|
await db.insert(
|
|
'${xmppStateTable}_new',
|
|
{
|
|
...statePair,
|
|
'accountJid': accountJid,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
await db.execute('DROP TABLE $xmppStateTable');
|
|
await db
|
|
.execute('ALTER TABLE ${xmppStateTable}_new RENAME TO $xmppStateTable');
|
|
|
|
// Migrate messages
|
|
await db.execute(
|
|
'''
|
|
CREATE TABLE ${messagesTable}_new (
|
|
id TEXT NOT NULL PRIMARY KEY,
|
|
accountJid TEXT NOT NULL,
|
|
sender TEXT NOT NULL,
|
|
body TEXT,
|
|
timestamp INTEGER NOT NULL,
|
|
sid TEXT NOT NULL,
|
|
conversationJid TEXT NOT NULL,
|
|
isFileUploadNotification INTEGER NOT NULL,
|
|
encrypted INTEGER NOT NULL,
|
|
errorType INTEGER,
|
|
warningType INTEGER,
|
|
received INTEGER,
|
|
displayed INTEGER,
|
|
acked INTEGER,
|
|
originId TEXT,
|
|
quote_id TEXT,
|
|
file_metadata_id TEXT,
|
|
isDownloading INTEGER NOT NULL,
|
|
isUploading INTEGER NOT NULL,
|
|
isRetracted INTEGER,
|
|
isEdited INTEGER NOT NULL,
|
|
containsNoStore INTEGER NOT NULL,
|
|
stickerPackId TEXT,
|
|
pseudoMessageType INTEGER,
|
|
pseudoMessageData TEXT,
|
|
CONSTRAINT fk_quote
|
|
FOREIGN KEY (quote_id)
|
|
REFERENCES $messagesTable (id)
|
|
CONSTRAINT fk_file_metadata
|
|
FOREIGN KEY (file_metadata_id)
|
|
REFERENCES $fileMetadataTable (id)
|
|
)''',
|
|
);
|
|
// Build up the message map
|
|
/// Message's old id attribute -> Message's new UUID attribute.
|
|
const uuid = Uuid();
|
|
final messageMap = <int, String>{};
|
|
|
|
if (migrateRows) {
|
|
final messages = await db.query(messagesTable);
|
|
for (final message in messages) {
|
|
messageMap[message['id']! as int] = uuid.v4();
|
|
}
|
|
// Then migrate messages
|
|
for (final message in messages) {
|
|
await db.insert('${messagesTable}_new', {
|
|
...Map.from(message)
|
|
..remove('id')
|
|
..remove('quote_id'),
|
|
'accountJid': accountJid,
|
|
'quote_id': messageMap.maybeGet(message['quote_id'] as int?),
|
|
'id': messageMap[message['id']! as int],
|
|
});
|
|
}
|
|
}
|
|
await db.execute('DROP TABLE $messagesTable');
|
|
await db.execute('ALTER TABLE ${messagesTable}_new RENAME TO $messagesTable');
|
|
await db.execute(
|
|
'CREATE INDEX idx_messages_sid ON $messagesTable (accountJid, sid)',
|
|
);
|
|
await db.execute(
|
|
'CREATE INDEX idx_messages_origin_sid ON $messagesTable (accountJid, originId, sid)',
|
|
);
|
|
|
|
// Migrate conversations
|
|
await db.execute(
|
|
'''
|
|
CREATE TABLE ${conversationsTable}_new (
|
|
jid TEXT NOT NULL,
|
|
accountJid TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
avatarPath TEXT NOT NULL,
|
|
avatarHash TEXT,
|
|
type TEXT NOT NULL,
|
|
lastChangeTimestamp INTEGER NOT NULL,
|
|
unreadCounter INTEGER NOT NULL,
|
|
open INTEGER NOT NULL,
|
|
muted INTEGER NOT NULL,
|
|
encrypted INTEGER NOT NULL,
|
|
lastMessageId TEXT,
|
|
contactId TEXT,
|
|
contactAvatarPath TEXT,
|
|
contactDisplayName TEXT,
|
|
PRIMARY KEY (jid, accountJid),
|
|
CONSTRAINT fk_last_message
|
|
FOREIGN KEY (lastMessageId)
|
|
REFERENCES $messagesTable (id),
|
|
CONSTRAINT fk_contact_id
|
|
FOREIGN KEY (contactId)
|
|
REFERENCES $contactsTable (id)
|
|
ON DELETE SET NULL
|
|
)''',
|
|
);
|
|
if (migrateRows) {
|
|
for (final conversation in await db.query(conversationsTable)) {
|
|
await db.insert(
|
|
'${conversationsTable}_new',
|
|
{
|
|
...Map.from(conversation)..remove('lastMessageId'),
|
|
'lastMessageId':
|
|
messageMap.maybeGet(conversation['lastMessageId'] as int?),
|
|
'accountJid': accountJid,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
await db.execute('DROP TABLE $conversationsTable');
|
|
await db.execute(
|
|
'ALTER TABLE ${conversationsTable}_new RENAME TO $conversationsTable',
|
|
);
|
|
await db.execute(
|
|
'CREATE INDEX idx_conversation_id ON $conversationsTable (accountJid, jid)',
|
|
);
|
|
|
|
// Migrate groupchat details
|
|
await db.execute(
|
|
'''
|
|
CREATE TABLE ${groupchatTable}_new (
|
|
jid TEXT NOT NULL,
|
|
accountJid TEXT NOT NULL,
|
|
nick TEXT NOT NULL,
|
|
PRIMARY KEY (jid, accountJid),
|
|
CONSTRAINT fk_groupchat
|
|
FOREIGN KEY (jid, accountJid)
|
|
REFERENCES $conversationsTable (jid, accountJid)
|
|
ON DELETE CASCADE
|
|
)''',
|
|
);
|
|
if (migrateRows) {
|
|
for (final g in await db.query(groupchatTable)) {
|
|
await db.insert(
|
|
'${groupchatTable}_new',
|
|
{
|
|
...g,
|
|
'accountJid': accountJid,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
await db.execute('DROP TABLE $groupchatTable');
|
|
await db
|
|
.execute('ALTER TABLE ${groupchatTable}_new RENAME TO $groupchatTable');
|
|
|
|
// Migrate reactions
|
|
await db.execute(
|
|
'''
|
|
CREATE TABLE ${reactionsTable}_new (
|
|
accountJid TEXT NOT NULL,
|
|
message_id TEXT NOT NULL,
|
|
senderJid TEXT NOT NULL,
|
|
emoji TEXT NOT NULL,
|
|
PRIMARY KEY (accountJid, senderJid, emoji, message_id),
|
|
CONSTRAINT fk_message
|
|
FOREIGN KEY (message_id)
|
|
REFERENCES $messagesTable (id)
|
|
ON DELETE CASCADE
|
|
)''',
|
|
);
|
|
if (migrateRows) {
|
|
for (final reaction in await db.query(reactionsTable)) {
|
|
await db.insert(
|
|
'${reactionsTable}_new',
|
|
{
|
|
...Map.from(reaction)..remove('message_id'),
|
|
'message_id': messageMap.maybeGet(reaction['message_id']! as int),
|
|
'accountJid': accountJid,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
await db.execute('DROP TABLE $reactionsTable');
|
|
await db
|
|
.execute('ALTER TABLE ${reactionsTable}_new RENAME TO $reactionsTable');
|
|
|
|
// Migrate the roster
|
|
await db.execute(
|
|
'''
|
|
CREATE TABLE ${rosterTable}_new (
|
|
jid TEXT NOT NULL,
|
|
accountJid TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
avatarPath TEXT NOT NULL,
|
|
avatarHash TEXT NOT NULL,
|
|
subscription TEXT NOT NULL,
|
|
ask TEXT NOT NULL,
|
|
contactId TEXT,
|
|
contactAvatarPath TEXT,
|
|
contactDisplayName TEXT,
|
|
pseudoRosterItem INTEGER NOT NULL,
|
|
CONSTRAINT fk_contact_id
|
|
FOREIGN KEY (contactId)
|
|
REFERENCES $contactsTable (id)
|
|
ON DELETE SET NULL
|
|
)''',
|
|
);
|
|
if (migrateRows) {
|
|
for (final rosterItem in await db.query(rosterTable)) {
|
|
await db.insert(
|
|
'${rosterTable}_new',
|
|
{
|
|
...Map.from(rosterItem)..remove('id'),
|
|
'accountJid': accountJid,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
await db.execute('DROP TABLE $rosterTable');
|
|
await db.execute('ALTER TABLE ${rosterTable}_new RENAME TO $rosterTable');
|
|
|
|
// Migrate the blocklist
|
|
await db.execute(
|
|
'''
|
|
CREATE TABLE ${blocklistTable}_new (
|
|
jid TEXT NOT NULL,
|
|
accountJid TEXT NOT NULL,
|
|
PRIMARY KEY (accountJid, jid)
|
|
);
|
|
''',
|
|
);
|
|
if (migrateRows) {
|
|
for (final blocklistItem in await db.query(blocklistTable)) {
|
|
await db.insert(
|
|
'${blocklistTable}_new',
|
|
{
|
|
...blocklistItem,
|
|
'accountJid': accountJid,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
await db.execute('DROP TABLE $blocklistTable');
|
|
await db
|
|
.execute('ALTER TABLE ${blocklistTable}_new RENAME TO $blocklistTable');
|
|
|
|
// Migrate the notifications list
|
|
await db.execute(
|
|
'''
|
|
CREATE TABLE ${notificationsTable}_new (
|
|
id INTEGER NOT NULL,
|
|
conversationJid TEXT NOT NULL,
|
|
accountJid TEXT NOT NULL,
|
|
sender TEXT,
|
|
senderJid TEXT,
|
|
avatarPath TEXT,
|
|
body TEXT NOT NULL,
|
|
mime TEXT,
|
|
path TEXT,
|
|
timestamp INTEGER NOT NULL,
|
|
PRIMARY KEY (id, conversationJid, senderJid, timestamp, accountJid)
|
|
)''',
|
|
);
|
|
if (migrateRows) {
|
|
for (final notification in await db.query(notificationsTable)) {
|
|
await db.insert(
|
|
'${notificationsTable}_new',
|
|
{
|
|
...notification,
|
|
'accountJid': accountJid,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
await db.execute('DROP TABLE $notificationsTable');
|
|
await db.execute(
|
|
'ALTER TABLE ${notificationsTable}_new RENAME TO $notificationsTable',
|
|
);
|
|
|
|
// Migrate OMEMO device list
|
|
await db.execute(
|
|
'''
|
|
CREATE TABLE ${omemoDeviceListTable}_new (
|
|
jid TEXT NOT NULL,
|
|
accountJid TEXT NOT NULL,
|
|
devices TEXT NOT NULL,
|
|
PRIMARY KEY (accountJid, jid)
|
|
)''',
|
|
);
|
|
{
|
|
for (final deviceListEntry in await db.query(omemoDeviceListTable)) {
|
|
await db.insert(
|
|
'${omemoDeviceListTable}_new',
|
|
{
|
|
...deviceListEntry,
|
|
'accountJid': accountJid,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
await db.execute('DROP TABLE $omemoDeviceListTable');
|
|
await db.execute(
|
|
'ALTER TABLE ${omemoDeviceListTable}_new RENAME TO $omemoDeviceListTable',
|
|
);
|
|
|
|
// Migrate OMEMO trust
|
|
await db.execute(
|
|
'''
|
|
CREATE TABLE ${omemoTrustTable}_new (
|
|
jid TEXT NOT NULL,
|
|
accountJid TEXT NOT NULL,
|
|
device INTEGER NOT NULL,
|
|
trust INTEGER NOT NULL,
|
|
enabled INTEGER NOT NULL,
|
|
PRIMARY KEY (accountJid, jid, device)
|
|
)''',
|
|
);
|
|
if (migrateRows) {
|
|
for (final trustItem in await db.query(omemoTrustTable)) {
|
|
await db.insert(
|
|
'${omemoTrustTable}_new',
|
|
{
|
|
...trustItem,
|
|
'accountJid': accountJid,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
await db.execute('DROP TABLE $omemoTrustTable');
|
|
await db
|
|
.execute('ALTER TABLE ${omemoTrustTable}_new RENAME TO $omemoTrustTable');
|
|
|
|
// Migrate OMEMO ratchets
|
|
await db.execute(
|
|
'''
|
|
CREATE TABLE ${omemoRatchetsTable}_new (
|
|
jid TEXT NOT NULL,
|
|
accountJid TEXT NOT NULL,
|
|
device INTEGER NOT NULL,
|
|
dhsPub TEXT NOT NULL,
|
|
dhs TEXT NOT NULL,
|
|
dhrPub TEXT,
|
|
rk TEXT NOT NULL,
|
|
cks TEXT,
|
|
ckr TEXT,
|
|
ns INTEGER NOT NULL,
|
|
nr INTEGER NOT NULL,
|
|
pn INTEGER NOT NULL,
|
|
ik TEXT NOT NULL,
|
|
ad TEXT NOT NULL,
|
|
skipped TEXT NOT NULL,
|
|
kex TEXT NOT NULL,
|
|
acked INTEGER NOT NULL,
|
|
PRIMARY KEY (accountJid, jid, device)
|
|
)''',
|
|
);
|
|
if (migrateRows) {
|
|
for (final ratchet in await db.query(omemoRatchetsTable)) {
|
|
await db.insert(
|
|
'${omemoRatchetsTable}_new',
|
|
{
|
|
...ratchet,
|
|
'accountJid': accountJid,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
await db.execute('DROP TABLE $omemoRatchetsTable');
|
|
await db.execute(
|
|
'ALTER TABLE ${omemoRatchetsTable}_new RENAME TO $omemoRatchetsTable',
|
|
);
|
|
}
|