@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
@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;
+ }
}
}
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;
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();
this::resolveSignalServiceAddress);
this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath());
this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath());
+ this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath());
}
public String getUsername() {
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);
}
sticker = new Sticker(stickerPackId, messageSticker.getPackKey());
account.getStickerStore().updateSticker(sticker);
}
+ enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey()));
}
return actions;
}
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()) {
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);
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() {
public File getAvatarsPath() {
return avatarsPath;
}
+
+ public File getStickerPacksPath() {
+ return stickerPacksPath;
+ }
}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+package org.asamk.signal.manager.jobs;
+
+public interface Job {
+
+ void run(Context context);
+}
--- /dev/null
+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());
+ }
+ }
+}