Implement sticker pack retrieval

Fixes #410
This commit is contained in:
AsamK 2021-06-13 13:37:25 +02:00
parent f40c351662
commit 2d068997c5
7 changed files with 253 additions and 14 deletions

View file

@ -18,6 +18,19 @@ public class JsonStickerPack {
@JsonProperty
public List<JsonSticker> stickers;
// For deserialization
private JsonStickerPack() {
}
public JsonStickerPack(
final String title, final String author, final JsonSticker cover, final List<JsonSticker> stickers
) {
this.title = title;
this.author = author;
this.cover = cover;
this.stickers = stickers;
}
public static class JsonSticker {
@JsonProperty
@ -28,5 +41,15 @@ public class JsonStickerPack {
@JsonProperty
public String contentType;
// For deserialization
private JsonSticker() {
}
public JsonSticker(final String emoji, final String file, final String contentType) {
this.emoji = emoji;
this.file = file;
this.contentType = contentType;
}
}
}

View file

@ -34,6 +34,9 @@ import org.asamk.signal.manager.helper.GroupV2Helper;
import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.helper.ProfileHelper;
import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
import org.asamk.signal.manager.jobs.Context;
import org.asamk.signal.manager.jobs.Job;
import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
@ -202,6 +205,7 @@ public class Manager implements Closeable {
private final PinHelper pinHelper;
private final AvatarStore avatarStore;
private final AttachmentStore attachmentStore;
private final StickerPackStore stickerPackStore;
private final SignalSessionLock sessionLock = new SignalSessionLock() {
private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
@ -275,6 +279,7 @@ public class Manager implements Closeable {
this::resolveSignalServiceAddress);
this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath());
}
public String getUsername() {
@ -1434,18 +1439,20 @@ public class Manager implements Closeable {
var messageSender = createMessageSender();
var packKey = KeyUtils.createStickerUploadKey();
var packId = messageSender.uploadStickerManifest(manifest, packKey);
var packIdString = messageSender.uploadStickerManifest(manifest, packKey);
var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString));
var sticker = new Sticker(StickerPackId.deserialize(Hex.fromStringCondensed(packId)), packKey);
var sticker = new Sticker(packId, packKey);
account.getStickerStore().updateSticker(sticker);
try {
return new URI("https",
"signal.art",
"/addstickers/",
"pack_id=" + URLEncoder.encode(packId, StandardCharsets.UTF_8) + "&pack_key=" + URLEncoder.encode(
Hex.toStringCondensed(packKey),
StandardCharsets.UTF_8)).toString();
"pack_id="
+ URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8)
+ "&pack_key="
+ URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)).toString();
} catch (URISyntaxException e) {
throw new AssertionError(e);
}
@ -1939,6 +1946,7 @@ public class Manager implements Closeable {
sticker = new Sticker(stickerPackId, messageSticker.getPackKey());
account.getStickerStore().updateSticker(sticker);
}
enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey()));
}
return actions;
}
@ -2461,16 +2469,23 @@ public class Manager implements Closeable {
continue;
}
final var stickerPackId = StickerPackId.deserialize(m.getPackId().get());
final var installed = !m.getType().isPresent()
|| m.getType().get() == StickerPackOperationMessage.Type.INSTALL;
var sticker = account.getStickerStore().getSticker(stickerPackId);
if (sticker == null) {
if (!m.getPackKey().isPresent()) {
continue;
if (m.getPackKey().isPresent()) {
if (sticker == null) {
sticker = new Sticker(stickerPackId, m.getPackKey().get());
}
if (installed) {
enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get()));
}
sticker = new Sticker(stickerPackId, m.getPackKey().get());
}
sticker.setInstalled(!m.getType().isPresent()
|| m.getType().get() == StickerPackOperationMessage.Type.INSTALL);
account.getStickerStore().updateSticker(sticker);
if (sticker != null) {
sticker.setInstalled(installed);
account.getStickerStore().updateSticker(sticker);
}
}
}
if (syncMessage.getFetchType().isPresent()) {
@ -2939,6 +2954,11 @@ public class Manager implements Closeable {
return account.getRecipientStore().resolveRecipientTrusted(address);
}
private void enqueueJob(Job job) {
var context = new Context(account, accountManager, messageReceiver, stickerPackStore);
job.run(context);
}
@Override
public void close() throws IOException {
close(true);

View file

@ -7,17 +7,22 @@ public class PathConfig {
private final File dataPath;
private final File attachmentsPath;
private final File avatarsPath;
private final File stickerPacksPath;
public static PathConfig createDefault(final File settingsPath) {
return new PathConfig(new File(settingsPath, "data"),
new File(settingsPath, "attachments"),
new File(settingsPath, "avatars"));
new File(settingsPath, "avatars"),
new File(settingsPath, "stickers"));
}
private PathConfig(final File dataPath, final File attachmentsPath, final File avatarsPath) {
private PathConfig(
final File dataPath, final File attachmentsPath, final File avatarsPath, final File stickerPacksPath
) {
this.dataPath = dataPath;
this.attachmentsPath = attachmentsPath;
this.avatarsPath = avatarsPath;
this.stickerPacksPath = stickerPacksPath;
}
public File getDataPath() {
@ -31,4 +36,8 @@ public class PathConfig {
public File getAvatarsPath() {
return avatarsPath;
}
public File getStickerPacksPath() {
return stickerPacksPath;
}
}

View file

@ -0,0 +1,65 @@
package org.asamk.signal.manager;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.asamk.signal.manager.storage.stickers.StickerPackId;
import org.asamk.signal.manager.util.IOUtils;
import org.whispersystems.signalservice.internal.util.Hex;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
public class StickerPackStore {
private final File stickersPath;
public StickerPackStore(final File stickersPath) {
this.stickersPath = stickersPath;
}
public boolean existsStickerPack(StickerPackId stickerPackId) {
return getStickerPackManifestFile(stickerPackId).exists();
}
public void storeManifest(StickerPackId stickerPackId, JsonStickerPack manifest) throws IOException {
try (OutputStream output = new FileOutputStream(getStickerPackManifestFile(stickerPackId))) {
try (var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) {
new ObjectMapper().writeValue(writer, manifest);
}
}
}
public void storeSticker(StickerPackId stickerPackId, int stickerId, StickerStorer storer) throws IOException {
createStickerPackDir(stickerPackId);
try (OutputStream output = new FileOutputStream(getStickerPackStickerFile(stickerPackId, stickerId))) {
storer.store(output);
}
}
private File getStickerPackManifestFile(StickerPackId stickerPackId) {
return new File(getStickerPackPath(stickerPackId), "manifest.json");
}
private File getStickerPackStickerFile(StickerPackId stickerPackId, int stickerId) {
return new File(getStickerPackPath(stickerPackId), String.valueOf(stickerId));
}
private File getStickerPackPath(StickerPackId stickerPackId) {
return new File(stickersPath, Hex.toStringCondensed(stickerPackId.serialize()));
}
private void createStickerPackDir(StickerPackId stickerPackId) throws IOException {
IOUtils.createPrivateDirectories(getStickerPackPath(stickerPackId));
}
@FunctionalInterface
public interface StickerStorer {
void store(OutputStream outputStream) throws IOException;
}
}

View file

@ -0,0 +1,42 @@
package org.asamk.signal.manager.jobs;
import org.asamk.signal.manager.StickerPackStore;
import org.asamk.signal.manager.storage.SignalAccount;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
public class Context {
private SignalAccount account;
private SignalServiceAccountManager accountManager;
private SignalServiceMessageReceiver messageReceiver;
private StickerPackStore stickerPackStore;
public Context(
final SignalAccount account,
final SignalServiceAccountManager accountManager,
final SignalServiceMessageReceiver messageReceiver,
final StickerPackStore stickerPackStore
) {
this.account = account;
this.accountManager = accountManager;
this.messageReceiver = messageReceiver;
this.stickerPackStore = stickerPackStore;
}
public SignalAccount getAccount() {
return account;
}
public SignalServiceAccountManager getAccountManager() {
return accountManager;
}
public SignalServiceMessageReceiver getMessageReceiver() {
return messageReceiver;
}
public StickerPackStore getStickerPackStore() {
return stickerPackStore;
}
}

View file

@ -0,0 +1,6 @@
package org.asamk.signal.manager.jobs;
public interface Job {
void run(Context context);
}

View file

@ -0,0 +1,74 @@
package org.asamk.signal.manager.jobs;
import org.asamk.signal.manager.JsonStickerPack;
import org.asamk.signal.manager.storage.stickers.StickerPackId;
import org.asamk.signal.manager.util.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.signalservice.internal.util.Hex;
import java.io.IOException;
import java.util.HashSet;
import java.util.stream.Collectors;
public class RetrieveStickerPackJob implements Job {
private final static Logger logger = LoggerFactory.getLogger(RetrieveStickerPackJob.class);
private final StickerPackId packId;
private final byte[] packKey;
public RetrieveStickerPackJob(final StickerPackId packId, final byte[] packKey) {
this.packId = packId;
this.packKey = packKey;
}
@Override
public void run(Context context) {
if (context.getStickerPackStore().existsStickerPack(packId)) {
logger.debug("Sticker pack {} already downloaded.", Hex.toStringCondensed(packId.serialize()));
return;
}
logger.debug("Retrieving sticker pack {}.", Hex.toStringCondensed(packId.serialize()));
try {
final var manifest = context.getMessageReceiver().retrieveStickerManifest(packId.serialize(), packKey);
final var stickerIds = new HashSet<Integer>();
if (manifest.getCover().isPresent()) {
stickerIds.add(manifest.getCover().get().getId());
}
for (var sticker : manifest.getStickers()) {
stickerIds.add(sticker.getId());
}
for (var id : stickerIds) {
final var inputStream = context.getMessageReceiver().retrieveSticker(packId.serialize(), packKey, id);
context.getStickerPackStore().storeSticker(packId, id, o -> IOUtils.copyStream(inputStream, o));
}
final var jsonManifest = new JsonStickerPack(manifest.getTitle().orNull(),
manifest.getAuthor().orNull(),
manifest.getCover()
.transform(c -> new JsonStickerPack.JsonSticker(c.getEmoji(),
String.valueOf(c.getId()),
c.getContentType()))
.orNull(),
manifest.getStickers()
.stream()
.map(c -> new JsonStickerPack.JsonSticker(c.getEmoji(),
String.valueOf(c.getId()),
c.getContentType()))
.collect(Collectors.toList()));
context.getStickerPackStore().storeManifest(packId, jsonManifest);
} catch (IOException e) {
logger.warn("Failed to retrieve sticker pack {}: {}",
Hex.toStringCondensed(packId.serialize()),
e.getMessage());
} catch (InvalidMessageException e) {
logger.warn("Failed to retrieve sticker pack {}, invalid pack data: {}",
Hex.toStringCondensed(packId.serialize()),
e.getMessage());
}
}
}