Skip to content

Commit

Permalink
Add migration for unread call history messages and fix json.seenStatus
Browse files Browse the repository at this point in the history
Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com>
  • Loading branch information
automated-signal and ayumi-signal committed Mar 5, 2024
1 parent 578d6f4 commit dc95fd9
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 4 deletions.
17 changes: 15 additions & 2 deletions ts/sql/Server.ts
Expand Up @@ -3471,9 +3471,16 @@ async function getCallHistoryUnreadCount(): Promise<number> {

async function markCallHistoryRead(callId: string): Promise<void> {
const db = await getWritableInstance();

const jsonPatch = JSON.stringify({
seenStatus: SeenStatus.Seen,
});

const [query, params] = sql`
UPDATE messages
SET seenStatus = ${SEEN_STATUS_UNSEEN}
SET
seenStatus = ${SEEN_STATUS_UNSEEN}
json = json_patch(json, ${jsonPatch})
WHERE type IS 'call-history'
AND callId IS ${callId}
`;
Expand All @@ -3497,9 +3504,15 @@ async function markAllCallHistoryRead(): Promise<ReadonlyArray<string>> {

const conversationIds = db.prepare(selectQuery).pluck().all(selectParams);

const jsonPatch = JSON.stringify({
seenStatus: SeenStatus.Seen,
});

const [updateQuery, updateParams] = sql`
UPDATE messages
SET seenStatus = ${SEEN_STATUS_SEEN}
SET
seenStatus = ${SEEN_STATUS_SEEN},
json = json_patch(json, ${jsonPatch})
${where};
`;

Expand Down
@@ -0,0 +1,60 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import type { Database } from '@signalapp/better-sqlite3';

import type { LoggerType } from '../../types/Logging';
import { ReadStatus } from '../../messages/MessageReadStatus';
import { SeenStatus } from '../../MessageSeenStatus';
import { strictAssert } from '../../util/assert';
import { sql, sqlConstant } from '../util';

export const version = 1000;

const READ_STATUS_UNREAD = sqlConstant(ReadStatus.Unread);
const READ_STATUS_READ = sqlConstant(ReadStatus.Read);
const SEEN_STATUS_UNSEEN = sqlConstant(SeenStatus.Unseen);

export function updateToSchemaVersion1000(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 1000) {
return;
}

db.transaction(() => {
const [selectQuery] = sql`
SELECT id
FROM messages
WHERE messages.type = 'call-history'
AND messages.readStatus IS ${READ_STATUS_UNREAD}
`;

const rows = db.prepare(selectQuery).all();

for (const row of rows) {
const { id } = row;
strictAssert(id != null, 'message id must exist');

const [updateQuery, updateParams] = sql`
UPDATE messages
SET
json = JSON_PATCH(json, ${JSON.stringify({
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Unseen,
})}),
readStatus = ${READ_STATUS_READ},
seenStatus = ${SEEN_STATUS_UNSEEN}
WHERE id = ${id}
`;

db.prepare(updateQuery).run(updateParams);
}
})();

db.pragma('user_version = 1000');

logger.info('updateToSchemaVersion1000: success!');
}
6 changes: 4 additions & 2 deletions ts/sql/migrations/index.ts
Expand Up @@ -74,10 +74,11 @@ import { updateToSchemaVersion950 } from './950-fts5-secure-delete';
import { updateToSchemaVersion960 } from './960-untag-pni';
import { updateToSchemaVersion970 } from './970-fts5-optimize';
import { updateToSchemaVersion980 } from './980-reaction-timestamp';
import { updateToSchemaVersion990 } from './990-phone-number-sharing';
import {
version as MAX_VERSION,
updateToSchemaVersion990,
} from './990-phone-number-sharing';
updateToSchemaVersion1000,
} from './1000-mark-unread-call-history-messages-as-unseen';

function updateToSchemaVersion1(
currentVersion: number,
Expand Down Expand Up @@ -2019,6 +2020,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion970,
updateToSchemaVersion980,
updateToSchemaVersion990,
updateToSchemaVersion1000,
];

export class DBVersionFromFutureError extends Error {
Expand Down
189 changes: 189 additions & 0 deletions ts/test-node/sql/migration_1000_test.ts
@@ -0,0 +1,189 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import { assert } from 'chai';
import type { Database } from '@signalapp/better-sqlite3';
import SQL from '@signalapp/better-sqlite3';
import { v4 as generateGuid } from 'uuid';

import { jsonToObject, sql } from '../../sql/util';
import { updateToVersion } from './helpers';
import type { MessageType } from '../../sql/Interface';
import { ReadStatus } from '../../messages/MessageReadStatus';
import { SeenStatus } from '../../MessageSeenStatus';

describe('SQL/updateToSchemaVersion1000', () => {
let db: Database;

beforeEach(() => {
db = new SQL(':memory:');
updateToVersion(db, 990);
});

afterEach(() => {
db.close();
});

function createCallHistoryMessage(options: {
messageId: string;
conversationId: string;
callId: string;
readStatus: ReadStatus;
seenStatus: SeenStatus;
}): MessageType {
const message: MessageType = {
id: options.messageId,
type: 'call-history',
conversationId: options.conversationId,
received_at: Date.now(),
sent_at: Date.now(),
received_at_ms: Date.now(),
timestamp: Date.now(),
readStatus: options.readStatus,
seenStatus: options.seenStatus,
callId: options.callId,
};

const json = JSON.stringify(message);

const [query, params] = sql`
INSERT INTO messages
(id, conversationId, type, readStatus, seenStatus, json)
VALUES
(
${message.id},
${message.conversationId},
${message.type},
${message.readStatus},
${message.seenStatus},
${json}
)
`;

db.prepare(query).run(params);

return message;
}

function createConversation(
type: 'private' | 'group',
discoveredUnregisteredAt?: number
) {
const id = generateGuid();
const groupId = type === 'group' ? generateGuid() : null;

const json = JSON.stringify({
type,
id,
groupId,
discoveredUnregisteredAt,
});

const [query, params] = sql`
INSERT INTO conversations
(id, type, groupId, json)
VALUES
(${id}, ${type}, ${groupId}, ${json});
`;

db.prepare(query).run(params);

return { id, groupId };
}

function getMessages() {
const [query] = sql`
SELECT json, readStatus, seenStatus FROM messages;
`;
return db
.prepare(query)
.all()
.map(row => {
return {
message: jsonToObject<MessageType>(row.json),
readStatus: row.readStatus,
seenStatus: row.seenStatus,
};
});
}

it('marks unread call history messages read and unseen', () => {
const conversation1 = createConversation('private');
const conversation2 = createConversation('group');

const callId1 = '1';
const callId2 = '2';

createCallHistoryMessage({
messageId: generateGuid(),
conversationId: conversation1.id,
callId: callId1,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
});

createCallHistoryMessage({
messageId: generateGuid(),
conversationId: conversation2.id,
callId: callId2,
readStatus: ReadStatus.Unread,
seenStatus: SeenStatus.Unseen,
});

updateToVersion(db, 1000);

const messages = getMessages();

assert.strictEqual(messages.length, 2);

assert.strictEqual(messages[0].message.readStatus, ReadStatus.Read);
assert.strictEqual(messages[0].message.seenStatus, SeenStatus.Unseen);
assert.strictEqual(messages[0].readStatus, ReadStatus.Read);
assert.strictEqual(messages[0].seenStatus, SeenStatus.Unseen);

assert.strictEqual(messages[1].message.readStatus, ReadStatus.Read);
assert.strictEqual(messages[1].message.seenStatus, SeenStatus.Unseen);
assert.strictEqual(messages[1].readStatus, ReadStatus.Read);
assert.strictEqual(messages[1].seenStatus, SeenStatus.Unseen);
});

it('does not mark read call history messages as unseen', () => {
const conversation1 = createConversation('private');
const conversation2 = createConversation('group');

const callId1 = '1';
const callId2 = '2';

createCallHistoryMessage({
messageId: generateGuid(),
conversationId: conversation1.id,
callId: callId1,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
});

createCallHistoryMessage({
messageId: generateGuid(),
conversationId: conversation2.id,
callId: callId2,
readStatus: ReadStatus.Read,
seenStatus: SeenStatus.Seen,
});

updateToVersion(db, 1000);

const messages = getMessages();

assert.strictEqual(messages.length, 2);

assert.strictEqual(messages[0].message.readStatus, ReadStatus.Read);
assert.strictEqual(messages[0].message.seenStatus, SeenStatus.Seen);
assert.strictEqual(messages[0].readStatus, ReadStatus.Read);
assert.strictEqual(messages[0].seenStatus, SeenStatus.Seen);

assert.strictEqual(messages[1].message.readStatus, ReadStatus.Read);
assert.strictEqual(messages[1].message.seenStatus, SeenStatus.Seen);
assert.strictEqual(messages[1].readStatus, ReadStatus.Read);
assert.strictEqual(messages[1].seenStatus, SeenStatus.Seen);
});
});

0 comments on commit dc95fd9

Please sign in to comment.