mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-01 22:01:49 +01:00
Introduce infrastructure for Notification Profiles
This commit is contained in:
@@ -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;
|
||||
|
||||
231
ts/sql/Server.ts
231
ts/sql/Server.ts
@@ -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;
|
||||
|
||||
58
ts/sql/migrations/1350-notification-profiles.ts
Normal file
58
ts/sql/migrations/1350-notification-profiles.ts
Normal 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!');
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user