Stickers in storage service
This commit is contained in:
parent
d8a7e99c81
commit
b47a906211
18 changed files with 1216 additions and 80 deletions
BIN
fixtures/stickerpack-ae8fedafda4768fd3384d4b3b9db963d-0.bin
Normal file
BIN
fixtures/stickerpack-ae8fedafda4768fd3384d4b3b9db963d-0.bin
Normal file
Binary file not shown.
BIN
fixtures/stickerpack-ae8fedafda4768fd3384d4b3b9db963d.bin
Normal file
BIN
fixtures/stickerpack-ae8fedafda4768fd3384d4b3b9db963d.bin
Normal file
Binary file not shown.
BIN
fixtures/stickerpack-c40ed069cdc2b91eccfccf25e6bcddfc-0.bin
Normal file
BIN
fixtures/stickerpack-c40ed069cdc2b91eccfccf25e6bcddfc-0.bin
Normal file
Binary file not shown.
BIN
fixtures/stickerpack-c40ed069cdc2b91eccfccf25e6bcddfc.bin
Normal file
BIN
fixtures/stickerpack-c40ed069cdc2b91eccfccf25e6bcddfc.bin
Normal file
Binary file not shown.
|
@ -40,6 +40,7 @@ message ManifestRecord {
|
||||||
GROUPV2 = 3;
|
GROUPV2 = 3;
|
||||||
ACCOUNT = 4;
|
ACCOUNT = 4;
|
||||||
STORY_DISTRIBUTION_LIST = 5;
|
STORY_DISTRIBUTION_LIST = 5;
|
||||||
|
STICKER_PACK = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional bytes raw = 1;
|
optional bytes raw = 1;
|
||||||
|
@ -59,6 +60,7 @@ message StorageRecord {
|
||||||
GroupV2Record groupV2 = 3;
|
GroupV2Record groupV2 = 3;
|
||||||
AccountRecord account = 4;
|
AccountRecord account = 4;
|
||||||
StoryDistributionListRecord storyDistributionList = 5;
|
StoryDistributionListRecord storyDistributionList = 5;
|
||||||
|
StickerPackRecord stickerPack = 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,3 +160,24 @@ message StoryDistributionListRecord {
|
||||||
optional bool allowsReplies = 5;
|
optional bool allowsReplies = 5;
|
||||||
optional bool isBlockList = 6;
|
optional bool isBlockList = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message StickerPackRecord {
|
||||||
|
optional bytes packId = 1; // 16 bytes
|
||||||
|
optional bytes packKey = 2; // 32 bytes, used to derive the AES-256 key
|
||||||
|
// aesKey = HKDF(
|
||||||
|
// input = packKey,
|
||||||
|
// salt = 32 zero bytes,
|
||||||
|
// info = "Sticker Pack"
|
||||||
|
// )
|
||||||
|
optional uint32 position = 3; // When displayed sticker packs should be first sorted
|
||||||
|
// in descending order by zero-based `position` and
|
||||||
|
// then by ascending `packId` (lexicographically,
|
||||||
|
// packId can be treated as a hex string).
|
||||||
|
// When installing a sticker pack the client should find
|
||||||
|
// the maximum `position` among currently known stickers
|
||||||
|
// and use `max_position + 1` as the value for the new
|
||||||
|
// `position`.
|
||||||
|
optional uint64 deletedAtTimestamp = 4; // Timestamp in milliseconds. When present and
|
||||||
|
// non-zero - `packKey` and `position` should
|
||||||
|
// be unset
|
||||||
|
}
|
||||||
|
|
|
@ -21,11 +21,13 @@ import {
|
||||||
mergeGroupV1Record,
|
mergeGroupV1Record,
|
||||||
mergeGroupV2Record,
|
mergeGroupV2Record,
|
||||||
mergeStoryDistributionListRecord,
|
mergeStoryDistributionListRecord,
|
||||||
|
mergeStickerPackRecord,
|
||||||
toAccountRecord,
|
toAccountRecord,
|
||||||
toContactRecord,
|
toContactRecord,
|
||||||
toGroupV1Record,
|
toGroupV1Record,
|
||||||
toGroupV2Record,
|
toGroupV2Record,
|
||||||
toStoryDistributionListRecord,
|
toStoryDistributionListRecord,
|
||||||
|
toStickerPackRecord,
|
||||||
} from './storageRecordOps';
|
} from './storageRecordOps';
|
||||||
import type { MergeResultType } from './storageRecordOps';
|
import type { MergeResultType } from './storageRecordOps';
|
||||||
import { MAX_READ_KEYS } from './storageConstants';
|
import { MAX_READ_KEYS } from './storageConstants';
|
||||||
|
@ -52,7 +54,12 @@ import type {
|
||||||
UnknownRecord,
|
UnknownRecord,
|
||||||
} from '../types/StorageService.d';
|
} from '../types/StorageService.d';
|
||||||
import MessageSender from '../textsecure/SendMessage';
|
import MessageSender from '../textsecure/SendMessage';
|
||||||
import type { StoryDistributionWithMembersType } from '../sql/Interface';
|
import type {
|
||||||
|
StoryDistributionWithMembersType,
|
||||||
|
StorageServiceFieldsType,
|
||||||
|
StickerPackType,
|
||||||
|
UninstalledStickerPackType,
|
||||||
|
} from '../sql/Interface';
|
||||||
import { MY_STORIES_ID } from '../types/Stories';
|
import { MY_STORIES_ID } from '../types/Stories';
|
||||||
|
|
||||||
type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier;
|
type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier;
|
||||||
|
@ -322,11 +329,15 @@ async function generateManifest(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const storyDistributionLists =
|
const {
|
||||||
await dataInterface.getAllStoryDistributionsWithMembers();
|
storyDistributionLists,
|
||||||
|
installedStickerPacks,
|
||||||
|
uninstalledStickerPacks,
|
||||||
|
} = await getNonConversationRecords();
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`storageService.upload(${version}): adding storyDistributionLists=${storyDistributionLists.length}`
|
`storageService.upload(${version}): ` +
|
||||||
|
`adding storyDistributionLists=${storyDistributionLists.length}`
|
||||||
);
|
);
|
||||||
|
|
||||||
storyDistributionLists.forEach(storyDistributionList => {
|
storyDistributionLists.forEach(storyDistributionList => {
|
||||||
|
@ -355,6 +366,81 @@ async function generateManifest(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`storageService.upload(${version}): ` +
|
||||||
|
`adding uninstalled stickerPacks=${uninstalledStickerPacks.length}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const uninstalledStickerPackIds = new Set<string>();
|
||||||
|
|
||||||
|
uninstalledStickerPacks.forEach(stickerPack => {
|
||||||
|
const storageRecord = new Proto.StorageRecord();
|
||||||
|
storageRecord.stickerPack = toStickerPackRecord(stickerPack);
|
||||||
|
|
||||||
|
uninstalledStickerPackIds.add(stickerPack.id);
|
||||||
|
|
||||||
|
const { isNewItem, storageID } = processStorageRecord({
|
||||||
|
currentStorageID: stickerPack.storageID,
|
||||||
|
currentStorageVersion: stickerPack.storageVersion,
|
||||||
|
identifierType: ITEM_TYPE.STICKER_PACK,
|
||||||
|
storageNeedsSync: stickerPack.storageNeedsSync,
|
||||||
|
storageRecord,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNewItem) {
|
||||||
|
postUploadUpdateFunctions.push(() => {
|
||||||
|
dataInterface.addUninstalledStickerPack({
|
||||||
|
...stickerPack,
|
||||||
|
storageID,
|
||||||
|
storageVersion: version,
|
||||||
|
storageNeedsSync: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`storageService.upload(${version}): ` +
|
||||||
|
`adding installed stickerPacks=${installedStickerPacks.length}`
|
||||||
|
);
|
||||||
|
|
||||||
|
installedStickerPacks.forEach(stickerPack => {
|
||||||
|
if (uninstalledStickerPackIds.has(stickerPack.id)) {
|
||||||
|
log.error(
|
||||||
|
`storageService.upload(${version}): ` +
|
||||||
|
`sticker pack ${stickerPack.id} is both installed and uninstalled`
|
||||||
|
);
|
||||||
|
window.reduxActions.stickers.uninstallStickerPack(
|
||||||
|
stickerPack.id,
|
||||||
|
stickerPack.key,
|
||||||
|
{ fromSync: true }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageRecord = new Proto.StorageRecord();
|
||||||
|
storageRecord.stickerPack = toStickerPackRecord(stickerPack);
|
||||||
|
|
||||||
|
const { isNewItem, storageID } = processStorageRecord({
|
||||||
|
currentStorageID: stickerPack.storageID,
|
||||||
|
currentStorageVersion: stickerPack.storageVersion,
|
||||||
|
identifierType: ITEM_TYPE.STICKER_PACK,
|
||||||
|
storageNeedsSync: stickerPack.storageNeedsSync,
|
||||||
|
storageRecord,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNewItem) {
|
||||||
|
postUploadUpdateFunctions.push(() => {
|
||||||
|
dataInterface.createOrUpdateStickerPack({
|
||||||
|
...stickerPack,
|
||||||
|
storageID,
|
||||||
|
storageVersion: version,
|
||||||
|
storageNeedsSync: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const unknownRecordsArray: ReadonlyArray<UnknownRecord> = (
|
const unknownRecordsArray: ReadonlyArray<UnknownRecord> = (
|
||||||
window.storage.get('storage-service-unknown-records') || []
|
window.storage.get('storage-service-unknown-records') || []
|
||||||
).filter((record: UnknownRecord) => !validRecordTypes.has(record.itemType));
|
).filter((record: UnknownRecord) => !validRecordTypes.has(record.itemType));
|
||||||
|
@ -858,6 +944,15 @@ async function mergeRecord(
|
||||||
storageVersion,
|
storageVersion,
|
||||||
storageRecord.storyDistributionList
|
storageRecord.storyDistributionList
|
||||||
);
|
);
|
||||||
|
} else if (
|
||||||
|
itemType === ITEM_TYPE.STICKER_PACK &&
|
||||||
|
storageRecord.stickerPack
|
||||||
|
) {
|
||||||
|
mergeResult = await mergeStickerPackRecord(
|
||||||
|
storageID,
|
||||||
|
storageVersion,
|
||||||
|
storageRecord.stickerPack
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
isUnsupported = true;
|
isUnsupported = true;
|
||||||
log.warn(
|
log.warn(
|
||||||
|
@ -914,6 +1009,31 @@ async function mergeRecord(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NonConversationRecordsResultType = Readonly<{
|
||||||
|
installedStickerPacks: ReadonlyArray<StickerPackType>;
|
||||||
|
uninstalledStickerPacks: ReadonlyArray<UninstalledStickerPackType>;
|
||||||
|
storyDistributionLists: ReadonlyArray<StoryDistributionWithMembersType>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// TODO: DESKTOP-3929
|
||||||
|
async function getNonConversationRecords(): Promise<NonConversationRecordsResultType> {
|
||||||
|
const [
|
||||||
|
storyDistributionLists,
|
||||||
|
uninstalledStickerPacks,
|
||||||
|
installedStickerPacks,
|
||||||
|
] = await Promise.all([
|
||||||
|
dataInterface.getAllStoryDistributionsWithMembers(),
|
||||||
|
dataInterface.getUninstalledStickerPacks(),
|
||||||
|
dataInterface.getInstalledStickerPacks(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
storyDistributionLists,
|
||||||
|
uninstalledStickerPacks,
|
||||||
|
installedStickerPacks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function processManifest(
|
async function processManifest(
|
||||||
manifest: Proto.IManifestRecord,
|
manifest: Proto.IManifestRecord,
|
||||||
version: number
|
version: number
|
||||||
|
@ -930,6 +1050,7 @@ async function processManifest(
|
||||||
|
|
||||||
const remoteKeys = new Set(remoteKeysTypeMap.keys());
|
const remoteKeys = new Set(remoteKeysTypeMap.keys());
|
||||||
const localVersions = new Map<string, number | undefined>();
|
const localVersions = new Map<string, number | undefined>();
|
||||||
|
let localRecordCount = 0;
|
||||||
|
|
||||||
const conversations = window.getConversations();
|
const conversations = window.getConversations();
|
||||||
conversations.forEach((conversation: ConversationModel) => {
|
conversations.forEach((conversation: ConversationModel) => {
|
||||||
|
@ -938,6 +1059,33 @@ async function processManifest(
|
||||||
localVersions.set(storageID, conversation.get('storageVersion'));
|
localVersions.set(storageID, conversation.get('storageVersion'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
localRecordCount += conversations.length;
|
||||||
|
|
||||||
|
{
|
||||||
|
const {
|
||||||
|
storyDistributionLists,
|
||||||
|
installedStickerPacks,
|
||||||
|
uninstalledStickerPacks,
|
||||||
|
} = await getNonConversationRecords();
|
||||||
|
|
||||||
|
const collectLocalKeysFromFields = ({
|
||||||
|
storageID,
|
||||||
|
storageVersion,
|
||||||
|
}: StorageServiceFieldsType): void => {
|
||||||
|
if (storageID) {
|
||||||
|
localVersions.set(storageID, storageVersion);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
storyDistributionLists.forEach(collectLocalKeysFromFields);
|
||||||
|
localRecordCount += storyDistributionLists.length;
|
||||||
|
|
||||||
|
uninstalledStickerPacks.forEach(collectLocalKeysFromFields);
|
||||||
|
localRecordCount += uninstalledStickerPacks.length;
|
||||||
|
|
||||||
|
installedStickerPacks.forEach(collectLocalKeysFromFields);
|
||||||
|
localRecordCount += installedStickerPacks.length;
|
||||||
|
}
|
||||||
|
|
||||||
const unknownRecordsArray: ReadonlyArray<UnknownRecord> =
|
const unknownRecordsArray: ReadonlyArray<UnknownRecord> =
|
||||||
window.storage.get('storage-service-unknown-records') || [];
|
window.storage.get('storage-service-unknown-records') || [];
|
||||||
|
@ -973,7 +1121,7 @@ async function processManifest(
|
||||||
);
|
);
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`storageService.process(${version}): localRecords=${conversations.length} ` +
|
`storageService.process(${version}): localRecords=${localRecordCount} ` +
|
||||||
`localKeys=${localVersions.size} unknownKeys=${stillUnknown.length} ` +
|
`localKeys=${localVersions.size} unknownKeys=${stillUnknown.length} ` +
|
||||||
`remoteKeys=${remoteKeys.size}`
|
`remoteKeys=${remoteKeys.size}`
|
||||||
);
|
);
|
||||||
|
@ -1025,33 +1173,96 @@ async function processManifest(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check to make sure we have a "My Stories" distribution list set up
|
// Refetch various records post-merge
|
||||||
const myStories = await dataInterface.getStoryDistributionWithMembers(
|
{
|
||||||
MY_STORIES_ID
|
const {
|
||||||
);
|
storyDistributionLists,
|
||||||
|
installedStickerPacks,
|
||||||
|
uninstalledStickerPacks,
|
||||||
|
} = await getNonConversationRecords();
|
||||||
|
|
||||||
if (!myStories) {
|
uninstalledStickerPacks.forEach(stickerPack => {
|
||||||
const storyDistribution: StoryDistributionWithMembersType = {
|
const { storageID, storageVersion } = stickerPack;
|
||||||
allowsReplies: true,
|
if (!storageID || remoteKeys.has(storageID)) {
|
||||||
id: MY_STORIES_ID,
|
return;
|
||||||
isBlockList: true,
|
}
|
||||||
members: [],
|
|
||||||
name: MY_STORIES_ID,
|
|
||||||
senderKeyInfo: undefined,
|
|
||||||
storageNeedsSync: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
await dataInterface.createNewStoryDistribution(storyDistribution);
|
const missingKey = redactStorageID(storageID, storageVersion);
|
||||||
|
log.info(
|
||||||
|
`storageService.process(${version}): localKey=${missingKey} was not ` +
|
||||||
|
'in remote manifest'
|
||||||
|
);
|
||||||
|
dataInterface.addUninstalledStickerPack({
|
||||||
|
...stickerPack,
|
||||||
|
storageID: undefined,
|
||||||
|
storageVersion: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const shouldSave = false;
|
installedStickerPacks.forEach(stickerPack => {
|
||||||
window.reduxActions.storyDistributionLists.createDistributionList(
|
const { storageID, storageVersion } = stickerPack;
|
||||||
storyDistribution.name,
|
if (!storageID || remoteKeys.has(storageID)) {
|
||||||
storyDistribution.members,
|
return;
|
||||||
storyDistribution,
|
}
|
||||||
shouldSave
|
|
||||||
|
const missingKey = redactStorageID(storageID, storageVersion);
|
||||||
|
log.info(
|
||||||
|
`storageService.process(${version}): localKey=${missingKey} was not ` +
|
||||||
|
'in remote manifest'
|
||||||
|
);
|
||||||
|
dataInterface.createOrUpdateStickerPack({
|
||||||
|
...stickerPack,
|
||||||
|
storageID: undefined,
|
||||||
|
storageVersion: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
storyDistributionLists.forEach(storyDistributionList => {
|
||||||
|
const { storageID, storageVersion } = storyDistributionList;
|
||||||
|
if (!storageID || remoteKeys.has(storageID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingKey = redactStorageID(storageID, storageVersion);
|
||||||
|
log.info(
|
||||||
|
`storageService.process(${version}): localKey=${missingKey} was not ` +
|
||||||
|
'in remote manifest'
|
||||||
|
);
|
||||||
|
dataInterface.modifyStoryDistribution({
|
||||||
|
...storyDistributionList,
|
||||||
|
storageID: undefined,
|
||||||
|
storageVersion: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check to make sure we have a "My Stories" distribution list set up
|
||||||
|
const myStories = storyDistributionLists.find(
|
||||||
|
({ id }) => id === MY_STORIES_ID
|
||||||
);
|
);
|
||||||
|
|
||||||
conflictCount += 1;
|
if (!myStories) {
|
||||||
|
const storyDistribution: StoryDistributionWithMembersType = {
|
||||||
|
allowsReplies: true,
|
||||||
|
id: MY_STORIES_ID,
|
||||||
|
isBlockList: true,
|
||||||
|
members: [],
|
||||||
|
name: MY_STORIES_ID,
|
||||||
|
senderKeyInfo: undefined,
|
||||||
|
storageNeedsSync: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await dataInterface.createNewStoryDistribution(storyDistribution);
|
||||||
|
|
||||||
|
const shouldSave = false;
|
||||||
|
window.reduxActions.storyDistributionLists.createDistributionList(
|
||||||
|
storyDistribution.name,
|
||||||
|
storyDistribution.members,
|
||||||
|
storyDistribution,
|
||||||
|
shouldSave
|
||||||
|
);
|
||||||
|
|
||||||
|
conflictCount += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
|
|
|
@ -45,7 +45,11 @@ import { SignalService as Proto } from '../protobuf';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import type { UUIDStringType } from '../types/UUID';
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
import { MY_STORIES_ID } from '../types/Stories';
|
import { MY_STORIES_ID } from '../types/Stories';
|
||||||
import type { StoryDistributionWithMembersType } from '../sql/Interface';
|
import * as Stickers from '../types/Stickers';
|
||||||
|
import type {
|
||||||
|
StoryDistributionWithMembersType,
|
||||||
|
StickerPackInfoType,
|
||||||
|
} from '../sql/Interface';
|
||||||
import dataInterface from '../sql/Client';
|
import dataInterface from '../sql/Client';
|
||||||
|
|
||||||
type RecordClass =
|
type RecordClass =
|
||||||
|
@ -411,6 +415,31 @@ export function toStoryDistributionListRecord(
|
||||||
return storyDistributionListRecord;
|
return storyDistributionListRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toStickerPackRecord(
|
||||||
|
stickerPack: StickerPackInfoType
|
||||||
|
): Proto.StickerPackRecord {
|
||||||
|
const stickerPackRecord = new Proto.StickerPackRecord();
|
||||||
|
|
||||||
|
stickerPackRecord.packId = Bytes.fromHex(stickerPack.id);
|
||||||
|
|
||||||
|
if (stickerPack.uninstalledAt !== undefined) {
|
||||||
|
stickerPackRecord.deletedAtTimestamp = Long.fromNumber(
|
||||||
|
stickerPack.uninstalledAt
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
stickerPackRecord.packKey = Bytes.fromBase64(stickerPack.key);
|
||||||
|
if (stickerPack.position) {
|
||||||
|
stickerPackRecord.position = stickerPack.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stickerPack.storageUnknownFields) {
|
||||||
|
stickerPackRecord.__unknownFields = [stickerPack.storageUnknownFields];
|
||||||
|
}
|
||||||
|
|
||||||
|
return stickerPackRecord;
|
||||||
|
}
|
||||||
|
|
||||||
type MessageRequestCapableRecord = Proto.IContactRecord | Proto.IGroupV1Record;
|
type MessageRequestCapableRecord = Proto.IContactRecord | Proto.IGroupV1Record;
|
||||||
|
|
||||||
function applyMessageRequestState(
|
function applyMessageRequestState(
|
||||||
|
@ -1355,3 +1384,118 @@ export async function mergeStoryDistributionListRecord(
|
||||||
oldStorageVersion,
|
oldStorageVersion,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function mergeStickerPackRecord(
|
||||||
|
storageID: string,
|
||||||
|
storageVersion: number,
|
||||||
|
stickerPackRecord: Proto.IStickerPackRecord
|
||||||
|
): Promise<MergeResultType> {
|
||||||
|
if (!stickerPackRecord.packId || Bytes.isEmpty(stickerPackRecord.packId)) {
|
||||||
|
throw new Error(`No stickerPackRecord identifier for ${storageID}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const details: Array<string> = [];
|
||||||
|
const id = Bytes.toHex(stickerPackRecord.packId);
|
||||||
|
|
||||||
|
const localStickerPack = await dataInterface.getStickerPackInfo(id);
|
||||||
|
|
||||||
|
if (stickerPackRecord.__unknownFields) {
|
||||||
|
details.push('adding unknown fields');
|
||||||
|
}
|
||||||
|
const storageUnknownFields = stickerPackRecord.__unknownFields
|
||||||
|
? Bytes.concatenate(stickerPackRecord.__unknownFields)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let stickerPack: StickerPackInfoType;
|
||||||
|
if (stickerPackRecord.deletedAtTimestamp?.toNumber()) {
|
||||||
|
stickerPack = {
|
||||||
|
id,
|
||||||
|
uninstalledAt: stickerPackRecord.deletedAtTimestamp.toNumber(),
|
||||||
|
storageID,
|
||||||
|
storageVersion,
|
||||||
|
storageUnknownFields,
|
||||||
|
storageNeedsSync: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
!stickerPackRecord.packKey ||
|
||||||
|
Bytes.isEmpty(stickerPackRecord.packKey)
|
||||||
|
) {
|
||||||
|
throw new Error(`No stickerPackRecord key for ${storageID}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
stickerPack = {
|
||||||
|
id,
|
||||||
|
key: Bytes.toBase64(stickerPackRecord.packKey),
|
||||||
|
position:
|
||||||
|
'position' in stickerPackRecord
|
||||||
|
? stickerPackRecord.position
|
||||||
|
: localStickerPack?.position ?? undefined,
|
||||||
|
storageID,
|
||||||
|
storageVersion,
|
||||||
|
storageUnknownFields,
|
||||||
|
storageNeedsSync: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldStorageID = localStickerPack?.storageID;
|
||||||
|
const oldStorageVersion = localStickerPack?.storageVersion;
|
||||||
|
|
||||||
|
const needsToClearUnknownFields =
|
||||||
|
!stickerPack.storageUnknownFields && localStickerPack?.storageUnknownFields;
|
||||||
|
|
||||||
|
if (needsToClearUnknownFields) {
|
||||||
|
details.push('clearing unknown fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hasConflict, details: conflictDetails } = doRecordsConflict(
|
||||||
|
toStickerPackRecord(stickerPack),
|
||||||
|
stickerPackRecord
|
||||||
|
);
|
||||||
|
|
||||||
|
const wasUninstalled = Boolean(localStickerPack?.uninstalledAt);
|
||||||
|
const isUninstalled = Boolean(stickerPack.uninstalledAt);
|
||||||
|
|
||||||
|
details.push(
|
||||||
|
`wasUninstalled=${wasUninstalled}`,
|
||||||
|
`isUninstalled=${isUninstalled}`,
|
||||||
|
`oldPosition=${localStickerPack?.position ?? '?'}`,
|
||||||
|
`newPosition=${stickerPack.position ?? '?'}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((!localStickerPack || !wasUninstalled) && isUninstalled) {
|
||||||
|
assert(localStickerPack?.key, 'Installed sticker pack has no key');
|
||||||
|
window.reduxActions.stickers.uninstallStickerPack(
|
||||||
|
localStickerPack.id,
|
||||||
|
localStickerPack.key,
|
||||||
|
{ fromStorageService: true }
|
||||||
|
);
|
||||||
|
} else if ((!localStickerPack || wasUninstalled) && !isUninstalled) {
|
||||||
|
assert(stickerPack.key, 'Sticker pack does not have key');
|
||||||
|
|
||||||
|
const status = Stickers.getStickerPackStatus(stickerPack.id);
|
||||||
|
if (status === 'downloaded') {
|
||||||
|
window.reduxActions.stickers.installStickerPack(
|
||||||
|
stickerPack.id,
|
||||||
|
stickerPack.key,
|
||||||
|
{
|
||||||
|
fromStorageService: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Stickers.downloadStickerPack(stickerPack.id, stickerPack.key, {
|
||||||
|
finalStatus: 'installed',
|
||||||
|
fromStorageService: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await dataInterface.updateStickerPackInfo(stickerPack);
|
||||||
|
|
||||||
|
return {
|
||||||
|
details: [...details, ...conflictDetails],
|
||||||
|
hasConflict,
|
||||||
|
oldStorageID,
|
||||||
|
oldStorageVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -84,6 +84,7 @@ import type {
|
||||||
SignedPreKeyType,
|
SignedPreKeyType,
|
||||||
StoredSignedPreKeyType,
|
StoredSignedPreKeyType,
|
||||||
StickerPackStatusType,
|
StickerPackStatusType,
|
||||||
|
StickerPackInfoType,
|
||||||
StickerPackType,
|
StickerPackType,
|
||||||
StickerType,
|
StickerType,
|
||||||
StoryDistributionMemberType,
|
StoryDistributionMemberType,
|
||||||
|
@ -92,6 +93,7 @@ import type {
|
||||||
StoryReadType,
|
StoryReadType,
|
||||||
UnprocessedType,
|
UnprocessedType,
|
||||||
UnprocessedUpdateType,
|
UnprocessedUpdateType,
|
||||||
|
UninstalledStickerPackType,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
import Server from './Server';
|
import Server from './Server';
|
||||||
import { isCorruptionError } from './errors';
|
import { isCorruptionError } from './errors';
|
||||||
|
@ -277,6 +279,7 @@ const dataInterface: ClientInterface = {
|
||||||
|
|
||||||
createOrUpdateStickerPack,
|
createOrUpdateStickerPack,
|
||||||
updateStickerPackStatus,
|
updateStickerPackStatus,
|
||||||
|
updateStickerPackInfo,
|
||||||
createOrUpdateSticker,
|
createOrUpdateSticker,
|
||||||
updateStickerLastUsed,
|
updateStickerLastUsed,
|
||||||
addStickerPackReference,
|
addStickerPackReference,
|
||||||
|
@ -284,6 +287,13 @@ const dataInterface: ClientInterface = {
|
||||||
getStickerCount,
|
getStickerCount,
|
||||||
deleteStickerPack,
|
deleteStickerPack,
|
||||||
getAllStickerPacks,
|
getAllStickerPacks,
|
||||||
|
addUninstalledStickerPack,
|
||||||
|
removeUninstalledStickerPack,
|
||||||
|
getInstalledStickerPacks,
|
||||||
|
getUninstalledStickerPacks,
|
||||||
|
installStickerPack,
|
||||||
|
uninstallStickerPack,
|
||||||
|
getStickerPackInfo,
|
||||||
getAllStickers,
|
getAllStickers,
|
||||||
getRecentStickers,
|
getRecentStickers,
|
||||||
clearAllErrorStickerPackAttempts,
|
clearAllErrorStickerPackAttempts,
|
||||||
|
@ -1601,6 +1611,9 @@ async function updateStickerPackStatus(
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await channels.updateStickerPackStatus(packId, status, options);
|
await channels.updateStickerPackStatus(packId, status, options);
|
||||||
}
|
}
|
||||||
|
async function updateStickerPackInfo(info: StickerPackInfoType): Promise<void> {
|
||||||
|
await channels.updateStickerPackInfo(info);
|
||||||
|
}
|
||||||
async function createOrUpdateSticker(sticker: StickerType): Promise<void> {
|
async function createOrUpdateSticker(sticker: StickerType): Promise<void> {
|
||||||
await channels.createOrUpdateSticker(sticker);
|
await channels.createOrUpdateSticker(sticker);
|
||||||
}
|
}
|
||||||
|
@ -1609,7 +1622,7 @@ async function updateStickerLastUsed(
|
||||||
stickerId: number,
|
stickerId: number,
|
||||||
timestamp: number
|
timestamp: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await channels.updateStickerLastUsed(packId, stickerId, timestamp);
|
return channels.updateStickerLastUsed(packId, stickerId, timestamp);
|
||||||
}
|
}
|
||||||
async function addStickerPackReference(
|
async function addStickerPackReference(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
|
@ -1624,15 +1637,46 @@ async function deleteStickerPackReference(
|
||||||
return channels.deleteStickerPackReference(messageId, packId);
|
return channels.deleteStickerPackReference(messageId, packId);
|
||||||
}
|
}
|
||||||
async function deleteStickerPack(packId: string): Promise<Array<string>> {
|
async function deleteStickerPack(packId: string): Promise<Array<string>> {
|
||||||
const paths = await channels.deleteStickerPack(packId);
|
return channels.deleteStickerPack(packId);
|
||||||
|
|
||||||
return paths;
|
|
||||||
}
|
}
|
||||||
async function getAllStickerPacks(): Promise<Array<StickerPackType>> {
|
async function getAllStickerPacks(): Promise<Array<StickerPackType>> {
|
||||||
const packs = await channels.getAllStickerPacks();
|
const packs = await channels.getAllStickerPacks();
|
||||||
|
|
||||||
return packs;
|
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>> {
|
async function getAllStickers(): Promise<Array<StickerType>> {
|
||||||
const stickers = await channels.getAllStickers();
|
const stickers = await channels.getAllStickers();
|
||||||
|
|
||||||
|
|
|
@ -202,22 +202,49 @@ export const StickerPackStatuses = [
|
||||||
|
|
||||||
export type StickerPackStatusType = typeof StickerPackStatuses[number];
|
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;
|
id: string;
|
||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
attemptedStatus?: 'downloaded' | 'installed' | 'ephemeral';
|
uninstalledAt?: undefined;
|
||||||
author: string;
|
position?: number | null;
|
||||||
coverStickerId: number;
|
}> &
|
||||||
createdAt: number;
|
StorageServiceFieldsType;
|
||||||
downloadAttempts: number;
|
|
||||||
installedAt?: number;
|
export type UninstalledStickerPackType = Readonly<{
|
||||||
lastUsed?: number;
|
id: string;
|
||||||
status: StickerPackStatusType;
|
key?: undefined;
|
||||||
stickerCount: number;
|
|
||||||
stickers: Record<string, StickerType>;
|
uninstalledAt: number;
|
||||||
title: string;
|
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 = {
|
export type UnprocessedType = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -267,12 +294,8 @@ export type StoryDistributionType = Readonly<{
|
||||||
allowsReplies: boolean;
|
allowsReplies: boolean;
|
||||||
isBlockList: boolean;
|
isBlockList: boolean;
|
||||||
senderKeyInfo: SenderKeyInfoType | undefined;
|
senderKeyInfo: SenderKeyInfoType | undefined;
|
||||||
|
}> &
|
||||||
storageID?: string;
|
StorageServiceFieldsType;
|
||||||
storageVersion?: number;
|
|
||||||
storageUnknownFields?: Uint8Array | null;
|
|
||||||
storageNeedsSync: boolean;
|
|
||||||
}>;
|
|
||||||
export type StoryDistributionMemberType = Readonly<{
|
export type StoryDistributionMemberType = Readonly<{
|
||||||
listId: UUIDStringType;
|
listId: UUIDStringType;
|
||||||
uuid: UUIDStringType;
|
uuid: UUIDStringType;
|
||||||
|
@ -543,6 +566,7 @@ export type DataInterface = {
|
||||||
status: StickerPackStatusType,
|
status: StickerPackStatusType,
|
||||||
options?: { timestamp: number }
|
options?: { timestamp: number }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
updateStickerPackInfo: (info: StickerPackInfoType) => Promise<void>;
|
||||||
createOrUpdateSticker: (sticker: StickerType) => Promise<void>;
|
createOrUpdateSticker: (sticker: StickerType) => Promise<void>;
|
||||||
updateStickerLastUsed: (
|
updateStickerLastUsed: (
|
||||||
packId: string,
|
packId: string,
|
||||||
|
@ -557,6 +581,17 @@ export type DataInterface = {
|
||||||
getStickerCount: () => Promise<number>;
|
getStickerCount: () => Promise<number>;
|
||||||
deleteStickerPack: (packId: string) => Promise<Array<string>>;
|
deleteStickerPack: (packId: string) => Promise<Array<string>>;
|
||||||
getAllStickerPacks: () => Promise<Array<StickerPackType>>;
|
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>>;
|
getAllStickers: () => Promise<Array<StickerType>>;
|
||||||
getRecentStickers: (options?: {
|
getRecentStickers: (options?: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
|
270
ts/sql/Server.ts
270
ts/sql/Server.ts
|
@ -81,6 +81,7 @@ import type {
|
||||||
GetUnreadByConversationAndMarkReadResultType,
|
GetUnreadByConversationAndMarkReadResultType,
|
||||||
IdentityKeyIdType,
|
IdentityKeyIdType,
|
||||||
StoredIdentityKeyType,
|
StoredIdentityKeyType,
|
||||||
|
InstalledStickerPackType,
|
||||||
ItemKeyType,
|
ItemKeyType,
|
||||||
StoredItemType,
|
StoredItemType,
|
||||||
ConversationMessageStatsType,
|
ConversationMessageStatsType,
|
||||||
|
@ -104,6 +105,7 @@ import type {
|
||||||
SessionType,
|
SessionType,
|
||||||
SignedPreKeyIdType,
|
SignedPreKeyIdType,
|
||||||
StoredSignedPreKeyType,
|
StoredSignedPreKeyType,
|
||||||
|
StickerPackInfoType,
|
||||||
StickerPackStatusType,
|
StickerPackStatusType,
|
||||||
StickerPackType,
|
StickerPackType,
|
||||||
StickerType,
|
StickerType,
|
||||||
|
@ -111,6 +113,7 @@ import type {
|
||||||
StoryDistributionType,
|
StoryDistributionType,
|
||||||
StoryDistributionWithMembersType,
|
StoryDistributionWithMembersType,
|
||||||
StoryReadType,
|
StoryReadType,
|
||||||
|
UninstalledStickerPackType,
|
||||||
UnprocessedType,
|
UnprocessedType,
|
||||||
UnprocessedUpdateType,
|
UnprocessedUpdateType,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
|
@ -268,6 +271,7 @@ const dataInterface: ServerInterface = {
|
||||||
|
|
||||||
createOrUpdateStickerPack,
|
createOrUpdateStickerPack,
|
||||||
updateStickerPackStatus,
|
updateStickerPackStatus,
|
||||||
|
updateStickerPackInfo,
|
||||||
createOrUpdateSticker,
|
createOrUpdateSticker,
|
||||||
updateStickerLastUsed,
|
updateStickerLastUsed,
|
||||||
addStickerPackReference,
|
addStickerPackReference,
|
||||||
|
@ -275,6 +279,13 @@ const dataInterface: ServerInterface = {
|
||||||
getStickerCount,
|
getStickerCount,
|
||||||
deleteStickerPack,
|
deleteStickerPack,
|
||||||
getAllStickerPacks,
|
getAllStickerPacks,
|
||||||
|
addUninstalledStickerPack,
|
||||||
|
removeUninstalledStickerPack,
|
||||||
|
getInstalledStickerPacks,
|
||||||
|
getUninstalledStickerPacks,
|
||||||
|
installStickerPack,
|
||||||
|
uninstallStickerPack,
|
||||||
|
getStickerPackInfo,
|
||||||
getAllStickers,
|
getAllStickers,
|
||||||
getRecentStickers,
|
getRecentStickers,
|
||||||
clearAllErrorStickerPackAttempts,
|
clearAllErrorStickerPackAttempts,
|
||||||
|
@ -3446,6 +3457,10 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
|
||||||
status,
|
status,
|
||||||
stickerCount,
|
stickerCount,
|
||||||
title,
|
title,
|
||||||
|
storageID,
|
||||||
|
storageVersion,
|
||||||
|
storageUnknownFields,
|
||||||
|
storageNeedsSync,
|
||||||
} = pack;
|
} = pack;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new Error(
|
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>(
|
.prepare<Query>(
|
||||||
`
|
`
|
||||||
SELECT id
|
SELECT id
|
||||||
|
@ -3461,7 +3491,7 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
|
||||||
WHERE id = $id;
|
WHERE id = $id;
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.all({ id });
|
.get({ id });
|
||||||
const payload = {
|
const payload = {
|
||||||
attemptedStatus: attemptedStatus ?? null,
|
attemptedStatus: attemptedStatus ?? null,
|
||||||
author,
|
author,
|
||||||
|
@ -3475,9 +3505,14 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
|
||||||
status,
|
status,
|
||||||
stickerCount,
|
stickerCount,
|
||||||
title,
|
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>(
|
db.prepare<Query>(
|
||||||
`
|
`
|
||||||
UPDATE sticker_packs SET
|
UPDATE sticker_packs SET
|
||||||
|
@ -3491,7 +3526,12 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
|
||||||
lastUsed = $lastUsed,
|
lastUsed = $lastUsed,
|
||||||
status = $status,
|
status = $status,
|
||||||
stickerCount = $stickerCount,
|
stickerCount = $stickerCount,
|
||||||
title = $title
|
title = $title,
|
||||||
|
position = $position,
|
||||||
|
storageID = $storageID,
|
||||||
|
storageVersion = $storageVersion,
|
||||||
|
storageUnknownFields = $storageUnknownFields,
|
||||||
|
storageNeedsSync = $storageNeedsSync
|
||||||
WHERE id = $id;
|
WHERE id = $id;
|
||||||
`
|
`
|
||||||
).run(payload);
|
).run(payload);
|
||||||
|
@ -3513,7 +3553,12 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
|
||||||
lastUsed,
|
lastUsed,
|
||||||
status,
|
status,
|
||||||
stickerCount,
|
stickerCount,
|
||||||
title
|
title,
|
||||||
|
position,
|
||||||
|
storageID,
|
||||||
|
storageVersion,
|
||||||
|
storageUnknownFields,
|
||||||
|
storageNeedsSync
|
||||||
) values (
|
) values (
|
||||||
$attemptedStatus,
|
$attemptedStatus,
|
||||||
$author,
|
$author,
|
||||||
|
@ -3526,16 +3571,21 @@ async function createOrUpdateStickerPack(pack: StickerPackType): Promise<void> {
|
||||||
$lastUsed,
|
$lastUsed,
|
||||||
$status,
|
$status,
|
||||||
$stickerCount,
|
$stickerCount,
|
||||||
$title
|
$title,
|
||||||
|
$position,
|
||||||
|
$storageID,
|
||||||
|
$storageVersion,
|
||||||
|
$storageUnknownFields,
|
||||||
|
$storageNeedsSync
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
).run(payload);
|
).run(payload);
|
||||||
}
|
}
|
||||||
async function updateStickerPackStatus(
|
function updateStickerPackStatusSync(
|
||||||
id: string,
|
id: string,
|
||||||
status: StickerPackStatusType,
|
status: StickerPackStatusType,
|
||||||
options?: { timestamp: number }
|
options?: { timestamp: number }
|
||||||
): Promise<void> {
|
): void {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
const timestamp = options ? options.timestamp || Date.now() : Date.now();
|
const timestamp = options ? options.timestamp || Date.now() : Date.now();
|
||||||
const installedAt = status === 'installed' ? timestamp : null;
|
const installedAt = status === 'installed' ? timestamp : null;
|
||||||
|
@ -3552,6 +3602,61 @@ async function updateStickerPackStatus(
|
||||||
installedAt,
|
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> {
|
async function clearAllErrorStickerPackAttempts(): Promise<void> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
|
|
||||||
|
@ -3823,13 +3928,160 @@ async function getAllStickerPacks(): Promise<Array<StickerPackType>> {
|
||||||
.prepare<EmptyQuery>(
|
.prepare<EmptyQuery>(
|
||||||
`
|
`
|
||||||
SELECT * FROM sticker_packs
|
SELECT * FROM sticker_packs
|
||||||
ORDER BY installedAt DESC, createdAt DESC
|
ORDER BY position ASC, id ASC
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
return rows || [];
|
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>> {
|
async function getAllStickers(): Promise<Array<StickerType>> {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
|
|
||||||
|
|
62
ts/sql/migrations/65-add-storage-id-to-stickers.ts
Normal file
62
ts/sql/migrations/65-add-storage-id-to-stickers.ts
Normal 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!');
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ import updateToSchemaVersion61 from './61-distribution-list-storage';
|
||||||
import updateToSchemaVersion62 from './62-add-urgent-to-send-log';
|
import updateToSchemaVersion62 from './62-add-urgent-to-send-log';
|
||||||
import updateToSchemaVersion63 from './63-add-urgent-to-unprocessed';
|
import updateToSchemaVersion63 from './63-add-urgent-to-unprocessed';
|
||||||
import updateToSchemaVersion64 from './64-uuid-column-for-pre-keys';
|
import updateToSchemaVersion64 from './64-uuid-column-for-pre-keys';
|
||||||
|
import updateToSchemaVersion65 from './65-add-storage-id-to-stickers';
|
||||||
|
|
||||||
function updateToSchemaVersion1(
|
function updateToSchemaVersion1(
|
||||||
currentVersion: number,
|
currentVersion: number,
|
||||||
|
@ -1943,6 +1944,7 @@ export const SCHEMA_VERSIONS = [
|
||||||
updateToSchemaVersion62,
|
updateToSchemaVersion62,
|
||||||
updateToSchemaVersion63,
|
updateToSchemaVersion63,
|
||||||
updateToSchemaVersion64,
|
updateToSchemaVersion64,
|
||||||
|
updateToSchemaVersion65,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function updateSchema(db: Database, logger: LoggerType): void {
|
export function updateSchema(db: Database, logger: LoggerType): void {
|
||||||
|
|
|
@ -6,7 +6,9 @@ import { isNumber, last } from 'lodash';
|
||||||
|
|
||||||
export type EmptyQuery = [];
|
export type EmptyQuery = [];
|
||||||
export type ArrayQuery = Array<Array<null | number | bigint | string>>;
|
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 JSONRows = Array<{ readonly json: string }>;
|
||||||
|
|
||||||
export type TableType =
|
export type TableType =
|
||||||
|
|
|
@ -14,13 +14,13 @@ import {
|
||||||
downloadStickerPack as externalDownloadStickerPack,
|
downloadStickerPack as externalDownloadStickerPack,
|
||||||
maybeDeletePack,
|
maybeDeletePack,
|
||||||
} from '../../types/Stickers';
|
} from '../../types/Stickers';
|
||||||
|
import { storageServiceUploadJob } from '../../services/storage';
|
||||||
import { sendStickerPackSync } from '../../shims/textsecure';
|
import { sendStickerPackSync } from '../../shims/textsecure';
|
||||||
import { trigger } from '../../shims/events';
|
import { trigger } from '../../shims/events';
|
||||||
|
|
||||||
import type { NoopActionType } from './noop';
|
import type { NoopActionType } from './noop';
|
||||||
|
|
||||||
const { getRecentStickers, updateStickerLastUsed, updateStickerPackStatus } =
|
const { getRecentStickers, updateStickerLastUsed } = dataInterface;
|
||||||
dataInterface;
|
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -204,7 +204,7 @@ function downloadStickerPack(
|
||||||
function installStickerPack(
|
function installStickerPack(
|
||||||
packId: string,
|
packId: string,
|
||||||
packKey: string,
|
packKey: string,
|
||||||
options: { fromSync: boolean } | null = null
|
options: { fromSync?: boolean; fromStorageService?: boolean } = {}
|
||||||
): InstallStickerPackAction {
|
): InstallStickerPackAction {
|
||||||
return {
|
return {
|
||||||
type: 'stickers/INSTALL_STICKER_PACK',
|
type: 'stickers/INSTALL_STICKER_PACK',
|
||||||
|
@ -214,25 +214,28 @@ function installStickerPack(
|
||||||
async function doInstallStickerPack(
|
async function doInstallStickerPack(
|
||||||
packId: string,
|
packId: string,
|
||||||
packKey: string,
|
packKey: string,
|
||||||
options: { fromSync: boolean } | null
|
options: { fromSync?: boolean; fromStorageService?: boolean } = {}
|
||||||
): Promise<InstallStickerPackPayloadType> {
|
): Promise<InstallStickerPackPayloadType> {
|
||||||
const { fromSync } = options || { fromSync: false };
|
const { fromSync = false, fromStorageService = false } = options;
|
||||||
|
|
||||||
const status = 'installed';
|
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
await updateStickerPackStatus(packId, status, { timestamp });
|
await dataInterface.installStickerPack(packId, timestamp);
|
||||||
|
|
||||||
if (!fromSync) {
|
if (!fromSync && !fromStorageService) {
|
||||||
// Kick this off, but don't wait for it
|
// Kick this off, but don't wait for it
|
||||||
sendStickerPackSync(packId, packKey, true);
|
sendStickerPackSync(packId, packKey, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!fromStorageService) {
|
||||||
|
storageServiceUploadJob();
|
||||||
|
}
|
||||||
|
|
||||||
const recentStickers = await getRecentStickers();
|
const recentStickers = await getRecentStickers();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
packId,
|
packId,
|
||||||
fromSync,
|
fromSync,
|
||||||
status,
|
status: 'installed',
|
||||||
installedAt: timestamp,
|
installedAt: timestamp,
|
||||||
recentStickers: recentStickers.map(item => ({
|
recentStickers: recentStickers.map(item => ({
|
||||||
packId: item.packId,
|
packId: item.packId,
|
||||||
|
@ -243,7 +246,7 @@ async function doInstallStickerPack(
|
||||||
function uninstallStickerPack(
|
function uninstallStickerPack(
|
||||||
packId: string,
|
packId: string,
|
||||||
packKey: string,
|
packKey: string,
|
||||||
options: { fromSync: boolean } | null = null
|
options: { fromSync?: boolean; fromStorageService?: boolean } = {}
|
||||||
): UninstallStickerPackAction {
|
): UninstallStickerPackAction {
|
||||||
return {
|
return {
|
||||||
type: 'stickers/UNINSTALL_STICKER_PACK',
|
type: 'stickers/UNINSTALL_STICKER_PACK',
|
||||||
|
@ -253,27 +256,31 @@ function uninstallStickerPack(
|
||||||
async function doUninstallStickerPack(
|
async function doUninstallStickerPack(
|
||||||
packId: string,
|
packId: string,
|
||||||
packKey: string,
|
packKey: string,
|
||||||
options: { fromSync: boolean } | null
|
options: { fromSync?: boolean; fromStorageService?: boolean } = {}
|
||||||
): Promise<UninstallStickerPackPayloadType> {
|
): Promise<UninstallStickerPackPayloadType> {
|
||||||
const { fromSync } = options || { fromSync: false };
|
const { fromSync = false, fromStorageService = false } = options;
|
||||||
|
|
||||||
const status = 'downloaded';
|
const timestamp = Date.now();
|
||||||
await updateStickerPackStatus(packId, status);
|
await dataInterface.uninstallStickerPack(packId, timestamp);
|
||||||
|
|
||||||
// If there are no more references, it should be removed
|
// If there are no more references, it should be removed
|
||||||
await maybeDeletePack(packId);
|
await maybeDeletePack(packId);
|
||||||
|
|
||||||
if (!fromSync) {
|
if (!fromSync && !fromStorageService) {
|
||||||
// Kick this off, but don't wait for it
|
// Kick this off, but don't wait for it
|
||||||
sendStickerPackSync(packId, packKey, false);
|
sendStickerPackSync(packId, packKey, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!fromStorageService) {
|
||||||
|
storageServiceUploadJob();
|
||||||
|
}
|
||||||
|
|
||||||
const recentStickers = await getRecentStickers();
|
const recentStickers = await getRecentStickers();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
packId,
|
packId,
|
||||||
fromSync,
|
fromSync,
|
||||||
status,
|
status: 'downloaded',
|
||||||
installedAt: undefined,
|
installedAt: undefined,
|
||||||
recentStickers: recentStickers.map(item => ({
|
recentStickers: recentStickers.map(item => ({
|
||||||
packId: item.packId,
|
packId: item.packId,
|
||||||
|
@ -313,7 +320,7 @@ function stickerPackUpdated(
|
||||||
function useSticker(
|
function useSticker(
|
||||||
packId: string,
|
packId: string,
|
||||||
stickerId: number,
|
stickerId: number,
|
||||||
time = Date.now()
|
time?: number
|
||||||
): UseStickerAction {
|
): UseStickerAction {
|
||||||
return {
|
return {
|
||||||
type: 'stickers/USE_STICKER',
|
type: 'stickers/USE_STICKER',
|
||||||
|
|
|
@ -61,6 +61,7 @@ export async function initStorage(
|
||||||
state = state.updateAccount({
|
state = state.updateAccount({
|
||||||
profileKey: phone.profileKey.serialize(),
|
profileKey: phone.profileKey.serialize(),
|
||||||
e164: phone.device.number,
|
e164: phone.device.number,
|
||||||
|
givenName: phone.profileName,
|
||||||
});
|
});
|
||||||
|
|
||||||
state = state
|
state = state
|
||||||
|
@ -76,6 +77,7 @@ export async function initStorage(
|
||||||
|
|
||||||
identityKey: contact.publicKey.serialize(),
|
identityKey: contact.publicKey.serialize(),
|
||||||
profileKey: contact.profileKey.serialize(),
|
profileKey: contact.profileKey.serialize(),
|
||||||
|
givenName: contact.profileName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
312
ts/test-mock/storage/sticker_test.ts
Normal file
312
ts/test-mock/storage/sticker_test.ts
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { range } from 'lodash';
|
||||||
|
import { Proto } from '@signalapp/mock-server';
|
||||||
|
import type { StorageStateRecord } from '@signalapp/mock-server';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import * as durations from '../../util/durations';
|
||||||
|
import type { App, Bootstrap } from './fixtures';
|
||||||
|
import { initStorage, debug } from './fixtures';
|
||||||
|
|
||||||
|
const { StickerPackOperation } = Proto.SyncMessage;
|
||||||
|
|
||||||
|
const FIXTURES = path.join(__dirname, '..', '..', '..', 'fixtures');
|
||||||
|
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
|
||||||
|
|
||||||
|
const EMPTY = new Uint8Array(0);
|
||||||
|
|
||||||
|
export type StickerPackType = Readonly<{
|
||||||
|
id: Buffer;
|
||||||
|
key: Buffer;
|
||||||
|
stickerCount: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const STICKER_PACKS: ReadonlyArray<StickerPackType> = [
|
||||||
|
{
|
||||||
|
id: Buffer.from('c40ed069cdc2b91eccfccf25e6bcddfc', 'hex'),
|
||||||
|
key: Buffer.from(
|
||||||
|
'cefadd6e81c128680aead1711eb5c92c10f63bdfbc78528a4519ba682de396e4',
|
||||||
|
'hex'
|
||||||
|
),
|
||||||
|
stickerCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: Buffer.from('ae8fedafda4768fd3384d4b3b9db963d', 'hex'),
|
||||||
|
key: Buffer.from(
|
||||||
|
'53f4aa8b95e1c2e75afab2328fe67eb6d7affbcd4f50cd4da89dfc325dbc73ca',
|
||||||
|
'hex'
|
||||||
|
),
|
||||||
|
stickerCount: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getStickerPackLink(pack: StickerPackType): string {
|
||||||
|
return (
|
||||||
|
`https://signal.art/addstickers/#pack_id=${pack.id.toString('hex')}&` +
|
||||||
|
`pack_key=${pack.key.toString('hex')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStickerPackRecordPredicate(
|
||||||
|
pack: StickerPackType
|
||||||
|
): (record: StorageStateRecord) => boolean {
|
||||||
|
return ({ type, record }: StorageStateRecord): boolean => {
|
||||||
|
if (type !== IdentifierType.STICKER_PACK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return pack.id.equals(record.stickerPack?.packId ?? EMPTY);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('storage service', function needsName() {
|
||||||
|
this.timeout(durations.MINUTE);
|
||||||
|
|
||||||
|
let bootstrap: Bootstrap;
|
||||||
|
let app: App;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
({ bootstrap, app } = await initStorage());
|
||||||
|
|
||||||
|
const { server } = bootstrap;
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
STICKER_PACKS.map(async ({ id, stickerCount }) => {
|
||||||
|
const hexId = id.toString('hex');
|
||||||
|
|
||||||
|
await server.storeStickerPack({
|
||||||
|
id,
|
||||||
|
manifest: await fs.readFile(
|
||||||
|
path.join(FIXTURES, `stickerpack-${hexId}.bin`)
|
||||||
|
),
|
||||||
|
stickers: await Promise.all(
|
||||||
|
range(0, stickerCount).map(async index =>
|
||||||
|
fs.readFile(
|
||||||
|
path.join(FIXTURES, `stickerpack-${hexId}-${index}.bin`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function after() {
|
||||||
|
if (!bootstrap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentTest?.state !== 'passed') {
|
||||||
|
await bootstrap.saveLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
await app.close();
|
||||||
|
await bootstrap.teardown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should install/uninstall stickers', async () => {
|
||||||
|
const { phone, desktop, contacts } = bootstrap;
|
||||||
|
const [firstContact] = contacts;
|
||||||
|
|
||||||
|
const window = await app.getWindow();
|
||||||
|
|
||||||
|
const leftPane = window.locator('.left-pane-wrapper');
|
||||||
|
const conversationStack = window.locator('.conversation-stack');
|
||||||
|
|
||||||
|
debug('sending two sticker pack links');
|
||||||
|
await firstContact.sendText(
|
||||||
|
desktop,
|
||||||
|
`First sticker pack ${getStickerPackLink(STICKER_PACKS[0])}`
|
||||||
|
);
|
||||||
|
await firstContact.sendText(
|
||||||
|
desktop,
|
||||||
|
`Second sticker pack ${getStickerPackLink(STICKER_PACKS[1])}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await leftPane
|
||||||
|
.locator(
|
||||||
|
'_react=ConversationListItem' +
|
||||||
|
`[title = ${JSON.stringify(firstContact.profileName)}]`
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
{
|
||||||
|
debug('installing first sticker pack via UI');
|
||||||
|
const state = await phone.expectStorageState('initial state');
|
||||||
|
|
||||||
|
await conversationStack
|
||||||
|
.locator(`a:has-text("${STICKER_PACKS[0].id.toString('hex')}")`)
|
||||||
|
.click({ noWaitAfter: true });
|
||||||
|
await window
|
||||||
|
.locator(
|
||||||
|
'.module-sticker-manager__preview-modal__container button >> "Install"'
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
debug('waiting for sync message');
|
||||||
|
const { syncMessage } = await phone.waitForSyncMessage(entry =>
|
||||||
|
Boolean(entry.syncMessage.stickerPackOperation?.length)
|
||||||
|
);
|
||||||
|
const [syncOp] = syncMessage.stickerPackOperation ?? [];
|
||||||
|
assert.isTrue(STICKER_PACKS[0].id.equals(syncOp?.packId ?? EMPTY));
|
||||||
|
assert.isTrue(STICKER_PACKS[0].key.equals(syncOp?.packKey ?? EMPTY));
|
||||||
|
assert.strictEqual(syncOp?.type, StickerPackOperation.Type.INSTALL);
|
||||||
|
|
||||||
|
debug('waiting for storage service update');
|
||||||
|
const stateAfter = await phone.waitForStorageState({ after: state });
|
||||||
|
const stickerPack = stateAfter.findRecord(
|
||||||
|
getStickerPackRecordPredicate(STICKER_PACKS[0])
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
stickerPack,
|
||||||
|
'New storage state should have sticker pack record'
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
STICKER_PACKS[0].key.equals(
|
||||||
|
stickerPack?.record.stickerPack?.packKey ?? EMPTY
|
||||||
|
),
|
||||||
|
'Wrong sticker pack key'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
stickerPack?.record.stickerPack?.position,
|
||||||
|
6,
|
||||||
|
'Wrong sticker pack position'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
debug('uninstalling first sticker pack via UI');
|
||||||
|
const state = await phone.expectStorageState('initial state');
|
||||||
|
|
||||||
|
await conversationStack
|
||||||
|
.locator(`a:has-text("${STICKER_PACKS[0].id.toString('hex')}")`)
|
||||||
|
.click({ noWaitAfter: true });
|
||||||
|
await window
|
||||||
|
.locator(
|
||||||
|
'.module-sticker-manager__preview-modal__container button ' +
|
||||||
|
'>> "Uninstall"'
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Confirm
|
||||||
|
await window.locator('.module-Modal button >> "Uninstall"').click();
|
||||||
|
|
||||||
|
debug('waiting for sync message');
|
||||||
|
const { syncMessage } = await phone.waitForSyncMessage(entry =>
|
||||||
|
Boolean(entry.syncMessage.stickerPackOperation?.length)
|
||||||
|
);
|
||||||
|
const [syncOp] = syncMessage.stickerPackOperation ?? [];
|
||||||
|
assert.isTrue(STICKER_PACKS[0].id.equals(syncOp?.packId ?? EMPTY));
|
||||||
|
assert.strictEqual(syncOp?.type, StickerPackOperation.Type.REMOVE);
|
||||||
|
|
||||||
|
debug('waiting for storage service update');
|
||||||
|
const stateAfter = await phone.waitForStorageState({ after: state });
|
||||||
|
const stickerPack = stateAfter.findRecord(
|
||||||
|
getStickerPackRecordPredicate(STICKER_PACKS[0])
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
stickerPack,
|
||||||
|
'New storage state should have sticker pack record'
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
stickerPack?.record.stickerPack?.packKey,
|
||||||
|
EMPTY,
|
||||||
|
'Sticker pack key should be removed'
|
||||||
|
);
|
||||||
|
const deletedAt =
|
||||||
|
stickerPack?.record.stickerPack?.deletedAtTimestamp?.toNumber() ?? 0;
|
||||||
|
assert.isAbove(
|
||||||
|
deletedAt,
|
||||||
|
Date.now() - durations.HOUR,
|
||||||
|
'Sticker pack should have deleted at timestamp'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('opening sticker picker');
|
||||||
|
conversationStack
|
||||||
|
.locator('.CompositionArea .module-sticker-button__button')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
const stickerPicker = conversationStack.locator('.module-sticker-picker');
|
||||||
|
|
||||||
|
{
|
||||||
|
debug('installing first sticker pack via storage service');
|
||||||
|
const state = await phone.expectStorageState('initial state');
|
||||||
|
|
||||||
|
await phone.setStorageState(
|
||||||
|
state.updateRecord(
|
||||||
|
getStickerPackRecordPredicate(STICKER_PACKS[0]),
|
||||||
|
record => ({
|
||||||
|
...record,
|
||||||
|
stickerPack: {
|
||||||
|
...record?.stickerPack,
|
||||||
|
packKey: STICKER_PACKS[0].key,
|
||||||
|
position: 7,
|
||||||
|
deletedAtTimestamp: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await phone.sendFetchStorage({
|
||||||
|
timestamp: bootstrap.getTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
debug('waiting for sticker pack to become visible');
|
||||||
|
stickerPicker
|
||||||
|
.locator(
|
||||||
|
'button.module-sticker-picker__header__button' +
|
||||||
|
`[key="${STICKER_PACKS[0].id.toString('hex')}"]`
|
||||||
|
)
|
||||||
|
.waitFor();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
debug('installing second sticker pack via sync message');
|
||||||
|
const state = await phone.expectStorageState('initial state');
|
||||||
|
|
||||||
|
await phone.sendStickerPackSync({
|
||||||
|
type: 'install',
|
||||||
|
packId: STICKER_PACKS[1].id,
|
||||||
|
packKey: STICKER_PACKS[1].key,
|
||||||
|
timestamp: bootstrap.getTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
debug('waiting for sticker pack to become visible');
|
||||||
|
stickerPicker
|
||||||
|
.locator(
|
||||||
|
'button.module-sticker-picker__header__button' +
|
||||||
|
`[key="${STICKER_PACKS[1].id.toString('hex')}"]`
|
||||||
|
)
|
||||||
|
.waitFor();
|
||||||
|
|
||||||
|
debug('waiting for storage service update');
|
||||||
|
const stateAfter = await phone.waitForStorageState({ after: state });
|
||||||
|
const stickerPack = stateAfter.findRecord(
|
||||||
|
getStickerPackRecordPredicate(STICKER_PACKS[1])
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
stickerPack,
|
||||||
|
'New storage state should have sticker pack record'
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
STICKER_PACKS[1].key.equals(
|
||||||
|
stickerPack?.record.stickerPack?.packKey ?? EMPTY
|
||||||
|
),
|
||||||
|
'Wrong sticker pack key'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
stickerPack?.record.stickerPack?.position,
|
||||||
|
6,
|
||||||
|
'Wrong sticker pack position'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('Verifying the final manifest version');
|
||||||
|
const finalState = await phone.expectStorageState('consistency check');
|
||||||
|
|
||||||
|
assert.strictEqual(finalState.version, 5);
|
||||||
|
});
|
||||||
|
});
|
|
@ -2357,4 +2357,36 @@ describe('SQL migrations test', () => {
|
||||||
assert.strictEqual(payload.urgent, 1);
|
assert.strictEqual(payload.urgent, 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateToSchemaVersion65', () => {
|
||||||
|
it('initializes sticker pack positions', () => {
|
||||||
|
updateToVersion(64);
|
||||||
|
|
||||||
|
db.exec(
|
||||||
|
`
|
||||||
|
INSERT INTO sticker_packs
|
||||||
|
(id, key, lastUsed)
|
||||||
|
VALUES
|
||||||
|
("a", "key-1", 1),
|
||||||
|
("b", "key-2", 2),
|
||||||
|
("c", "key-3", 3);
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
updateToVersion(65);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
db
|
||||||
|
.prepare(
|
||||||
|
'SELECT id, position FROM sticker_packs ORDER BY position DESC'
|
||||||
|
)
|
||||||
|
.all(),
|
||||||
|
[
|
||||||
|
{ id: 'a', position: 2 },
|
||||||
|
{ id: 'b', position: 1 },
|
||||||
|
{ id: 'c', position: 0 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -96,6 +96,8 @@ const STICKER_PACK_DEFAULTS: StickerPackType = {
|
||||||
stickerCount: 0,
|
stickerCount: 0,
|
||||||
stickers: {},
|
stickers: {},
|
||||||
title: '',
|
title: '',
|
||||||
|
|
||||||
|
storageNeedsSync: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const VALID_PACK_ID_REGEXP = /^[0-9a-f]{32}$/i;
|
const VALID_PACK_ID_REGEXP = /^[0-9a-f]{32}$/i;
|
||||||
|
@ -529,6 +531,7 @@ export async function downloadEphemeralPack(
|
||||||
export type DownloadStickerPackOptions = Readonly<{
|
export type DownloadStickerPackOptions = Readonly<{
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
fromSync?: boolean;
|
fromSync?: boolean;
|
||||||
|
fromStorageService?: boolean;
|
||||||
finalStatus?: StickerPackStatusType;
|
finalStatus?: StickerPackStatusType;
|
||||||
suppressError?: boolean;
|
suppressError?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
@ -558,6 +561,7 @@ async function doDownloadStickerPack(
|
||||||
finalStatus = 'downloaded',
|
finalStatus = 'downloaded',
|
||||||
messageId,
|
messageId,
|
||||||
fromSync = false,
|
fromSync = false,
|
||||||
|
fromStorageService = false,
|
||||||
suppressError = false,
|
suppressError = false,
|
||||||
}: DownloadStickerPackOptions
|
}: DownloadStickerPackOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -668,6 +672,7 @@ async function doDownloadStickerPack(
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
stickers: {},
|
stickers: {},
|
||||||
|
storageNeedsSync: !fromStorageService,
|
||||||
...pick(proto, ['title', 'author']),
|
...pick(proto, ['title', 'author']),
|
||||||
};
|
};
|
||||||
await Data.createOrUpdateStickerPack(pack);
|
await Data.createOrUpdateStickerPack(pack);
|
||||||
|
@ -748,7 +753,10 @@ async function doDownloadStickerPack(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalStatus === 'installed') {
|
if (finalStatus === 'installed') {
|
||||||
await installStickerPack(packId, packKey, { fromSync });
|
await installStickerPack(packId, packKey, {
|
||||||
|
fromSync,
|
||||||
|
fromStorageService,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Mark the pack as complete
|
// Mark the pack as complete
|
||||||
await Data.updateStickerPackStatus(packId, finalStatus);
|
await Data.updateStickerPackStatus(packId, finalStatus);
|
||||||
|
@ -888,7 +896,7 @@ export async function deletePackReference(
|
||||||
}
|
}
|
||||||
|
|
||||||
// The override; doesn't honor our ref-counting scheme - just deletes it all.
|
// The override; doesn't honor our ref-counting scheme - just deletes it all.
|
||||||
export async function deletePack(packId: string): Promise<void> {
|
async function deletePack(packId: string): Promise<void> {
|
||||||
const isBlessed = Boolean(BLESSED_PACKS[packId]);
|
const isBlessed = Boolean(BLESSED_PACKS[packId]);
|
||||||
if (isBlessed) {
|
if (isBlessed) {
|
||||||
return;
|
return;
|
||||||
|
|
Loading…
Reference in a new issue