Stickers in storage service

This commit is contained in:
Fedor Indutny 2022-08-03 10:10:49 -07:00 committed by GitHub
parent d8a7e99c81
commit b47a906211
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1216 additions and 80 deletions

View file

@ -84,6 +84,7 @@ import type {
SignedPreKeyType,
StoredSignedPreKeyType,
StickerPackStatusType,
StickerPackInfoType,
StickerPackType,
StickerType,
StoryDistributionMemberType,
@ -92,6 +93,7 @@ import type {
StoryReadType,
UnprocessedType,
UnprocessedUpdateType,
UninstalledStickerPackType,
} from './Interface';
import Server from './Server';
import { isCorruptionError } from './errors';
@ -277,6 +279,7 @@ const dataInterface: ClientInterface = {
createOrUpdateStickerPack,
updateStickerPackStatus,
updateStickerPackInfo,
createOrUpdateSticker,
updateStickerLastUsed,
addStickerPackReference,
@ -284,6 +287,13 @@ const dataInterface: ClientInterface = {
getStickerCount,
deleteStickerPack,
getAllStickerPacks,
addUninstalledStickerPack,
removeUninstalledStickerPack,
getInstalledStickerPacks,
getUninstalledStickerPacks,
installStickerPack,
uninstallStickerPack,
getStickerPackInfo,
getAllStickers,
getRecentStickers,
clearAllErrorStickerPackAttempts,
@ -1601,6 +1611,9 @@ async function updateStickerPackStatus(
): Promise<void> {
await channels.updateStickerPackStatus(packId, status, options);
}
async function updateStickerPackInfo(info: StickerPackInfoType): Promise<void> {
await channels.updateStickerPackInfo(info);
}
async function createOrUpdateSticker(sticker: StickerType): Promise<void> {
await channels.createOrUpdateSticker(sticker);
}
@ -1609,7 +1622,7 @@ async function updateStickerLastUsed(
stickerId: number,
timestamp: number
): Promise<void> {
await channels.updateStickerLastUsed(packId, stickerId, timestamp);
return channels.updateStickerLastUsed(packId, stickerId, timestamp);
}
async function addStickerPackReference(
messageId: string,
@ -1624,15 +1637,46 @@ async function deleteStickerPackReference(
return channels.deleteStickerPackReference(messageId, packId);
}
async function deleteStickerPack(packId: string): Promise<Array<string>> {
const paths = await channels.deleteStickerPack(packId);
return paths;
return channels.deleteStickerPack(packId);
}
async function getAllStickerPacks(): Promise<Array<StickerPackType>> {
const packs = await channels.getAllStickerPacks();
return packs;
}
async function addUninstalledStickerPack(
pack: UninstalledStickerPackType
): Promise<void> {
return channels.addUninstalledStickerPack(pack);
}
async function removeUninstalledStickerPack(packId: string): Promise<void> {
return channels.removeUninstalledStickerPack(packId);
}
async function getInstalledStickerPacks(): Promise<Array<StickerPackType>> {
return channels.getInstalledStickerPacks();
}
async function getUninstalledStickerPacks(): Promise<
Array<UninstalledStickerPackType>
> {
return channels.getUninstalledStickerPacks();
}
async function installStickerPack(
packId: string,
timestamp: number
): Promise<void> {
return channels.installStickerPack(packId, timestamp);
}
async function uninstallStickerPack(
packId: string,
timestamp: number
): Promise<void> {
return channels.uninstallStickerPack(packId, timestamp);
}
async function getStickerPackInfo(
packId: string
): Promise<StickerPackInfoType | undefined> {
return channels.getStickerPackInfo(packId);
}
async function getAllStickers(): Promise<Array<StickerType>> {
const stickers = await channels.getAllStickers();

View file

@ -202,22 +202,49 @@ export const StickerPackStatuses = [
export type StickerPackStatusType = typeof StickerPackStatuses[number];
export type StickerPackType = Readonly<{
export type StorageServiceFieldsType = Readonly<{
storageID?: string;
storageVersion?: number;
storageUnknownFields?: Uint8Array | null;
storageNeedsSync: boolean;
}>;
export type InstalledStickerPackType = Readonly<{
id: string;
key: string;
attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral';
author: string;
coverStickerId: number;
createdAt: number;
downloadAttempts: number;
installedAt?: number;
lastUsed?: number;
status: StickerPackStatusType;
stickerCount: number;
stickers: Record<string, StickerType>;
title: string;
}>;
uninstalledAt?: undefined;
position?: number | null;
}> &
StorageServiceFieldsType;
export type UninstalledStickerPackType = Readonly<{
id: string;
key?: undefined;
uninstalledAt: number;
position?: undefined;
}> &
StorageServiceFieldsType;
export type StickerPackInfoType =
| InstalledStickerPackType
| UninstalledStickerPackType;
export type StickerPackType = InstalledStickerPackType &
Readonly<{
attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral';
author: string;
coverStickerId: number;
createdAt: number;
downloadAttempts: number;
installedAt?: number;
lastUsed?: number;
status: StickerPackStatusType;
stickerCount: number;
stickers: Record<string, StickerType>;
title: string;
}>;
export type UnprocessedType = {
id: string;
@ -267,12 +294,8 @@ export type StoryDistributionType = Readonly<{
allowsReplies: boolean;
isBlockList: boolean;
senderKeyInfo: SenderKeyInfoType | undefined;
storageID?: string;
storageVersion?: number;
storageUnknownFields?: Uint8Array | null;
storageNeedsSync: boolean;
}>;
}> &
StorageServiceFieldsType;
export type StoryDistributionMemberType = Readonly<{
listId: UUIDStringType;
uuid: UUIDStringType;
@ -543,6 +566,7 @@ export type DataInterface = {
status: StickerPackStatusType,
options?: { timestamp: number }
) => Promise<void>;
updateStickerPackInfo: (info: StickerPackInfoType) => Promise<void>;
createOrUpdateSticker: (sticker: StickerType) => Promise<void>;
updateStickerLastUsed: (
packId: string,
@ -557,6 +581,17 @@ export type DataInterface = {
getStickerCount: () => Promise<number>;
deleteStickerPack: (packId: string) => Promise<Array<string>>;
getAllStickerPacks: () => Promise<Array<StickerPackType>>;
addUninstalledStickerPack: (
pack: UninstalledStickerPackType
) => Promise<void>;
removeUninstalledStickerPack: (packId: string) => Promise<void>;
getInstalledStickerPacks: () => Promise<Array<StickerPackType>>;
getUninstalledStickerPacks: () => Promise<Array<UninstalledStickerPackType>>;
installStickerPack: (packId: string, timestamp: number) => Promise<void>;
uninstallStickerPack: (packId: string, timestamp: number) => Promise<void>;
getStickerPackInfo: (
packId: string
) => Promise<StickerPackInfoType | undefined>;
getAllStickers: () => Promise<Array<StickerType>>;
getRecentStickers: (options?: {
limit?: number;

View file

@ -81,6 +81,7 @@ import type {
GetUnreadByConversationAndMarkReadResultType,
IdentityKeyIdType,
StoredIdentityKeyType,
InstalledStickerPackType,
ItemKeyType,
StoredItemType,
ConversationMessageStatsType,
@ -104,6 +105,7 @@ import type {
SessionType,
SignedPreKeyIdType,
StoredSignedPreKeyType,
StickerPackInfoType,
StickerPackStatusType,
StickerPackType,
StickerType,
@ -111,6 +113,7 @@ import type {
StoryDistributionType,
StoryDistributionWithMembersType,
StoryReadType,
UninstalledStickerPackType,
UnprocessedType,
UnprocessedUpdateType,
} from './Interface';
@ -268,6 +271,7 @@ const dataInterface: ServerInterface = {
createOrUpdateStickerPack,
updateStickerPackStatus,
updateStickerPackInfo,
createOrUpdateSticker,
updateStickerLastUsed,
addStickerPackReference,
@ -275,6 +279,13 @@ const dataInterface: ServerInterface = {
getStickerCount,
deleteStickerPack,
getAllStickerPacks,
addUninstalledStickerPack,
removeUninstalledStickerPack,
getInstalledStickerPacks,
getUninstalledStickerPacks,
installStickerPack,
uninstallStickerPack,
getStickerPackInfo,
getAllStickers,
getRecentStickers,
clearAllErrorStickerPackAttempts,
@ -3446,6 +3457,10 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
status,
stickerCount,
title,
storageID,
storageVersion,
storageUnknownFields,
storageNeedsSync,
} = pack;
if (!id) {
throw new Error(
@ -3453,7 +3468,22 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
);
}
const rows = db
let { position } = pack;
// Assign default position
if (!isNumber(position)) {
position = db
.prepare<EmptyQuery>(
`
SELECT IFNULL(MAX(position) + 1, 0)
FROM sticker_packs
`
)
.pluck()
.get();
}
const row = db
.prepare<Query>(
`
SELECT id
@ -3461,7 +3491,7 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
WHERE id = $id;
`
)
.all({ id });
.get({ id });
const payload = {
attemptedStatus: attemptedStatus ?? null,
author,
@ -3475,9 +3505,14 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
status,
stickerCount,
title,
position: position ?? 0,
storageID: storageID ?? null,
storageVersion: storageVersion ?? null,
storageUnknownFields: storageUnknownFields ?? null,
storageNeedsSync: storageNeedsSync ? 1 : 0,
};
if (rows && rows.length) {
if (row) {
db.prepare<Query>(
`
UPDATE sticker_packs SET
@ -3491,7 +3526,12 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
lastUsed = $lastUsed,
status = $status,
stickerCount = $stickerCount,
title = $title
title = $title,
position = $position,
storageID = $storageID,
storageVersion = $storageVersion,
storageUnknownFields = $storageUnknownFields,
storageNeedsSync = $storageNeedsSync
WHERE id = $id;
`
).run(payload);
@ -3513,7 +3553,12 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
lastUsed,
status,
stickerCount,
title
title,
position,
storageID,
storageVersion,
storageUnknownFields,
storageNeedsSync
) values (
$attemptedStatus,
$author,
@ -3526,16 +3571,21 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
$lastUsed,
$status,
$stickerCount,
$title
$title,
$position,
$storageID,
$storageVersion,
$storageUnknownFields,
$storageNeedsSync
)
`
).run(payload);
}
async function updateStickerPackStatus(
function updateStickerPackStatusSync(
id: string,
status: StickerPackStatusType,
options?: { timestamp: number }
): Promise<void> {
): void {
const db = getInstance();
const timestamp = options ? options.timestamp || Date.now() : Date.now();
const installedAt = status === 'installed' ? timestamp : null;
@ -3552,6 +3602,61 @@ async function updateStickerPackStatus(
installedAt,
});
}
async function updateStickerPackStatus(
id: string,
status: StickerPackStatusType,
options?: { timestamp: number }
): Promise<void> {
return updateStickerPackStatusSync(id, status, options);
}
async function updateStickerPackInfo({
id,
storageID,
storageVersion,
storageUnknownFields,
storageNeedsSync,
uninstalledAt,
}: StickerPackInfoType): Promise<void> {
const db = getInstance();
if (uninstalledAt) {
db.prepare<Query>(
`
UPDATE uninstalled_sticker_packs
SET
storageID = $storageID,
storageVersion = $storageVersion,
storageUnknownFields = $storageUnknownFields,
storageNeedsSync = $storageNeedsSync
WHERE id = $id;
`
).run({
id,
storageID: storageID ?? null,
storageVersion: storageVersion ?? null,
storageUnknownFields: storageUnknownFields ?? null,
storageNeedsSync: storageNeedsSync ? 1 : 0,
});
} else {
db.prepare<Query>(
`
UPDATE sticker_packs
SET
storageID = $storageID,
storageVersion = $storageVersion,
storageUnknownFields = $storageUnknownFields,
storageNeedsSync = $storageNeedsSync
WHERE id = $id;
`
).run({
id,
storageID: storageID ?? null,
storageVersion: storageVersion ?? null,
storageUnknownFields: storageUnknownFields ?? null,
storageNeedsSync: storageNeedsSync ? 1 : 0,
});
}
}
async function clearAllErrorStickerPackAttempts(): Promise<void> {
const db = getInstance();
@ -3823,13 +3928,160 @@ async function getAllStickerPacks(): Promise<Array<StickerPackType>> {
.prepare<EmptyQuery>(
`
SELECT * FROM sticker_packs
ORDER BY installedAt DESC, createdAt DESC
ORDER BY position ASC, id ASC
`
)
.all();
return rows || [];
}
function addUninstalledStickerPackSync(pack: UninstalledStickerPackType): void {
const db = getInstance();
db.prepare<Query>(
`
INSERT OR REPLACE INTO uninstalled_sticker_packs
(
id, uninstalledAt, storageID, storageVersion, storageUnknownFields,
storageNeedsSync
)
VALUES
(
$id, $uninstalledAt, $storageID, $storageVersion, $unknownFields,
$storageNeedsSync
)
`
).run({
id: pack.id,
uninstalledAt: pack.uninstalledAt,
storageID: pack.storageID ?? null,
storageVersion: pack.storageVersion ?? null,
unknownFields: pack.storageUnknownFields ?? null,
storageNeedsSync: pack.storageNeedsSync ? 1 : 0,
});
}
async function addUninstalledStickerPack(
pack: UninstalledStickerPackType
): Promise<void> {
return addUninstalledStickerPackSync(pack);
}
function removeUninstalledStickerPackSync(packId: string): void {
const db = getInstance();
db.prepare<Query>(
'DELETE FROM uninstalled_sticker_packs WHERE id IS $id'
).run({ id: packId });
}
async function removeUninstalledStickerPack(packId: string): Promise<void> {
return removeUninstalledStickerPackSync(packId);
}
async function getUninstalledStickerPacks(): Promise<
Array<UninstalledStickerPackType>
> {
const db = getInstance();
const rows = db
.prepare<EmptyQuery>(
'SELECT * FROM uninstalled_sticker_packs ORDER BY id ASC'
)
.all();
return rows || [];
}
async function getInstalledStickerPacks(): Promise<Array<StickerPackType>> {
const db = getInstance();
// If sticker pack has a storageID - it is being downloaded and about to be
// installed so we better sync it back to storage service if asked.
const rows = db
.prepare<EmptyQuery>(
`
SELECT *
FROM sticker_packs
WHERE
status IS "installed" OR
storageID IS NOT NULL
ORDER BY id ASC
`
)
.all();
return rows || [];
}
async function getStickerPackInfo(
packId: string
): Promise<StickerPackInfoType | undefined> {
const db = getInstance();
return db.transaction(() => {
const uninstalled = db
.prepare<Query>(
`
SELECT * FROM uninstalled_sticker_packs
WHERE id IS $packId
`
)
.get({ packId });
if (uninstalled) {
return uninstalled as UninstalledStickerPackType;
}
const installed = db
.prepare<Query>(
`
SELECT
id, key, position, storageID, storageVersion, storageUnknownFields
FROM sticker_packs
WHERE id IS $packId
`
)
.get({ packId });
if (installed) {
return installed as InstalledStickerPackType;
}
return undefined;
})();
}
async function installStickerPack(
packId: string,
timestamp: number
): Promise<void> {
const db = getInstance();
return db.transaction(() => {
const status = 'installed';
updateStickerPackStatusSync(packId, status, { timestamp });
removeUninstalledStickerPackSync(packId);
})();
}
async function uninstallStickerPack(
packId: string,
timestamp: number
): Promise<void> {
const db = getInstance();
return db.transaction(() => {
const status = 'downloaded';
updateStickerPackStatusSync(packId, status);
db.prepare<Query>(
`
UPDATE sticker_packs SET
storageID = NULL,
storageVersion = NULL,
storageUnknownFields = NULL,
storageNeedsSync = 0
WHERE id = $packId;
`
).run({ packId });
addUninstalledStickerPackSync({
id: packId,
uninstalledAt: timestamp,
storageNeedsSync: true,
});
})();
}
async function getAllStickers(): Promise<Array<StickerType>> {
const db = getInstance();

View file

@ -0,0 +1,62 @@
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from 'better-sqlite3';
import type { LoggerType } from '../../types/Logging';
export default function updateToSchemaVersion65(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 65) {
return;
}
db.transaction(() => {
db.exec(
`
ALTER TABLE sticker_packs ADD COLUMN position INTEGER DEFAULT 0 NOT NULL;
ALTER TABLE sticker_packs ADD COLUMN storageID STRING;
ALTER TABLE sticker_packs ADD COLUMN storageVersion INTEGER;
ALTER TABLE sticker_packs ADD COLUMN storageUnknownFields BLOB;
ALTER TABLE sticker_packs
ADD COLUMN storageNeedsSync
INTEGER DEFAULT 0 NOT NULL;
CREATE TABLE uninstalled_sticker_packs (
id STRING NOT NULL PRIMARY KEY,
uninstalledAt NUMBER NOT NULL,
storageID STRING,
storageVersion NUMBER,
storageUnknownFields BLOB,
storageNeedsSync INTEGER NOT NULL
);
-- Set initial position
UPDATE sticker_packs
SET
position = (row_number - 1),
storageNeedsSync = 1
FROM (
SELECT id, row_number() OVER (ORDER BY lastUsed DESC) as row_number
FROM sticker_packs
) as ordered_pairs
WHERE sticker_packs.id IS ordered_pairs.id;
-- See: getAllStickerPacks
CREATE INDEX sticker_packs_by_position_and_id ON sticker_packs (
position ASC,
id ASC
);
`
);
db.pragma('user_version = 65');
})();
logger.info('updateToSchemaVersion65: success!');
}

View file

@ -40,6 +40,7 @@ import updateToSchemaVersion61 from './61-distribution-list-storage';
import updateToSchemaVersion62 from './62-add-urgent-to-send-log';
import updateToSchemaVersion63 from './63-add-urgent-to-unprocessed';
import updateToSchemaVersion64 from './64-uuid-column-for-pre-keys';
import updateToSchemaVersion65 from './65-add-storage-id-to-stickers';
function updateToSchemaVersion1(
currentVersion: number,
@ -1943,6 +1944,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion62,
updateToSchemaVersion63,
updateToSchemaVersion64,
updateToSchemaVersion65,
];
export function updateSchema(db: Database, logger: LoggerType): void {

View file

@ -6,7 +6,9 @@ import { isNumber, last } from 'lodash';
export type EmptyQuery = [];
export type ArrayQuery = Array<Array<null | number | bigint | string>>;
export type Query = { [key: string]: null | number | bigint | string | Buffer };
export type Query = {
[key: string]: null | number | bigint | string | Uint8Array;
};
export type JSONRows = Array<{ readonly json: string }>;
export type TableType =