Improve handling of group story replies

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
automated-signal
2025-12-05 17:49:10 -06:00
committed by GitHub
parent f9927ca9e3
commit 4be697da2a
5 changed files with 165 additions and 9 deletions

View File

@@ -4119,6 +4119,23 @@ export class ConversationModel {
expireTimer = this.get('expireTimer');
}
if (storyId && isGroup(this.attributes)) {
const story = await getMessageById(storyId);
strictAssert(story, 'story being replied to must exist');
strictAssert(
story.expireTimer != null && story.expireTimer > 0,
'story missing expireTimer'
);
strictAssert(
story.expirationStartTimestamp != null &&
story.expirationStartTimestamp > 0,
'story missing expirationStartTimestamp'
);
expireTimer = story.expireTimer;
expirationStartTimestamp = story.expirationStartTimestamp;
}
const recipientMaybeConversations = map(
this.getRecipients({
isStoryReply: storyId !== undefined,

View File

@@ -0,0 +1,32 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { LoggerType } from '../../types/Logging.std.js';
import type { WritableDB } from '../Interface.std.js';
import { sql } from '../util.std.js';
export default function updateToSchemaVersion1580(
db: WritableDB,
logger: LoggerType
): void {
const [query] = sql`
DELETE FROM messages
WHERE id IN (
SELECT messages.id from messages
INNER JOIN conversations ON messages.conversationId = conversations.id
WHERE
conversations.type = 'group'
AND messages.storyId IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM messages AS messages_exists
WHERE messages.storyId = messages_exists.id AND messages_exists.isErased IS NOT 1
)
)
`;
const result = db.prepare(query).run();
if (result.changes > 0) {
logger.warn(
`Deleted ${result.changes} group story replies without matching stories`
);
}
}

View File

@@ -134,6 +134,7 @@ import updateToSchemaVersion1550 from './1550-has-link-preview.std.js';
import updateToSchemaVersion1560 from './1560-pinned-messages.std.js';
import updateToSchemaVersion1561 from './1561-cleanup-polls.std.js';
import updateToSchemaVersion1570 from './1570-pinned-messages-updates.std.js';
import updateToSchemaVersion1580 from './1580-expired-group-replies.std.js';
import { DataWriter } from '../Server.node.js';
@@ -1627,6 +1628,7 @@ export const SCHEMA_VERSIONS: ReadonlyArray<SchemaUpdateType> = [
// 1561, 1551, and 1541 all refer to the same migration
{ version: 1561, update: updateToSchemaVersion1561 },
{ version: 1570, update: updateToSchemaVersion1570 },
{ version: 1580, update: updateToSchemaVersion1580 },
];
export class DBVersionFromFutureError extends Error {

View File

@@ -0,0 +1,108 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { type WritableDB } from '../../sql/Interface.std.js';
import {
createDB,
getTableData,
insertData,
updateToVersion,
} from './helpers.node.js';
describe('SQL/updateToSchemaVersion1580', () => {
let db: WritableDB;
beforeEach(() => {
db = createDB();
updateToVersion(db, 1570);
});
afterEach(() => {
db.close();
});
it('deletes any expired group story replies', () => {
insertData(db, 'conversations', [
{
id: 'groupConvoId',
type: 'group',
},
{
id: 'directConvoId',
type: 'private',
},
]);
insertData(db, 'messages', [
{
id: 'story-that-exists',
type: 'story',
conversationId: 'groupConvoId',
timestamp: Date.now(),
},
{
id: 'doe-story',
type: 'story',
conversationId: 'groupConvoId',
timestamp: Date.now(),
isErased: 1,
},
{
id: 'group-reply-to-existing-story',
conversationId: 'groupConvoId',
timestamp: Date.now(),
storyId: 'story-that-exists',
},
{
id: 'group-reply-to-non-existing-story',
conversationId: 'groupConvoId',
timestamp: Date.now(),
storyId: 'story-that-does-not-exist',
},
{
id: 'group-reply-to-doe-story',
conversationId: 'groupConvoId',
timestamp: Date.now(),
storyId: 'doe-story',
},
{
id: 'direct-reply-to-existing-story',
conversationId: 'directConvoId',
timestamp: Date.now(),
storyId: 'storyThatExists',
},
{
id: 'direct-reply-to-non-existing-story',
conversationId: 'directConvoId',
timestamp: Date.now(),
storyId: 'storyThatDoesNotExist',
},
{
id: 'normal-group-message',
conversationId: 'groupConvoId',
timestamp: Date.now(),
},
{
id: 'normal-direct-message',
conversationId: 'directConvoId',
timestamp: Date.now(),
},
]);
updateToVersion(db, 1580);
assert.deepStrictEqual(
getTableData(db, 'messages').map(row => row.id),
[
'story-that-exists',
'doe-story',
'group-reply-to-existing-story',
// 'group-reply-to-non-existing-story', <-- DELETED!
// 'group-reply-to-doe-story', <-- DELETED!
'direct-reply-to-existing-story',
'direct-reply-to-non-existing-story',
'normal-group-message',
'normal-direct-message',
]
);
});
});

View File

@@ -180,17 +180,14 @@ async function cleanupStoryReplies(
}
if (isGroupConversation) {
// Cleanup all group replies
await Promise.all(
replies.map(reply => {
const replyMessageModel = window.MessageCache.register(
new MessageModel(reply)
);
return eraseMessageContents(replyMessageModel);
})
// Delete all group replies
await DataWriter.removeMessages(
replies.map(reply => reply.id),
{ cleanupMessages }
);
} else {
// Refresh the storyReplyContext data for 1:1 conversations
// Clean out the storyReplyContext data for 1:1 conversations; these remain in the
// 1:1 timeline with a "story not found" message
await Promise.all(
replies.map(async reply => {
const model = window.MessageCache.register(new MessageModel(reply));