Support additional sticker states
Co-authored-by: scott@signal.org Co-authored-by: ken@signal.org
This commit is contained in:
parent
41880cfe66
commit
be5d0837f8
35 changed files with 925 additions and 249 deletions
|
@ -2,6 +2,7 @@
|
|||
textsecure,
|
||||
Signal,
|
||||
log,
|
||||
navigator,
|
||||
reduxStore,
|
||||
reduxActions,
|
||||
URL
|
||||
|
@ -9,7 +10,7 @@
|
|||
|
||||
const BLESSED_PACKS = {};
|
||||
|
||||
const { isNumber, pick, reject, groupBy } = require('lodash');
|
||||
const { isNumber, pick, reject, groupBy, values } = require('lodash');
|
||||
const pMap = require('p-map');
|
||||
const Queue = require('p-queue');
|
||||
const qs = require('qs');
|
||||
|
@ -34,6 +35,7 @@ module.exports = {
|
|||
deletePack,
|
||||
deletePackReference,
|
||||
downloadStickerPack,
|
||||
downloadEphemeralPack,
|
||||
getDataFromLink,
|
||||
getInitialState,
|
||||
getInstalledStickerPacks,
|
||||
|
@ -44,6 +46,8 @@ module.exports = {
|
|||
maybeDeletePack,
|
||||
downloadQueuedPacks,
|
||||
redactPackId,
|
||||
removeEphemeralPack,
|
||||
savePackMetadata,
|
||||
};
|
||||
|
||||
let initialState = null;
|
||||
|
@ -88,8 +92,8 @@ function getInstalledStickerPacks() {
|
|||
return [];
|
||||
}
|
||||
|
||||
const values = Object.values(packs);
|
||||
return values.filter(pack => pack.status === 'installed');
|
||||
const items = Object.values(packs);
|
||||
return items.filter(pack => pack.status === 'installed');
|
||||
}
|
||||
|
||||
function downloadQueuedPacks() {
|
||||
|
@ -113,7 +117,7 @@ function capturePacksToDownload(existingPackLookup) {
|
|||
const existing = existingPackLookup[id];
|
||||
if (
|
||||
!existing ||
|
||||
(existing.status !== 'advertised' && existing.status !== 'installed')
|
||||
(existing.status !== 'downloaded' && existing.status !== 'installed')
|
||||
) {
|
||||
toDownload[id] = {
|
||||
id,
|
||||
|
@ -130,6 +134,18 @@ function capturePacksToDownload(existingPackLookup) {
|
|||
}
|
||||
|
||||
const existing = existingPackLookup[id];
|
||||
|
||||
// These packs should never end up in the database, but if they do we'll delete them
|
||||
if (existing.status === 'ephemeral') {
|
||||
deletePack(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't automatically download these; not until a user action kicks it off
|
||||
if (existing.status === 'known') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (doesPackNeedDownload(existing)) {
|
||||
toDownload[id] = {
|
||||
id,
|
||||
|
@ -147,14 +163,23 @@ function doesPackNeedDownload(pack) {
|
|||
return true;
|
||||
}
|
||||
|
||||
const stickerCount = Object.keys(pack.stickers || {}).length;
|
||||
return (
|
||||
!pack.status ||
|
||||
pack.status === 'error' ||
|
||||
pack.status === 'pending' ||
|
||||
!pack.stickerCount ||
|
||||
stickerCount < pack.stickerCount
|
||||
);
|
||||
const { status, stickerCount } = pack;
|
||||
const stickersDownloaded = Object.keys(pack.stickers || {}).length;
|
||||
|
||||
if (
|
||||
(status === 'installed' || status === 'downloaded') &&
|
||||
stickerCount > 0 &&
|
||||
stickersDownloaded >= stickerCount
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we don't understand a pack's status, we'll download it
|
||||
// If a pack has any other status, we'll download it
|
||||
// If a pack has zero stickers in it, we'll download it
|
||||
// If a pack doesn't have enough downloaded stickers, we'll download it
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function getPacksForRedux() {
|
||||
|
@ -209,10 +234,15 @@ async function decryptSticker(packKey, ciphertext) {
|
|||
return plaintext;
|
||||
}
|
||||
|
||||
async function downloadSticker(packId, packKey, proto) {
|
||||
async function downloadSticker(packId, packKey, proto, options) {
|
||||
const { ephemeral } = options || {};
|
||||
|
||||
const ciphertext = await textsecure.messaging.getSticker(packId, proto.id);
|
||||
const plaintext = await decryptSticker(packKey, ciphertext);
|
||||
const sticker = await Signal.Migrations.processNewSticker(plaintext);
|
||||
|
||||
const sticker = ephemeral
|
||||
? await Signal.Migrations.processNewEphemeralSticker(plaintext, options)
|
||||
: await Signal.Migrations.processNewSticker(plaintext, options);
|
||||
|
||||
return {
|
||||
...pick(proto, ['id', 'emoji']),
|
||||
|
@ -221,6 +251,156 @@ async function downloadSticker(packId, packKey, proto) {
|
|||
};
|
||||
}
|
||||
|
||||
async function savePackMetadata(packId, packKey, options = {}) {
|
||||
const { messageId } = options;
|
||||
|
||||
const existing = getStickerPack(packId);
|
||||
if (existing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { stickerPackAdded } = getReduxStickerActions();
|
||||
const pack = {
|
||||
id: packId,
|
||||
key: packKey,
|
||||
status: 'known',
|
||||
};
|
||||
stickerPackAdded(pack);
|
||||
|
||||
await createOrUpdateStickerPack(pack);
|
||||
if (messageId) {
|
||||
await addStickerPackReference(messageId, packId);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeEphemeralPack(packId) {
|
||||
const existing = getStickerPack(packId);
|
||||
if (
|
||||
existing.status !== 'ephemeral' &&
|
||||
!(existing.status === 'error' && existing.attemptedStatus === 'ephemeral')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { removeStickerPack } = getReduxStickerActions();
|
||||
removeStickerPack(packId);
|
||||
|
||||
const stickers = values(existing.stickers);
|
||||
const paths = stickers.map(sticker => sticker.path);
|
||||
await pMap(paths, Signal.Migrations.deleteTempFile, {
|
||||
concurrency: 3,
|
||||
});
|
||||
|
||||
// Remove it from database in case it made it there
|
||||
await deleteStickerPack(packId);
|
||||
}
|
||||
|
||||
async function downloadEphemeralPack(packId, packKey) {
|
||||
const {
|
||||
stickerAdded,
|
||||
stickerPackAdded,
|
||||
stickerPackUpdated,
|
||||
} = getReduxStickerActions();
|
||||
|
||||
const existingPack = getStickerPack(packId);
|
||||
if (existingPack) {
|
||||
log.warn(
|
||||
`Ephemeral download for pack ${redactPackId(
|
||||
packId
|
||||
)} requested, we already know about it. Skipping.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Synchronous placeholder to help with race conditions
|
||||
const placeholder = {
|
||||
id: packId,
|
||||
key: packKey,
|
||||
status: 'ephemeral',
|
||||
};
|
||||
stickerPackAdded(placeholder);
|
||||
|
||||
const ciphertext = await textsecure.messaging.getStickerPackManifest(
|
||||
packId
|
||||
);
|
||||
const plaintext = await decryptSticker(packKey, ciphertext);
|
||||
const proto = textsecure.protobuf.StickerPack.decode(plaintext);
|
||||
const firstStickerProto = proto.stickers ? proto.stickers[0] : null;
|
||||
const stickerCount = proto.stickers.length;
|
||||
|
||||
const coverProto = proto.cover || firstStickerProto;
|
||||
const coverStickerId = coverProto ? coverProto.id : null;
|
||||
|
||||
if (!coverProto || !isNumber(coverStickerId)) {
|
||||
throw new Error(
|
||||
`Sticker pack ${redactPackId(
|
||||
packId
|
||||
)} is malformed - it has no cover, and no stickers`
|
||||
);
|
||||
}
|
||||
|
||||
const nonCoverStickers = reject(
|
||||
proto.stickers,
|
||||
sticker => !isNumber(sticker.id) || sticker.id === coverStickerId
|
||||
);
|
||||
|
||||
const coverIncludedInList = nonCoverStickers.length < stickerCount;
|
||||
|
||||
const pack = {
|
||||
id: packId,
|
||||
key: packKey,
|
||||
coverStickerId,
|
||||
stickerCount,
|
||||
status: 'ephemeral',
|
||||
...pick(proto, ['title', 'author']),
|
||||
};
|
||||
stickerPackAdded(pack);
|
||||
|
||||
const downloadStickerJob = async stickerProto => {
|
||||
const stickerInfo = await downloadSticker(packId, packKey, stickerProto, {
|
||||
ephemeral: true,
|
||||
});
|
||||
const sticker = {
|
||||
...stickerInfo,
|
||||
isCoverOnly: !coverIncludedInList && stickerInfo.id === coverStickerId,
|
||||
};
|
||||
|
||||
const statusCheck = getStickerPackStatus(packId);
|
||||
if (statusCheck !== 'ephemeral') {
|
||||
throw new Error(
|
||||
`Ephemeral download for pack ${redactPackId(
|
||||
packId
|
||||
)} interrupted by status change. Status is now ${statusCheck}.`
|
||||
);
|
||||
}
|
||||
|
||||
stickerAdded(sticker);
|
||||
};
|
||||
|
||||
// Download the cover first
|
||||
await downloadStickerJob(coverProto);
|
||||
|
||||
// Then the rest
|
||||
await pMap(nonCoverStickers, downloadStickerJob, { concurrency: 3 });
|
||||
} catch (error) {
|
||||
// Because the user could install this pack while we are still downloading this
|
||||
// ephemeral pack, we don't want to go change its status unless we're still in
|
||||
// ephemeral mode.
|
||||
const statusCheck = getStickerPackStatus(packId);
|
||||
if (statusCheck === 'ephemeral') {
|
||||
stickerPackUpdated(packId, {
|
||||
attemptedStatus: 'ephemeral',
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
log.error(
|
||||
`Ephemeral download error for sticker pack ${redactPackId(packId)}:`,
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadStickerPack(packId, packKey, options = {}) {
|
||||
// This will ensure that only one download process is in progress at any given time
|
||||
return downloadQueue.add(async () => {
|
||||
|
@ -244,7 +424,12 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
installStickerPack,
|
||||
} = getReduxStickerActions();
|
||||
|
||||
const finalStatus = options.finalStatus || 'advertised';
|
||||
const finalStatus = options.finalStatus || 'downloaded';
|
||||
if (finalStatus !== 'downloaded' && finalStatus !== 'installed') {
|
||||
throw new Error(
|
||||
`doDownloadStickerPack: invalid finalStatus of ${finalStatus} requested.`
|
||||
);
|
||||
}
|
||||
|
||||
const existing = getStickerPack(packId);
|
||||
if (!doesPackNeedDownload(existing)) {
|
||||
|
@ -256,7 +441,10 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
return;
|
||||
}
|
||||
|
||||
const downloadAttempts = (existing ? existing.downloadAttempts || 0 : 0) + 1;
|
||||
// We don't count this as an attempt if we're offline
|
||||
const attemptIncrement = navigator.onLine ? 1 : 0;
|
||||
const downloadAttempts =
|
||||
(existing ? existing.downloadAttempts || 0 : 0) + attemptIncrement;
|
||||
if (downloadAttempts > 3) {
|
||||
log.warn(
|
||||
`Refusing to attempt another download for pack ${redactPackId(
|
||||
|
@ -280,6 +468,16 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
let nonCoverStickers;
|
||||
|
||||
try {
|
||||
// Synchronous placeholder to help with race conditions
|
||||
const placeholder = {
|
||||
id: packId,
|
||||
key: packKey,
|
||||
attemptedStatus: finalStatus,
|
||||
downloadAttempts,
|
||||
status: 'pending',
|
||||
};
|
||||
stickerPackAdded(placeholder);
|
||||
|
||||
const ciphertext = await textsecure.messaging.getStickerPackManifest(
|
||||
packId
|
||||
);
|
||||
|
@ -307,8 +505,10 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
coverIncludedInList = nonCoverStickers.length < stickerCount;
|
||||
|
||||
// status can be:
|
||||
// - 'known'
|
||||
// - 'ephemeral' (should not hit database)
|
||||
// - 'pending'
|
||||
// - 'advertised'
|
||||
// - 'downloaded'
|
||||
// - 'error'
|
||||
// - 'installed'
|
||||
const pack = {
|
||||
|
@ -365,6 +565,13 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
// Then the rest
|
||||
await pMap(nonCoverStickers, downloadStickerJob, { concurrency: 3 });
|
||||
|
||||
// Allow for the user marking this pack as installed in the middle of our download;
|
||||
// don't overwrite that status.
|
||||
const existingStatus = getStickerPackStatus(packId);
|
||||
if (existingStatus === 'installed') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (finalStatus === 'installed') {
|
||||
await installStickerPack(packId, packKey, { fromSync });
|
||||
} else {
|
||||
|
@ -380,11 +587,12 @@ async function doDownloadStickerPack(packId, packKey, options = {}) {
|
|||
error && error.stack ? error.stack : error
|
||||
);
|
||||
|
||||
const errorState = 'error';
|
||||
await updateStickerPackStatus(packId, errorState);
|
||||
const errorStatus = 'error';
|
||||
await updateStickerPackStatus(packId, errorStatus);
|
||||
if (stickerPackUpdated) {
|
||||
stickerPackUpdated(packId, {
|
||||
state: errorState,
|
||||
attemptedStatus: finalStatus,
|
||||
status: errorStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue