session-desktop/ts/node/migration/sessionMigrations.ts

1240 lines
42 KiB
TypeScript

import * as BetterSqlite3 from 'better-sqlite3';
import { compact, map, pick } from 'lodash';
import { ConversationAttributes } from '../../models/conversationAttributes';
import {
CLOSED_GROUP_V2_KEY_PAIRS_TABLE,
CONVERSATIONS_TABLE,
dropFtsAndTriggers,
GUARD_NODE_TABLE,
jsonToObject,
LAST_HASHES_TABLE,
MESSAGES_TABLE,
NODES_FOR_PUBKEY_TABLE,
objectToJSON,
OPEN_GROUP_ROOMS_V2_TABLE,
rebuildFtsTable,
} from '../database_utility';
import { sqlNode } from '../sql';
// tslint:disable: no-console quotemark one-variable-per-declaration
function getSessionSchemaVersion(db: BetterSqlite3.Database) {
const result = db
.prepare(
`
SELECT MAX(version) as version FROM loki_schema;
`
)
.get();
if (!result || !result.version) {
return 0;
}
return result.version;
}
function createSessionSchemaTable(db: BetterSqlite3.Database) {
db.transaction(() => {
db.exec(`
CREATE TABLE loki_schema(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
version INTEGER
);
INSERT INTO loki_schema (
version
) values (
0
);
`);
})();
}
const LOKI_SCHEMA_VERSIONS = [
updateToSessionSchemaVersion1,
updateToSessionSchemaVersion2,
updateToSessionSchemaVersion3,
updateToSessionSchemaVersion4,
updateToSessionSchemaVersion5,
updateToSessionSchemaVersion6,
updateToSessionSchemaVersion7,
updateToSessionSchemaVersion8,
updateToSessionSchemaVersion9,
updateToSessionSchemaVersion10,
updateToSessionSchemaVersion11,
updateToSessionSchemaVersion12,
updateToSessionSchemaVersion13,
updateToSessionSchemaVersion14,
updateToSessionSchemaVersion15,
updateToSessionSchemaVersion16,
updateToSessionSchemaVersion17,
updateToSessionSchemaVersion18,
updateToSessionSchemaVersion19,
updateToSessionSchemaVersion20,
updateToSessionSchemaVersion21,
updateToSessionSchemaVersion22,
updateToSessionSchemaVersion23,
updateToSessionSchemaVersion24,
updateToSessionSchemaVersion25,
updateToSessionSchemaVersion26,
updateToSessionSchemaVersion27,
updateToSessionSchemaVersion28,
updateToSessionSchemaVersion29,
];
function updateToSessionSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 1;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
ALTER TABLE ${MESSAGES_TABLE}
ADD COLUMN serverId INTEGER;
CREATE TABLE servers(
serverUrl STRING PRIMARY KEY ASC,
token TEXT
);
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion2(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 2;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
CREATE TABLE pairingAuthorisations(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
primaryDevicePubKey VARCHAR(255),
secondaryDevicePubKey VARCHAR(255),
isGranted BOOLEAN,
json TEXT,
UNIQUE(primaryDevicePubKey, secondaryDevicePubKey)
);
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion3(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 3;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
CREATE TABLE ${GUARD_NODE_TABLE}(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
ed25519PubKey VARCHAR(64)
);
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion4(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 4;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
DROP TABLE ${LAST_HASHES_TABLE};
CREATE TABLE ${LAST_HASHES_TABLE}(
id TEXT,
snode TEXT,
hash TEXT,
expiresAt INTEGER,
PRIMARY KEY (id, snode)
);
-- Add senderIdentity field to unprocessed needed for medium size groups
ALTER TABLE unprocessed ADD senderIdentity TEXT;
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion5(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 5;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
CREATE TABLE ${NODES_FOR_PUBKEY_TABLE} (
pubkey TEXT PRIMARY KEY,
json TEXT
);
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion6(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 6;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
-- Remove RSS Feed conversations
DELETE FROM ${CONVERSATIONS_TABLE} WHERE
type = 'group' AND
id LIKE 'rss://%';
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion7(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 7;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
-- Remove multi device data
DELETE FROM pairingAuthorisations;
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion8(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 8;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
ALTER TABLE ${MESSAGES_TABLE}
ADD COLUMN serverTimestamp INTEGER;
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion9(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 9;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
const rows = db
.prepare(
`SELECT * FROM ${CONVERSATIONS_TABLE} WHERE
type = 'group' AND
id LIKE '__textsecure_group__!%';
`
)
.all();
const conversationIdRows = db
.prepare(`SELECT id FROM ${CONVERSATIONS_TABLE} ORDER BY id ASC;`)
.all();
const allOldConversationIds = map(conversationIdRows, row => row.id);
rows.forEach(o => {
const oldId = o.id;
const newId = oldId.replace('__textsecure_group__!', '');
console.log(`migrating conversation, ${oldId} to ${newId}`);
if (allOldConversationIds.includes(newId)) {
console.log(
'Found a duplicate conversation after prefix removing. We need to take care of it'
);
// We have another conversation with the same future name.
// We decided to keep only the conversation with the higher number of messages
const countMessagesOld = sqlNode.getMessagesCountByConversation(oldId, db);
const countMessagesNew = sqlNode.getMessagesCountByConversation(newId, db);
console.log(`countMessagesOld: ${countMessagesOld}, countMessagesNew: ${countMessagesNew}`);
const deleteId = countMessagesOld > countMessagesNew ? newId : oldId;
db.prepare(`DELETE FROM ${CONVERSATIONS_TABLE} WHERE id = $deleteId;`).run({ deleteId });
}
const morphedObject = {
...o,
id: newId,
};
db.prepare(
`UPDATE ${CONVERSATIONS_TABLE} SET
id = $newId,
json = $json
WHERE id = $oldId;`
).run({
newId,
json: objectToJSON(morphedObject),
oldId,
});
});
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion10(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 10;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
CREATE TABLE ${CLOSED_GROUP_V2_KEY_PAIRS_TABLE} (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
groupPublicKey TEXT,
timestamp NUMBER,
json TEXT
);
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion11(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 11;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
function remove05PrefixFromStringIfNeeded(str: string) {
if (str.length === 66 && str.startsWith('05')) {
return str.substr(2);
}
return str;
}
db.transaction(() => {
// the migration is called only once, so all current groups not being open groups are v1 closed group.
const allClosedGroupV1Ids = db
.prepare(
`SELECT id FROM ${CONVERSATIONS_TABLE} WHERE
type = 'group' AND
id NOT LIKE 'publicChat:%';`
)
.all()
.map(m => m.id) as Array<string>;
allClosedGroupV1Ids.forEach(groupV1Id => {
try {
console.log('Migrating closed group v1 to v2: pubkey', groupV1Id);
const groupV1IdentityKey = sqlNode.getIdentityKeyById(groupV1Id, db);
if (!groupV1IdentityKey) {
return;
}
const encryptionPubKeyWithoutPrefix = remove05PrefixFromStringIfNeeded(
groupV1IdentityKey.id
);
// Note:
// this is what we get from getIdentityKeyById:
// {
// id: string;
// secretKey?: string;
// }
// and this is what we want saved in db:
// {
// publicHex: string; // without prefix
// privateHex: string;
// }
const keyPair = {
publicHex: encryptionPubKeyWithoutPrefix,
privateHex: groupV1IdentityKey.secretKey,
};
sqlNode.addClosedGroupEncryptionKeyPair(groupV1Id, keyPair, db);
} catch (e) {
console.error(e);
}
});
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion12(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 12;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
CREATE TABLE ${OPEN_GROUP_ROOMS_V2_TABLE} (
serverUrl TEXT NOT NULL,
roomId TEXT NOT NULL,
conversationId TEXT,
json TEXT,
PRIMARY KEY (serverUrl, roomId)
);
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion13(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 13;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
// Clear any already deleted db entries.
// secure_delete = ON will make sure next deleted entries are overwritten with 0 right away
db.transaction(() => {
db.pragma('secure_delete = ON');
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion14(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 14;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
DROP TABLE IF EXISTS servers;
DROP TABLE IF EXISTS sessions;
DROP TABLE IF EXISTS preKeys;
DROP TABLE IF EXISTS contactPreKeys;
DROP TABLE IF EXISTS contactSignedPreKeys;
DROP TABLE IF EXISTS signedPreKeys;
DROP TABLE IF EXISTS senderKeys;
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion15(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 15;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
DROP TABLE pairingAuthorisations;
DROP TRIGGER messages_on_delete;
DROP TRIGGER messages_on_update;
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion16(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 16;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
ALTER TABLE ${MESSAGES_TABLE} ADD COLUMN serverHash TEXT;
ALTER TABLE ${MESSAGES_TABLE} ADD COLUMN isDeleted BOOLEAN;
CREATE INDEX messages_serverHash ON ${MESSAGES_TABLE} (
serverHash
) WHERE serverHash IS NOT NULL;
CREATE INDEX messages_isDeleted ON ${MESSAGES_TABLE} (
isDeleted
) WHERE isDeleted IS NOT NULL;
ALTER TABLE unprocessed ADD serverHash TEXT;
CREATE INDEX messages_messageHash ON unprocessed (
serverHash
) WHERE serverHash IS NOT NULL;
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion17(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 17;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
UPDATE ${CONVERSATIONS_TABLE} SET
json = json_set(json, '$.isApproved', 1)
`);
// remove the moderators field. As it was only used for opengroups a long time ago and whatever is there is probably unused
db.exec(`
UPDATE ${CONVERSATIONS_TABLE} SET
json = json_remove(json, '$.moderators', '$.dataMessage', '$.accessKey', '$.profileSharing', '$.sessionRestoreSeen')
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion18(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 18;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
// Dropping all pre-existing schema relating to message searching.
// Recreating the full text search and related triggers
db.transaction(() => {
dropFtsAndTriggers(db);
rebuildFtsTable(db);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion19(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 19;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
DROP INDEX messages_schemaVersion;
ALTER TABLE ${MESSAGES_TABLE} DROP COLUMN schemaVersion;
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion20(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 20;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
// First we want to drop the column friendRequestStatus if it is there, otherwise the transaction fails
const rows = db.pragma(`table_info(${CONVERSATIONS_TABLE});`);
if (rows.some((m: any) => m.name === 'friendRequestStatus')) {
console.info('found column friendRequestStatus. Dropping it');
db.exec(`ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN friendRequestStatus;`);
}
// looking for all private conversations, with a nickname set
const rowsToUpdate = db
.prepare(
`SELECT * FROM ${CONVERSATIONS_TABLE} WHERE type = 'private' AND (name IS NULL or name = '') AND json_extract(json, '$.nickname') <> '';`
)
.all();
// tslint:disable-next-line: no-void-expression
(rowsToUpdate || []).forEach(r => {
const obj = jsonToObject(r.json);
// obj.profile.displayName is the display as this user set it.
if (obj?.nickname?.length && obj?.profile?.displayName?.length) {
// this one has a nickname set, but name is unset, set it to the displayName in the lokiProfile if it's existing
obj.name = obj.profile.displayName;
sqlNode.saveConversation(obj as ConversationAttributes, db);
}
});
writeSessionSchemaVersion(targetVersion, db);
});
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion21(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 21;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
UPDATE ${CONVERSATIONS_TABLE} SET
json = json_set(json, '$.didApproveMe', 1, '$.isApproved', 1)
WHERE type = 'private';
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion22(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 22;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`DROP INDEX messages_duplicate_check;`);
db.exec(`
ALTER TABLE ${MESSAGES_TABLE} DROP sourceDevice;
`);
db.exec(`
ALTER TABLE unprocessed DROP sourceDevice;
`);
db.exec(`
CREATE INDEX messages_duplicate_check ON ${MESSAGES_TABLE} (
source,
sent_at
);
`);
dropFtsAndTriggers(db);
// we also want to remove the read_by it could have 20 times the same value set in the array
// we do this once, and updated the code to not allow multiple entries in read_by as we do not care about multiple entries
// (read_by is only used in private chats)
db.exec(`
UPDATE ${MESSAGES_TABLE} SET
json = json_remove(json, '$.schemaVersion', '$.recipients', '$.decrypted_at', '$.sourceDevice', '$.read_by')
`);
rebuildFtsTable(db);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion23(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 23;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(
`
ALTER TABLE ${LAST_HASHES_TABLE} RENAME TO ${LAST_HASHES_TABLE}_old;
CREATE TABLE ${LAST_HASHES_TABLE}(
id TEXT,
snode TEXT,
hash TEXT,
expiresAt INTEGER,
namespace INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (id, snode, namespace)
);`
);
db.exec(
`INSERT INTO ${LAST_HASHES_TABLE}(id, snode, hash, expiresAt) SELECT id, snode, hash, expiresAt FROM ${LAST_HASHES_TABLE}_old;`
);
db.exec(`DROP TABLE ${LAST_HASHES_TABLE}_old;`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
// tslint:disable-next-line: max-func-body-length
function updateToSessionSchemaVersion24(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 24;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
// it's unlikely there is still a publicChat v1 convo in the db, but run this in a migration to be 100% sure (previously, run on app start instead)
db.prepare(
`DELETE FROM ${CONVERSATIONS_TABLE} WHERE
type = 'group' AND
id LIKE 'publicChat:1@%';`
).run();
db.exec(`
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN zombies TEXT DEFAULT "[]";
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN left INTEGER;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN expireTimer INTEGER;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN mentionedUs INTEGER;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN unreadCount INTEGER;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN lastMessageStatus TEXT;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN lastMessage TEXT;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN lastJoinedTimestamp INTEGER;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN groupAdmins TEXT DEFAULT "[]";
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN isKickedFromGroup INTEGER;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN subscriberCount INTEGER;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN is_medium_group INTEGER;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN avatarPointer TEXT; -- this is the url of the avatar for that conversation
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN avatarHash TEXT; -- only used for opengroup avatar.
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN nickname TEXT;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN profileKey TEXT;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN triggerNotificationsFor TEXT DEFAULT "all";
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN isTrustedForAttachmentDownload INTEGER DEFAULT "FALSE";
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN isPinned INTEGER DEFAULT "FALSE";
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN isApproved INTEGER DEFAULT "FALSE";
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN didApproveMe INTEGER DEFAULT "FALSE";
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN avatarInProfile TEXT;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN avatarPathInAvatar TEXT; -- this is very temporary, removed right below
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN displayNameInProfile TEXT;
UPDATE ${CONVERSATIONS_TABLE} SET
zombies = json_extract(json, '$.zombies'),
members = json_extract(json, '$.members'),
left = json_extract(json, '$.left'),
expireTimer = json_extract(json, '$.expireTimer'),
mentionedUs = json_extract(json, '$.mentionedUs'),
unreadCount = json_extract(json, '$.unreadCount'),
lastMessageStatus = json_extract(json, '$.lastMessageStatus'),
lastMessage = json_extract(json, '$.lastMessage'),
lastJoinedTimestamp = json_extract(json, '$.lastJoinedTimestamp'),
groupAdmins = json_extract(json, '$.groupAdmins'),
isKickedFromGroup = json_extract(json, '$.isKickedFromGroup'),
subscriberCount = json_extract(json, '$.subscriberCount'),
is_medium_group = json_extract(json, '$.is_medium_group'),
avatarPointer = json_extract(json, '$.avatarPointer'),
avatarHash = json_extract(json, '$.avatarHash'),
nickname = json_extract(json, '$.nickname'),
profileKey = json_extract(json, '$.profileKey'),
triggerNotificationsFor = json_extract(json, '$.triggerNotificationsFor'),
isTrustedForAttachmentDownload = json_extract(json, '$.isTrustedForAttachmentDownload'),
isPinned = json_extract(json, '$.isPinned'),
isApproved = json_extract(json, '$.isApproved'),
didApproveMe = json_extract(json, '$.didApproveMe'),
avatarInProfile = json_extract(json, '$.profile.avatar'),-- profile.avatar is no longer used. We rely on avatarInProfile only (for private chats and opengroups )
avatarPathInAvatar = json_extract(json, '$.avatar.path'),-- this is very temporary
displayNameInProfile = json_extract(json, '$.profile.displayName');
UPDATE ${CONVERSATIONS_TABLE} SET json = json_remove(json,
'$.zombies',
'$.members',
'$.left',
'$.expireTimer',
'$.mentionedUs',
'$.unreadCount',
'$.lastMessageStatus',
'$.lastJoinedTimestamp',
'$.lastMessage',
'$.groupAdmins',
'$.isKickedFromGroup',
'$.subscriberCount',
'$.is_medium_group',
'$.avatarPointer',
'$.avatarHash',
'$.nickname',
'$.profileKey',
'$.triggerNotificationsFor',
'$.isTrustedForAttachmentDownload',
'$.isPinned',
'$.isApproved',
'$.type',
'$.version',
'$.isMe',
'$.didApproveMe',
'$.active_at',
'$.id',
'$.moderators',
'$.sessionRestoreSeen',
'$.profileName',
'$.timestamp',
'$.profile',
'$.name',
'$.profileAvatar',
'$.avatarPath
');
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN json;
UPDATE ${CONVERSATIONS_TABLE} SET displayNameInProfile = name WHERE
type = 'group' AND
id NOT LIKE 'publicChat:%';
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN profileName;
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN name;
-- we want to rely on avatarInProfile only, but it can be set either in avatarInProfile or in avatarPathInAvatar.
-- make sure to override avatarInProfile with the value from avatarPathInAvatar if avatarInProfile is unset
UPDATE ${CONVERSATIONS_TABLE} SET avatarInProfile = avatarPathInAvatar WHERE avatarInProfile IS NULL;
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN avatarPathInAvatar;
CREATE INDEX conversation_nickname ON ${CONVERSATIONS_TABLE} (
nickname
);
CREATE INDEX conversation_displayNameInProfile ON ${CONVERSATIONS_TABLE} (
displayNameInProfile
);
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion25(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 25;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
// mark all conversation as read/write/upload capability to be true on migration.
// the next batch poll will update them if needed
db.exec(`
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN readCapability INTEGER DEFAULT 1;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN writeCapability INTEGER DEFAULT 1;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN uploadCapability INTEGER DEFAULT 1;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN conversationIdOrigin TEXT;
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN avatarHash;
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN avatarImageId INTEGER;
CREATE INDEX messages_convo_serverID ON ${MESSAGES_TABLE} (
serverId,
conversationId
);
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion26(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 26;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
db.exec(`
ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN groupModerators TEXT DEFAULT "[]"; -- those are for sogs only (for closed groups we only need the groupAdmins)
`);
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function getRoomIdFromConversationAttributes(attributes?: ConversationAttributes | null) {
if (!attributes) {
return null;
}
const indexSemiColon = attributes.id.indexOf(':');
const indexAt = attributes.id.indexOf('@');
if (indexSemiColon < 0 || indexAt < 0 || indexSemiColon >= indexAt) {
return null;
}
const roomId = attributes.id.substring(indexSemiColon, indexAt);
if (roomId.length <= 0) {
return null;
}
return roomId;
}
function updateToSessionSchemaVersion27(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 27;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
const domainNameToUse = 'open.getsession.org';
const urlToUse = `https://${domainNameToUse}`;
const ipToRemove = '116.203.70.33';
// defining this function here as this is very specific to this migration and used in a few places
function getNewConvoId(oldConvoId?: string) {
if (!oldConvoId) {
return null;
}
return (
oldConvoId
?.replace(`https://${ipToRemove}`, urlToUse)
// tslint:disable-next-line: no-http-string
?.replace(`http://${ipToRemove}`, urlToUse)
?.replace(ipToRemove, urlToUse)
);
}
getNewConvoId('test');
// tslint:disable-next-line: max-func-body-length
db.transaction(() => {
// First we want to drop the column friendRequestStatus if it is there, otherwise the transaction fails
const rows = db.pragma(`table_info(${CONVERSATIONS_TABLE});`);
if (rows.some((m: any) => m.name === 'friendRequestStatus')) {
console.info('found column friendRequestStatus. Dropping it');
db.exec(`ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN friendRequestStatus;`);
}
// We want to replace all the occurrences of the sogs server ip url (116.203.70.33 || http://116.203.70.33 || https://116.203.70.33) by its hostname: https://open.getsession.org
// This includes change the conversationTable, the openGroupRooms tables and every single message associated with them.
// Because the conversationId is used to link messages to conversation includes the ip/url in it...
/**
* First, remove duplicates for the v2 opengroup table, and replace the one without duplicates with their dns name syntax
*/
// rooms to rename are: crypto, lokinet, oxen, session, session-updates
const allSessionV2RoomsIp = sqlNode
.getAllV2OpenGroupRooms(db)
.filter(m => m.serverUrl.includes(ipToRemove));
const allSessionV2RoomsDns = sqlNode
.getAllV2OpenGroupRooms(db)
.filter(m => m.serverUrl.includes(domainNameToUse));
const duplicatesRoomsIpAndDns = allSessionV2RoomsIp.filter(ip =>
allSessionV2RoomsDns.some(dns => dns.roomId === ip.roomId)
);
const withIpButNotDuplicateRoom = allSessionV2RoomsIp.filter(ip => {
return !duplicatesRoomsIpAndDns.some(dns => dns.roomId === ip.roomId);
});
console.info(
'allSessionV2RoomsIp',
allSessionV2RoomsIp.map(m => pick(m, ['serverUrl', 'roomId']))
);
console.info(
'allSessionV2RoomsDns',
allSessionV2RoomsDns.map(m => pick(m, ['serverUrl', 'roomId']))
);
console.info(
'duplicatesRoomsIpAndDns',
duplicatesRoomsIpAndDns.map(m => pick(m, ['serverUrl', 'roomId']))
);
console.info(
'withIpButNotDuplicateRoom',
withIpButNotDuplicateRoom.map(m => pick(m, ['serverUrl', 'roomId']))
);
console.info(
'========> before room update:',
sqlNode
.getAllV2OpenGroupRooms(db)
.filter(m => m.serverUrl.includes(domainNameToUse) || m.serverUrl.includes(ipToRemove))
.map(m => pick(m, ['conversationId', 'serverUrl', 'roomId']))
);
// for those with duplicates, delete the one with the IP as we want to rely on the one with the DNS only now
// remove the ip ones completely which are duplicated.
// Note: this also removes the ones not duplicated, but we are recreating them just below with `saveV2OpenGroupRoom`
db.exec(`DELETE FROM ${OPEN_GROUP_ROOMS_V2_TABLE} WHERE serverUrl LIKE '%${ipToRemove}%';`);
// for those without duplicates, override the value with the Domain Name
withIpButNotDuplicateRoom.forEach(r => {
const newConvoId = getNewConvoId(r.conversationId);
if (!newConvoId) {
return;
}
console.info(
`withIpButNotDuplicateRoom: renaming room old:${r.conversationId} with saveV2OpenGroupRoom() new- conversationId:${newConvoId}: serverUrl:${urlToUse}`
);
sqlNode.saveV2OpenGroupRoom(
{
...r,
serverUrl: urlToUse,
conversationId: newConvoId,
},
db
);
});
console.info(
'<======== after room update:',
sqlNode
.getAllV2OpenGroupRooms(db)
.filter(m => m.serverUrl.includes(domainNameToUse) || m.serverUrl.includes(ipToRemove))
.map(m => pick(m, ['conversationId', 'serverUrl', 'roomId']))
);
/**
* Then, update the conversations table by doing the same thing
*/
const allSessionV2ConvosIp = compact(
sqlNode.getAllOpenGroupV2Conversations(db).filter(m => m?.id.includes(ipToRemove))
);
const allSessionV2ConvosDns = compact(
sqlNode.getAllOpenGroupV2Conversations(db).filter(m => m?.id.includes(domainNameToUse))
);
const duplicatesConvosIpAndDns = allSessionV2ConvosIp.filter(ip => {
const roomId = getRoomIdFromConversationAttributes(ip);
if (!roomId) {
return false;
}
return allSessionV2ConvosDns.some(dns => {
return getRoomIdFromConversationAttributes(dns) === roomId;
});
});
const withIpButNotDuplicateConvo = allSessionV2ConvosIp.filter(ip => {
const roomId = getRoomIdFromConversationAttributes(ip);
if (!roomId) {
return false;
}
return !allSessionV2ConvosDns.some(dns => {
return getRoomIdFromConversationAttributes(dns) === roomId;
});
});
console.info('========================================');
console.info(
'allSessionV2ConvosIp',
allSessionV2ConvosIp.map(m => m?.id)
);
console.info(
'allSessionV2ConvosDns',
allSessionV2ConvosDns.map(m => m?.id)
);
console.info(
'duplicatesConvosIpAndDns',
duplicatesConvosIpAndDns.map(m => m?.id)
);
console.info(
'withIpButNotDuplicateConvo',
withIpButNotDuplicateConvo.map(m => m?.id)
);
// for those with duplicates, delete the one with the IP as we want to rely on the one with the DNS only now
// remove the ip ones completely which are duplicated.
// Note: this also removes the ones not duplicated, but we are recreating them just below with `saveConversation`
db.exec(`DELETE FROM ${CONVERSATIONS_TABLE} WHERE id LIKE '%${ipToRemove}%';`);
// for those without duplicates, override the value with the DNS
const convoIdsToMigrateFromIpToDns: Map<string, string> = new Map();
withIpButNotDuplicateConvo.forEach(r => {
if (!r) {
return;
}
const newConvoId = getNewConvoId(r.id);
if (!newConvoId) {
return;
}
console.info(
`withIpButNotDuplicateConvo: renaming convo old:${r.id} with saveConversation() new- conversationId:${newConvoId}`
);
convoIdsToMigrateFromIpToDns.set(r.id, newConvoId);
sqlNode.saveConversation(
{
...r,
id: newConvoId,
},
db
);
});
console.info(
'after convos update:',
sqlNode
.getAllOpenGroupV2Conversations(db)
.filter(m => m?.id.includes(domainNameToUse) || m?.id.includes(ipToRemove))
.map(m => m?.id)
);
/**
* Lastly, we need to take care of messages.
* For duplicated rooms, we drop all the messages from the IP one. (Otherwise we would need to compare each message id to not break the PRIMARY_KEY on the messageID and those are just sogs messages).
* For non duplicated rooms which got renamed to their dns ID, we override the stored conversationId in the message with the new conversationID
*/
dropFtsAndTriggers(db);
// let's start with the non duplicateD ones, as doing so will make the duplicated one process easier
console.info('convoIdsToMigrateFromIpToDns', [...convoIdsToMigrateFromIpToDns.entries()]);
[...convoIdsToMigrateFromIpToDns.keys()].forEach(oldConvoId => {
const newConvoId = convoIdsToMigrateFromIpToDns.get(oldConvoId);
if (!newConvoId) {
return;
}
console.info(`About to migrate messages of ${oldConvoId} to ${newConvoId}`);
db.prepare(
`UPDATE ${MESSAGES_TABLE} SET
conversationId = $newConvoId,
json = json_set(json,'$.conversationId', $newConvoId)
WHERE conversationId = $oldConvoId;`
).run({ oldConvoId, newConvoId });
});
// now, the duplicated ones. We just need to move every message with a convoId matching that ip, because we already took care of the one to migrate to the dns before
console.log(
'Count of messages to be migrated: ',
db
.prepare(
`SELECT COUNT(*) FROM ${MESSAGES_TABLE} WHERE conversationId LIKE '%${ipToRemove}%';`
)
.get()
);
const messageWithIdsToUpdate = db
.prepare(
`SELECT DISTINCT conversationId FROM ${MESSAGES_TABLE} WHERE conversationID LIKE '%${ipToRemove}%'`
)
.all();
console.info('messageWithConversationIdsToUpdate', messageWithIdsToUpdate);
messageWithIdsToUpdate.forEach(oldConvo => {
const newConvoId = getNewConvoId(oldConvo.conversationId);
if (!newConvoId) {
return;
}
console.info('oldConvo.conversationId', oldConvo.conversationId, newConvoId);
db.prepare(
`UPDATE ${MESSAGES_TABLE} SET
conversationId = $newConvoId,
json = json_set(json,'$.conversationId', $newConvoId)
WHERE conversationId = $oldConvoId;`
).run({ oldConvoId: oldConvo.conversationId, newConvoId });
});
rebuildFtsTable(db);
console.info(
'removing lastMessageDeletedServerID & lastMessageFetchedServerID from rooms table'
);
db.exec(
`UPDATE ${OPEN_GROUP_ROOMS_V2_TABLE} SET
json = json_remove(json, '$.lastMessageDeletedServerID', '$.lastMessageFetchedServerID', '$.token' );`
);
console.info(
'removing lastMessageDeletedServerID & lastMessageFetchedServerID from rooms table. done'
);
writeSessionSchemaVersion(targetVersion, db);
console.log('... done');
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion28(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 28;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
// Keeping this empty migration because some people updated to this already, even if it is not needed anymore
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
function updateToSessionSchemaVersion29(currentVersion: number, db: BetterSqlite3.Database) {
const targetVersion = 29;
if (currentVersion >= targetVersion) {
return;
}
console.log(`updateToSessionSchemaVersion${targetVersion}: starting...`);
db.transaction(() => {
dropFtsAndTriggers(db);
db.exec(`CREATE INDEX messages_unread_by_conversation ON ${MESSAGES_TABLE} (
unread,
conversationId
);`);
rebuildFtsTable(db);
// Keeping this empty migration because some people updated to this already, even if it is not needed anymore
writeSessionSchemaVersion(targetVersion, db);
})();
console.log(`updateToSessionSchemaVersion${targetVersion}: success!`);
}
// function printTableColumns(table: string, db: BetterSqlite3.Database) {
// console.info(db.pragma(`table_info('${table}');`));
// }
function writeSessionSchemaVersion(newVersion: number, db: BetterSqlite3.Database) {
db.prepare(
`INSERT INTO loki_schema(
version
) values (
$newVersion
)`
).run({ newVersion });
}
export function updateSessionSchema(db: BetterSqlite3.Database) {
const result = db
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema';`)
.get();
if (!result) {
createSessionSchemaTable(db);
}
const lokiSchemaVersion = getSessionSchemaVersion(db);
console.log(
'updateSessionSchema:',
`Current loki schema version: ${lokiSchemaVersion};`,
`Most recent schema version: ${LOKI_SCHEMA_VERSIONS.length};`
);
for (let index = 0, max = LOKI_SCHEMA_VERSIONS.length; index < max; index += 1) {
const runSchemaUpdate = LOKI_SCHEMA_VERSIONS[index];
runSchemaUpdate(lokiSchemaVersion, db);
}
}