Skip to content

Commit

Permalink
Working on better serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
Auties00 committed Oct 4, 2023
1 parent edb79e3 commit 3e31765
Show file tree
Hide file tree
Showing 51 changed files with 674 additions and 348 deletions.
16 changes: 5 additions & 11 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -193,20 +193,9 @@
<groupId>cc.jilt</groupId>
<artifactId>jilt</artifactId>
<version>${jilt.version}</version>
<scope>provided</scope>
</annotationProcessorPath>
</annotationProcessorPaths>
<fork>true</fork>
<compilerArgs>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>-parameters</arg>
</compilerArgs>
<failOnError>true</failOnError>
Expand All @@ -230,6 +219,11 @@
</build>

<dependencies>
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-server</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>cc.jilt</groupId>
<artifactId>jilt</artifactId>
Expand Down
22 changes: 20 additions & 2 deletions src/main/java/it/auties/whatsapp/api/WebHistoryLength.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package it.auties.whatsapp.api;

import it.auties.whatsapp.util.Spec;
import it.auties.whatsapp.util.Specification;

/**
* The constants of this enumerated type describe the various chat history's codeLength that Whatsapp
* can send on the first login attempt
*/
public record WebHistoryLength(int size) {
private static final WebHistoryLength ZERO = new WebHistoryLength(0);
private static final WebHistoryLength STANDARD = new WebHistoryLength(Spec.Whatsapp.DEFAULT_HISTORY_SIZE);
private static final WebHistoryLength STANDARD = new WebHistoryLength(Specification.Whatsapp.DEFAULT_HISTORY_SIZE);
private static final WebHistoryLength EXTENDED = new WebHistoryLength(Integer.MAX_VALUE);

/**
Expand Down Expand Up @@ -43,4 +43,22 @@ public static WebHistoryLength extended() {
public static WebHistoryLength custom(int size) {
return new WebHistoryLength(size);
}

/**
* Returns whether this history size counts as zero
*
* @return a boolean
*/
public boolean isZero() {
return size == 0;
}

/**
* Returns whether this history size counts as extended
*
* @return a boolean
*/
public boolean isExtended() {
return size > Specification.Whatsapp.DEFAULT_HISTORY_SIZE;
}
}
70 changes: 47 additions & 23 deletions src/main/java/it/auties/whatsapp/api/Whatsapp.java
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,7 @@ private void onPrivacyFeatureChanged(PrivacySettingType type, PrivacySettingValu
* @return the same instance wrapped in a completable future
*/
public CompletableFuture<Whatsapp> changeNewChatsEphemeralTimer(@NonNull ChatEphemeralTimer timer) {
return socketHandler.sendQuery("set", "disappearing_mode", Node.of("disappearing_mode", Map.of("duration", timer.period()
.toSeconds())))
return socketHandler.sendQuery("set", "disappearing_mode", Node.of("disappearing_mode", Map.of("duration", timer.period().toSeconds())))
.thenRunAsync(() -> store().setNewChatsEphemeralTimer(timer))
.thenApply(ignored -> this);
}
Expand All @@ -361,7 +360,7 @@ public CompletableFuture<Whatsapp> createGdprAccountInfo() {
// TODO: Implement ready and error states
public CompletableFuture<GdprAccountReport> getGdprAccountInfoStatus() {
return socketHandler.sendQuery("get", "urn:xmpp:whatsapp:account", Node.of("gdpr", Map.of("gdpr", "status")))
.thenApplyAsync(result -> GdprAccountReport.ofPending(result.attributes().getLong("timestampSeconds")));
.thenApplyAsync(result -> GdprAccountReport.ofPending(result.attributes().getLong("timestamp")));
}
/**
Expand Down Expand Up @@ -513,7 +512,7 @@ private CompletableFuture<Void> attributeMessageMetadata(MessageInfo info) {
fixEphemeralMessage(info);
var content = info.message().content();
return switch (content) {
case LocalMediaMessage<?> mediaMessage -> attributeMediaMessage(mediaMessage);
case LocalMediaMessage<?> mediaMessage -> attributeMediaMessage(info.chatJid(), mediaMessage);
case ButtonMessage buttonMessage -> attributeButtonMessage(info, buttonMessage);
case TextMessage textMessage -> attributeTextMessage(textMessage);
case PollCreationMessage pollCreationMessage -> attributePollCreationMessage(info, pollCreationMessage);
Expand Down Expand Up @@ -594,16 +593,41 @@ private CompletableFuture<Void> attributeTextMessage(TextMessage textMessage) {
return CompletableFuture.completedFuture(null);
}

private CompletableFuture<Void> attributeMediaMessage(LocalMediaMessage<?> mediaMessage) {
return Medias.upload(mediaMessage.decodedMedia().orElseThrow(), mediaMessage.mediaType().toAttachmentType(), store().mediaConnection())
private CompletableFuture<Void> attributeMediaMessage(Jid chatJid, LocalMediaMessage<?> mediaMessage) {
var media = mediaMessage.decodedMedia()
.orElseThrow(() -> new IllegalArgumentException("Missing media to upload"));
var attachmentType = getAttachmentType(chatJid, mediaMessage);
var mediaConnection = store().mediaConnection();
return Medias.upload(media, attachmentType, mediaConnection)
.thenAccept(upload -> attributeMediaMessage(mediaMessage, upload));
}

private AttachmentType getAttachmentType(Jid chatJid, LocalMediaMessage<?> mediaMessage) {
if (!chatJid.hasServer(JidServer.CHANNEL)) {
return mediaMessage.attachmentType();
}

return switch (mediaMessage.mediaType()) {
case IMAGE -> AttachmentType.NEWSLETTER_IMAGE;
case DOCUMENT -> AttachmentType.NEWSLETTER_DOCUMENT;
case AUDIO -> AttachmentType.NEWSLETTER_AUDIO;
case VIDEO -> AttachmentType.NEWSLETTER_VIDEO;
case STICKER -> AttachmentType.NEWSLETTER_STICKER;
case NONE -> throw new IllegalArgumentException("Unexpected empty message");
};
}


private MutableAttachmentProvider<?> attributeMediaMessage(MutableAttachmentProvider<?> mediaMessage, MediaFile upload) {
if(mediaMessage instanceof LocalMediaMessage<?> localMediaMessage) {
localMediaMessage.setHandle(upload.handle());
}

return mediaMessage.setMediaSha256(upload.fileSha256())
.setMediaEncryptedSha256(upload.fileEncSha256())
.setMediaKey(upload.mediaKey())
.setMediaUrl(upload.url())
.setMediaKeyTimestamp(upload.timestamp())
.setMediaDirectPath(upload.directPath())
.setMediaSize(upload.fileLength());
}
Expand Down Expand Up @@ -657,22 +681,22 @@ private CompletableFuture<Void> attributePollUpdateMessage(MessageInfo info, Pol
private CompletableFuture<Void> attributeButtonMessage(MessageInfo info, ButtonMessage buttonMessage) {
return switch (buttonMessage) {
case ButtonsMessage buttonsMessage when buttonsMessage.header().isPresent()
&& buttonsMessage.header().get() instanceof LocalMediaMessage<?> mediaMessage -> attributeMediaMessage(mediaMessage);
&& buttonsMessage.header().get() instanceof LocalMediaMessage<?> mediaMessage -> attributeMediaMessage(info.chatJid(), mediaMessage);
case TemplateMessage templateMessage when templateMessage.format().isPresent() -> {
var templateFormatter = templateMessage.format().get();
yield switch (templateFormatter) {
case HighlyStructuredFourRowTemplate highlyStructuredFourRowTemplate
when highlyStructuredFourRowTemplate.title().isPresent() && highlyStructuredFourRowTemplate.title().get() instanceof LocalMediaMessage<?> fourRowMedia ->
attributeMediaMessage(fourRowMedia);
case HydratedFourRowTemplate hydratedFourRowTemplate when hydratedFourRowTemplate.title().isPresent() && hydratedFourRowTemplate.title().get() instanceof LocalMediaMessage<?> hydreatedFourRowMedia ->
attributeMediaMessage(hydreatedFourRowMedia);
attributeMediaMessage(info.chatJid(), fourRowMedia);
case HydratedFourRowTemplate hydratedFourRowTemplate when hydratedFourRowTemplate.title().isPresent() && hydratedFourRowTemplate.title().get() instanceof LocalMediaMessage<?> hydratedFourRowMedia ->
attributeMediaMessage(info.chatJid(), hydratedFourRowMedia);
case null, default -> CompletableFuture.completedFuture(null);
};
}
case InteractiveMessage interactiveMessage
when interactiveMessage.header().isPresent()
&& interactiveMessage.header().get().attachment().isPresent()
&& interactiveMessage.header().get().attachment().get() instanceof LocalMediaMessage<?> interactiveMedia -> attributeMediaMessage(interactiveMedia);
&& interactiveMessage.header().get().attachment().get() instanceof LocalMediaMessage<?> interactiveMedia -> attributeMediaMessage(info.chatJid(), interactiveMedia);
default -> CompletableFuture.completedFuture(null);
};
}
Expand Down Expand Up @@ -1237,7 +1261,7 @@ public <T extends JidProvider> CompletableFuture<T> changeGroupPicture(@NonNull
* @param contacts at least one contact to add to the group
* @return a CompletableFuture
*/
public CompletableFuture<GroupMetadata> createGroup(@NonNull String subject, @NonNull JidProvider... contacts) {
public CompletableFuture<Optional<GroupMetadata>> createGroup(@NonNull String subject, @NonNull JidProvider... contacts) {
return createGroup(subject, ChatEphemeralTimer.OFF, contacts);
}

Expand All @@ -1249,7 +1273,7 @@ public CompletableFuture<GroupMetadata> createGroup(@NonNull String subject, @No
* @param contacts at least one contact to add to the group
* @return a CompletableFuture
*/
public CompletableFuture<GroupMetadata> createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, @NonNull JidProvider... contacts) {
public CompletableFuture<Optional<GroupMetadata>> createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, @NonNull JidProvider... contacts) {
return createGroup(subject, timer, null, contacts);
}

Expand All @@ -1261,7 +1285,7 @@ public CompletableFuture<GroupMetadata> createGroup(@NonNull String subject, @No
* @param parentGroup the community to whom the new group will be linked
* @return a CompletableFuture
*/
public CompletableFuture<GroupMetadata> createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, JidProvider parentGroup) {
public CompletableFuture<Optional<GroupMetadata>> createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, JidProvider parentGroup) {
return createGroup(subject, timer, parentGroup, new JidProvider[0]);
}

Expand All @@ -1274,7 +1298,7 @@ public CompletableFuture<GroupMetadata> createGroup(@NonNull String subject, @No
* @param contacts at least one contact to add to the group, not enforced if part of a community
* @return a CompletableFuture
*/
public CompletableFuture<GroupMetadata> createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, JidProvider parentCommunity, @NonNull JidProvider... contacts) {
public CompletableFuture<Optional<GroupMetadata>> createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, JidProvider parentCommunity, @NonNull JidProvider... contacts) {
Validate.isTrue(!subject.isBlank(), "The subject of a group cannot be blank");
var minimumMembersCount = parentCommunity == null ? 1 : 0;
Validate.isTrue(contacts.length >= minimumMembersCount, "Expected at least %s members for this group", minimumMembersCount);
Expand All @@ -1294,12 +1318,11 @@ public CompletableFuture<GroupMetadata> createGroup(@NonNull String subject, @No
.thenApplyAsync(this::parseGroupResponse);
}

private GroupMetadata parseGroupResponse(Node response) {
private Optional<GroupMetadata> parseGroupResponse(Node response) {
return Optional.ofNullable(response)
.flatMap(node -> node.findNode("group"))
.map(socketHandler::parseGroupMetadata)
.map(this::addNewGroup)
.orElseThrow(() -> new NoSuchElementException("Missing group response, something went wrong: %s".formatted(findErrorNode(response))));
.map(this::addNewGroup);
}

private GroupMetadata addNewGroup(GroupMetadata result) {
Expand Down Expand Up @@ -2145,7 +2168,7 @@ private CompletableFuture<CompanionLinkResult> linkDevice(byte[] advIdentity, by
.build();
var deviceIdentityBytes = DeviceIdentitySpec.encode(deviceIdentity);
var accountSignatureMessage = BytesHelper.concat(
Spec.Whatsapp.ACCOUNT_SIGNATURE_HEADER,
Specification.Whatsapp.ACCOUNT_SIGNATURE_HEADER,
deviceIdentityBytes,
advIdentity
);
Expand All @@ -2170,7 +2193,7 @@ private CompletableFuture<CompanionLinkResult> linkDevice(byte[] advIdentity, by
.validIndexes(knownDevices)
.build();
var keyIndexListBytes = KeyIndexListSpec.encode(keyIndexList);
var deviceSignatureMessage = BytesHelper.concat(Spec.Whatsapp.DEVICE_MOBILE_SIGNATURE_HEADER, keyIndexListBytes);
var deviceSignatureMessage = BytesHelper.concat(Specification.Whatsapp.DEVICE_MOBILE_SIGNATURE_HEADER, keyIndexListBytes);
var keyAccountSignature = Curve25519.sign(keys().identityKeyPair().privateKey(), deviceSignatureMessage, true);
var signedKeyIndexList = new SignedKeyIndexListBuilder()
.accountSignature(keyAccountSignature)
Expand All @@ -2187,13 +2210,13 @@ private CompletableFuture<CompanionLinkResult> linkDevice(byte[] advIdentity, by
private int getMaxLinkedDevices() {
var maxDevices = socketHandler.store().properties().get("linked_device_max_count");
if(maxDevices == null){
return Spec.Whatsapp.MAX_COMPANIONS;
return Specification.Whatsapp.MAX_COMPANIONS;
}

try {
return Integer.parseInt(maxDevices);
}catch (NumberFormatException exception){
return Spec.Whatsapp.MAX_COMPANIONS;
return Specification.Whatsapp.MAX_COMPANIONS;
}
}

Expand Down Expand Up @@ -2226,7 +2249,7 @@ private CompletableFuture<Void> awaitCompanionRegistration(Jid device) {
}
};
addLinkedDevicesListener(listener);
return future.orTimeout(Spec.Whatsapp.COMPANION_PAIRING_TIMEOUT, TimeUnit.SECONDS)
return future.orTimeout(Specification.Whatsapp.COMPANION_PAIRING_TIMEOUT, TimeUnit.SECONDS)
.exceptionally(ignored -> null)
.thenRun(() -> removeListener(listener));
}
Expand Down Expand Up @@ -2586,6 +2609,7 @@ private Node createCallNode(JidProvider provider) {
Map.of("v", 2, "type", cipheredMessage.type(), "count", 0), cipheredMessage.message());
}


/**
* Rejects an incoming call or stops an active call
* Mobile API only
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,12 @@

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
Expand Down Expand Up @@ -312,7 +309,7 @@ public synchronized CompletableFuture<Void> attributeStore(Store store) {
public void deleteSession(@NonNull Controller<?> controller) {
try {
var folderPath = getSessionDirectory(controller.clientType(), controller.uuid().toString());
Files.deleteIfExists(folderPath);
delete(folderPath);
var phoneNumber = controller.phoneNumber().orElse(null);
if (phoneNumber == null) {
return;
Expand All @@ -324,6 +321,22 @@ public void deleteSession(@NonNull Controller<?> controller) {
}
}

private Path delete(Path path) throws IOException {
return Files.walkFileTree(path, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
}

@Override
public void linkMetadata(@NonNull Controller<?> controller) {
controller.phoneNumber()
Expand All @@ -343,9 +356,9 @@ private void linkToUuid(ClientType type, UUID uuid, String string) {

private void deserializeChat(Store store, Path chatFile) {
try (var input = new GZIPInputStream(Files.newInputStream(chatFile))) {
store.addChat(Smile.readValue(input, Chat.class));
store.addChatDirect(Smile.readValue(input, Chat.class));
} catch (IOException exception) {
store.addChat(rescueChat(chatFile));
store.addChatDirect(rescueChat(chatFile));
}
}

Expand All @@ -361,7 +374,6 @@ private Chat rescueChat(Path entry) {
.replaceAll("~~", ":");
return new ChatBuilder()
.jid(Jid.of(chatName))
.historySyncMessages(new ConcurrentLinkedDeque<>())
.build();
}

Expand Down
Loading

0 comments on commit 3e31765

Please sign in to comment.