Introduce infrastructure for Notification Profiles

This commit is contained in:
Scott Nonnenberg
2025-05-06 00:39:04 +10:00
committed by GitHub
parent 80872ef15c
commit 98270316c5
35 changed files with 2917 additions and 38 deletions

View File

@@ -45,6 +45,7 @@ import type {
import type { SyncTaskType } from '../util/syncTasks';
import type { AttachmentBackupJobType } from '../types/AttachmentBackup';
import type { GifType } from '../components/fun/panels/FunPanelGifs';
import type { NotificationProfileType } from '../types/NotificationProfile';
export type ReadableDB = Database & { __readable_db: never };
export type WritableDB = ReadableDB & { __writable_db: never };
@@ -726,6 +727,9 @@ type ReadableInterface = {
}): Array<StoryReadType>;
countStoryReadsByConversation(conversationId: string): number;
getAllNotificationProfiles(): Array<NotificationProfileType>;
getNotificationProfileById(id: string): NotificationProfileType | undefined;
getMessagesNeedingUpgrade: (
limit: number,
options: { maxVersion: number }
@@ -1020,6 +1024,12 @@ type WritableInterface = {
_deleteAllStoryReads(): void;
addNewStoryRead(read: StoryReadType): void;
_deleteAllNotificationProfiles(): void;
deleteNotificationProfileById(id: string): void;
markNotificationProfileDeleted(id: string): number | undefined;
createNotificationProfile(profile: NotificationProfileType): void;
updateNotificationProfile(profile: NotificationProfileType): void;
removeAll: () => void;
removeAllConfiguration: () => void;
eraseStorageServiceState: () => void;

View File

@@ -211,8 +211,9 @@ import {
getGroupSendMemberEndorsement,
replaceAllEndorsementsForGroup,
} from './server/groupSendEndorsements';
import type { GifType } from '../components/fun/panels/FunPanelGifs';
import { INITIAL_EXPIRE_TIMER_VERSION } from '../util/expirationTimer';
import type { GifType } from '../components/fun/panels/FunPanelGifs';
import type { NotificationProfileType } from '../types/NotificationProfile';
type ConversationRow = Readonly<{
json: string;
@@ -363,6 +364,9 @@ export const DataReader: ServerReadableInterface = {
getCallHistoryGroups,
hasGroupCallHistoryMessage,
getAllNotificationProfiles,
getNotificationProfileById,
callLinkExists,
defunctCallLinkExists,
getAllCallLinks,
@@ -586,6 +590,12 @@ export const DataWriter: ServerWritableInterface = {
_deleteAllStoryReads,
addNewStoryRead,
_deleteAllNotificationProfiles,
createNotificationProfile,
deleteNotificationProfileById,
markNotificationProfileDeleted,
updateNotificationProfile,
removeAll,
removeAllConfiguration,
eraseStorageServiceState,
@@ -6859,6 +6869,224 @@ function countStoryReadsByConversation(
);
}
type NotificationProfileForDatabase = Readonly<
{
emoji: string | null;
allowAllCalls: 0 | 1;
allowAllMentions: 0 | 1;
scheduleEnabled: 0 | 1;
allowedMembersJson: string | null;
scheduleStartTime: number | null;
scheduleEndTime: number | null;
scheduleDaysEnabledJson: string | null;
deletedAtTimestampMs: number | null;
storageID: string | null;
storageVersion: number | null;
storageNeedsSync: 0 | 1;
} & Omit<
NotificationProfileType,
| 'emoji'
| 'allowAllCalls'
| 'allowAllMentions'
| 'scheduleEnabled'
| 'allowedMembers'
| 'scheduleStartTime'
| 'scheduleEndTime'
| 'scheduleDaysEnabled'
| 'deletedAtTimestampMs'
| 'storageID'
| 'storageVersion'
| 'storageNeedsSync'
>
>;
function hydrateNotificationProfile(
profile: NotificationProfileForDatabase
): NotificationProfileType {
return {
...omit(profile, ['allowedMembersJson', 'scheduleDaysEnabledJson']),
emoji: profile.emoji || undefined,
allowAllCalls: Boolean(profile.allowAllCalls),
allowAllMentions: Boolean(profile.allowAllMentions),
scheduleEnabled: Boolean(profile.scheduleEnabled),
allowedMembers: profile.allowedMembersJson
? new Set(JSON.parse(profile.allowedMembersJson))
: new Set(),
scheduleStartTime: profile.scheduleStartTime || undefined,
scheduleEndTime: profile.scheduleEndTime || undefined,
scheduleDaysEnabled: profile.scheduleDaysEnabledJson
? JSON.parse(profile.scheduleDaysEnabledJson)
: undefined,
deletedAtTimestampMs: profile.deletedAtTimestampMs || undefined,
storageID: profile.storageID || undefined,
storageVersion: profile.storageVersion || undefined,
storageNeedsSync: Boolean(profile.storageNeedsSync),
storageUnknownFields: profile.storageUnknownFields || undefined,
};
}
function freezeNotificationProfile(
profile: NotificationProfileType
): NotificationProfileForDatabase {
return {
...omit(profile, ['allowedMembers', 'scheduleDaysEnabled']),
emoji: profile.emoji || null,
allowAllCalls: profile.allowAllCalls ? 1 : 0,
allowAllMentions: profile.allowAllMentions ? 1 : 0,
scheduleEnabled: profile.scheduleEnabled ? 1 : 0,
allowedMembersJson: profile.allowedMembers
? JSON.stringify(Array.from(profile.allowedMembers))
: null,
scheduleStartTime: profile.scheduleStartTime || null,
scheduleEndTime: profile.scheduleEndTime || null,
scheduleDaysEnabledJson: profile.scheduleDaysEnabled
? JSON.stringify(profile.scheduleDaysEnabled)
: null,
deletedAtTimestampMs: profile.deletedAtTimestampMs || null,
storageID: profile.storageID || null,
storageVersion: profile.storageVersion || null,
storageNeedsSync: profile.storageNeedsSync ? 1 : 0,
storageUnknownFields: profile.storageUnknownFields || null,
};
}
function getAllNotificationProfiles(
db: ReadableDB
): Array<NotificationProfileType> {
const notificationProfiles = db
.prepare('SELECT * FROM notificationProfiles ORDER BY createdAtMs DESC;')
.all<NotificationProfileForDatabase>();
return notificationProfiles.map(hydrateNotificationProfile);
}
function getNotificationProfileById(
db: ReadableDB,
id: string
): NotificationProfileType | undefined {
const [query, parameters] =
sql`SELECT * FROM notificationProfiles WHERE id = ${id}`;
const fromDatabase = db
.prepare(query)
.get<NotificationProfileForDatabase>(parameters);
if (fromDatabase) {
return hydrateNotificationProfile(fromDatabase);
}
return undefined;
}
function _deleteAllNotificationProfiles(db: WritableDB): void {
db.prepare('DELETE FROM notificationProfiles;').run();
}
function deleteNotificationProfileById(db: WritableDB, id: string): void {
const [query, parameters] =
sql`DELETE FROM notificationProfiles WHERE id = ${id}`;
db.prepare(query).run(parameters);
}
function markNotificationProfileDeleted(
db: WritableDB,
id: string
): number | undefined {
const now = new Date().getTime();
const [query, parameters] = sql`
UPDATE notificationProfiles
SET deletedAtTimestampMs = ${now}
WHERE
id = ${id} AND
deletedAtTimestampMs IS NULL
RETURNING deletedAtTimestampMs`;
const record = db
.prepare(query)
.get<{ deletedAtTimestampMs: number }>(parameters);
return record?.deletedAtTimestampMs;
}
function createNotificationProfile(
db: WritableDB,
profile: NotificationProfileType
): void {
strictAssert(profile.name, 'Notification profile does not have a valid name');
const forDatabase = freezeNotificationProfile(profile);
db.prepare(
`
INSERT INTO notificationProfiles(
id,
name,
emoji,
color,
createdAtMs,
allowAllCalls,
allowAllMentions,
allowedMembersJson,
scheduleEnabled,
scheduleStartTime,
scheduleEndTime,
scheduleDaysEnabledJson,
deletedAtTimestampMs,
storageID,
storageVersion,
storageUnknownFields,
storageNeedsSync
) VALUES (
$id,
$name,
$emoji,
$color,
$createdAtMs,
$allowAllCalls,
$allowAllMentions,
$allowedMembersJson,
$scheduleEnabled,
$scheduleStartTime,
$scheduleEndTime,
$scheduleDaysEnabledJson,
$deletedAtTimestampMs,
$storageID,
$storageVersion,
$storageUnknownFields,
$storageNeedsSync
);
`
).run(forDatabase);
}
function updateNotificationProfile(
db: WritableDB,
profile: NotificationProfileType
): void {
strictAssert(profile.name, 'Notification profile does not have a valid name');
db.transaction(() => {
const forDatabase = freezeNotificationProfile(profile);
db.prepare(
`
UPDATE notificationProfiles SET
name = $name,
emoji = $emoji,
color = $color,
createdAtMs = $createdAtMs,
allowAllCalls = $allowAllCalls,
allowAllMentions = $allowAllMentions,
allowedMembersJson = $allowedMembersJson,
scheduleEnabled = $scheduleEnabled,
scheduleStartTime = $scheduleStartTime,
scheduleEndTime = $scheduleEndTime,
scheduleDaysEnabledJson = $scheduleDaysEnabledJson,
deletedAtTimestampMs = $deletedAtTimestampMs,
storageID = $storageID,
storageVersion = $storageVersion,
storageUnknownFields = $storageUnknownFields,
storageNeedsSync = $storageNeedsSync
WHERE
id = $id;
`
).run(forDatabase);
})();
}
// All data in database
function removeAll(db: WritableDB): void {
db.transaction(() => {
@@ -6885,6 +7113,7 @@ function removeAll(db: WritableDB): void {
DELETE FROM kyberPreKeys;
DELETE FROM messages_fts;
DELETE FROM messages;
DELETE FROM notificationProfiles;
DELETE FROM preKeys;
DELETE FROM reactions;
DELETE FROM senderKeys;

View File

@@ -0,0 +1,58 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from '@signalapp/sqlcipher';
import type { LoggerType } from '../../types/Logging';
import { sql } from '../util';
export const version = 1350;
export function updateToSchemaVersion1350(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 1350) {
return;
}
db.transaction(() => {
const [query] = sql`
CREATE TABLE notificationProfiles(
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
emoji TEXT,
/* A numeric representation of a color, like 0xAARRGGBB */
color INTEGER NOT NULL,
createdAtMs INTEGER NOT NULL,
allowAllCalls INTEGER NOT NULL,
allowAllMentions INTEGER NOT NULL,
/* A JSON array of conversationId strings */
allowedMembersJson TEXT NOT NULL,
scheduleEnabled INTEGER NOT NULL,
/* 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345) */
scheduleStartTime INTEGER,
scheduleEndTime INTEGER,
/* A JSON object with true/false for each of the numbers in the Protobuf enum */
scheduleDaysEnabledJson TEXT,
deletedAtTimestampMs INTEGER,
storageID TEXT,
storageVersion INTEGER,
storageUnknownFields BLOB,
storageNeedsSync INTEGER NOT NULL DEFAULT 0
) STRICT;
`;
db.exec(query);
db.pragma('user_version = 1350');
})();
logger.info('updateToSchemaVersion1350: success!');
}

View File

@@ -109,10 +109,11 @@ import { updateToSchemaVersion1300 } from './1300-sticker-pack-refs';
import { updateToSchemaVersion1310 } from './1310-muted-fixup';
import { updateToSchemaVersion1320 } from './1320-unprocessed-received-at-date';
import { updateToSchemaVersion1330 } from './1330-sync-tasks-type-index';
import { updateToSchemaVersion1340 } from './1340-recent-gifs';
import {
updateToSchemaVersion1340,
updateToSchemaVersion1350,
version as MAX_VERSION,
} from './1340-recent-gifs';
} from './1350-notification-profiles';
import { DataWriter } from '../Server';
@@ -2100,6 +2101,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion1320,
updateToSchemaVersion1330,
updateToSchemaVersion1340,
updateToSchemaVersion1350,
];
export class DBVersionFromFutureError extends Error {