From 3e31765118c9c206d3e728b66f838debc356ff2e Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Wed, 4 Oct 2023 23:47:54 +0200 Subject: [PATCH] Working on better serialization --- pom.xml | 16 +- .../auties/whatsapp/api/WebHistoryLength.java | 22 +- .../java/it/auties/whatsapp/api/Whatsapp.java | 70 ++-- .../DefaultControllerSerializer.java | 30 +- .../it/auties/whatsapp/controller/Keys.java | 12 +- .../whatsapp/controller/KeysBuilder.java | 9 - .../whatsapp/controller/StoreBuilder.java | 6 +- .../auties/whatsapp/crypto/GroupCipher.java | 2 +- .../java/it/auties/whatsapp/crypto/Hkdf.java | 2 +- .../whatsapp/crypto/SessionBuilder.java | 2 +- .../auties/whatsapp/crypto/SessionCipher.java | 5 +- .../whatsapp/model/media/AttachmentType.java | 37 +- .../whatsapp/model/media/MediaFile.java | 3 +- .../whatsapp/model/media/MediaKeys.java | 4 +- .../whatsapp/model/media/MediaUpload.java | 2 +- .../media/MutableAttachmentProvider.java | 7 + .../model/message/model/MessageContainer.java | 4 + .../model/message/model/MessageType.java | 6 +- .../model/reserved/LocalMediaMessage.java | 11 + .../payment/PaymentInvoiceMessage.java | 8 +- .../model/message/standard/AudioMessage.java | 9 +- .../message/standard/DocumentMessage.java | 18 +- .../model/message/standard/ImageMessage.java | 9 +- .../message/standard/StickerMessage.java | 10 +- .../message/standard/VideoOrGifMessage.java | 10 +- .../whatsapp/model/request/Request.java | 11 +- .../signal/message/SenderKeyMessage.java | 2 +- .../model/signal/message/SignalMessage.java | 2 +- .../signal/message/SignalProtocolMessage.java | 4 +- .../model/signal/sender/SenderMessageKey.java | 8 +- .../model/sync/ExternalBlobReference.java | 5 + .../model/sync/HistorySyncNotification.java | 5 + .../whatsapp/model/sync/MutationKeys.java | 12 +- .../whatsapp/model/sync/PatchRequest.java | 4 +- .../whatsapp/socket/AppStateHandler.java | 12 +- .../auties/whatsapp/socket/AuthHandler.java | 26 +- .../whatsapp/socket/MessageHandler.java | 67 ++-- .../auties/whatsapp/socket/SocketHandler.java | 18 +- .../whatsapp/socket/SocketHandshake.java | 36 +- .../auties/whatsapp/socket/SocketSession.java | 351 ++++++++++++------ .../auties/whatsapp/socket/StreamHandler.java | 15 +- .../it/auties/whatsapp/util/BytesHelper.java | 13 +- .../util/ConcurrentDoublyLinkedList.java | 10 +- .../java/it/auties/whatsapp/util/Medias.java | 58 ++- .../auties/whatsapp/util/MetadataHelper.java | 2 +- .../whatsapp/util/RegistrationHelper.java | 2 +- .../util/{Spec.java => Specification.java} | 18 +- src/main/java/module-info.java | 1 + src/test/java/it/auties/whatsapp/Test.java | 11 +- .../it/auties/whatsapp/local/WebTest.java | 13 +- .../update/UpdateBinaryTokensTest.java | 2 +- 51 files changed, 674 insertions(+), 348 deletions(-) rename src/main/java/it/auties/whatsapp/util/{Spec.java => Specification.java} (77%) diff --git a/pom.xml b/pom.xml index 493e857e9..a89a8b281 100644 --- a/pom.xml +++ b/pom.xml @@ -193,20 +193,9 @@ cc.jilt jilt ${jilt.version} - provided - true - -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED -parameters true @@ -230,6 +219,11 @@ + + org.glassfish.tyrus + tyrus-server + 2.1.3 + cc.jilt jilt diff --git a/src/main/java/it/auties/whatsapp/api/WebHistoryLength.java b/src/main/java/it/auties/whatsapp/api/WebHistoryLength.java index ffdc8ef08..73d114092 100644 --- a/src/main/java/it/auties/whatsapp/api/WebHistoryLength.java +++ b/src/main/java/it/auties/whatsapp/api/WebHistoryLength.java @@ -1,6 +1,6 @@ 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 @@ -8,7 +8,7 @@ */ 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); /** @@ -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; + } } diff --git a/src/main/java/it/auties/whatsapp/api/Whatsapp.java b/src/main/java/it/auties/whatsapp/api/Whatsapp.java index 6c66199cb..5b2ff8a51 100644 --- a/src/main/java/it/auties/whatsapp/api/Whatsapp.java +++ b/src/main/java/it/auties/whatsapp/api/Whatsapp.java @@ -334,8 +334,7 @@ private void onPrivacyFeatureChanged(PrivacySettingType type, PrivacySettingValu * @return the same instance wrapped in a completable future */ public CompletableFuture 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); } @@ -361,7 +360,7 @@ public CompletableFuture createGdprAccountInfo() { // TODO: Implement ready and error states public CompletableFuture 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"))); } /** @@ -513,7 +512,7 @@ private CompletableFuture 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); @@ -594,16 +593,41 @@ private CompletableFuture attributeTextMessage(TextMessage textMessage) { return CompletableFuture.completedFuture(null); } - private CompletableFuture attributeMediaMessage(LocalMediaMessage mediaMessage) { - return Medias.upload(mediaMessage.decodedMedia().orElseThrow(), mediaMessage.mediaType().toAttachmentType(), store().mediaConnection()) + private CompletableFuture 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()); } @@ -657,22 +681,22 @@ private CompletableFuture attributePollUpdateMessage(MessageInfo info, Pol private CompletableFuture 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); }; } @@ -1237,7 +1261,7 @@ public CompletableFuture changeGroupPicture(@NonNull * @param contacts at least one contact to add to the group * @return a CompletableFuture */ - public CompletableFuture createGroup(@NonNull String subject, @NonNull JidProvider... contacts) { + public CompletableFuture> createGroup(@NonNull String subject, @NonNull JidProvider... contacts) { return createGroup(subject, ChatEphemeralTimer.OFF, contacts); } @@ -1249,7 +1273,7 @@ public CompletableFuture createGroup(@NonNull String subject, @No * @param contacts at least one contact to add to the group * @return a CompletableFuture */ - public CompletableFuture createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, @NonNull JidProvider... contacts) { + public CompletableFuture> createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, @NonNull JidProvider... contacts) { return createGroup(subject, timer, null, contacts); } @@ -1261,7 +1285,7 @@ public CompletableFuture createGroup(@NonNull String subject, @No * @param parentGroup the community to whom the new group will be linked * @return a CompletableFuture */ - public CompletableFuture createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, JidProvider parentGroup) { + public CompletableFuture> createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, JidProvider parentGroup) { return createGroup(subject, timer, parentGroup, new JidProvider[0]); } @@ -1274,7 +1298,7 @@ public CompletableFuture 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 createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, JidProvider parentCommunity, @NonNull JidProvider... contacts) { + public CompletableFuture> 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); @@ -1294,12 +1318,11 @@ public CompletableFuture createGroup(@NonNull String subject, @No .thenApplyAsync(this::parseGroupResponse); } - private GroupMetadata parseGroupResponse(Node response) { + private Optional 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) { @@ -2145,7 +2168,7 @@ private CompletableFuture 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 ); @@ -2170,7 +2193,7 @@ private CompletableFuture 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) @@ -2187,13 +2210,13 @@ private CompletableFuture 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; } } @@ -2226,7 +2249,7 @@ private CompletableFuture 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)); } @@ -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 diff --git a/src/main/java/it/auties/whatsapp/controller/DefaultControllerSerializer.java b/src/main/java/it/auties/whatsapp/controller/DefaultControllerSerializer.java index ff0a52e78..a9a511242 100644 --- a/src/main/java/it/auties/whatsapp/controller/DefaultControllerSerializer.java +++ b/src/main/java/it/auties/whatsapp/controller/DefaultControllerSerializer.java @@ -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; @@ -312,7 +309,7 @@ public synchronized CompletableFuture 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; @@ -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() @@ -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)); } } @@ -361,7 +374,6 @@ private Chat rescueChat(Path entry) { .replaceAll("~~", ":"); return new ChatBuilder() .jid(Jid.of(chatName)) - .historySyncMessages(new ConcurrentLinkedDeque<>()) .build(); } diff --git a/src/main/java/it/auties/whatsapp/controller/Keys.java b/src/main/java/it/auties/whatsapp/controller/Keys.java index 804815666..d8185cc43 100644 --- a/src/main/java/it/auties/whatsapp/controller/Keys.java +++ b/src/main/java/it/auties/whatsapp/controller/Keys.java @@ -84,11 +84,6 @@ public final class Keys extends Controller { @NonNull private final List preKeys; - /** - * The prologue to send in a message - */ - private final byte @NonNull [] prologue; - /** * The phone id for the mobile api */ @@ -176,7 +171,7 @@ public final class Keys extends Controller { private byte[] writeKey, readKey; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - Keys(@NonNull UUID uuid, PhoneNumber phoneNumber, @NonNull ControllerSerializer serializer, @NonNull ClientType clientType, @Nullable List alias, int registrationId, @NonNull SignalKeyPair noiseKeyPair, @NonNull SignalKeyPair ephemeralKeyPair, @NonNull SignalKeyPair identityKeyPair, @NonNull SignalKeyPair companionKeyPair, SignalSignedKeyPair signedKeyPair, byte @Nullable [] signedKeyIndex, @Nullable Long signedKeyIndexTimestamp, @NonNull List preKeys, byte @NonNull [] prologue, @NonNull String phoneId, @NonNull String deviceId, @NonNull String recoveryToken, @Nullable SignedDeviceIdentity companionIdentity, @NonNull Map senderKeys, @NonNull Map> appStateKeys, @NonNull Map sessions, @NonNull Map> hashStates, @NonNull Map> groupsPreKeys, boolean registered, boolean businessCertificate, boolean initialAppSync) { + Keys(@NonNull UUID uuid, PhoneNumber phoneNumber, @NonNull ControllerSerializer serializer, @NonNull ClientType clientType, @Nullable List alias, int registrationId, @NonNull SignalKeyPair noiseKeyPair, @NonNull SignalKeyPair ephemeralKeyPair, @NonNull SignalKeyPair identityKeyPair, @NonNull SignalKeyPair companionKeyPair, SignalSignedKeyPair signedKeyPair, byte @Nullable [] signedKeyIndex, @Nullable Long signedKeyIndexTimestamp, @NonNull List preKeys, @NonNull String phoneId, @NonNull String deviceId, @NonNull String recoveryToken, @Nullable SignedDeviceIdentity companionIdentity, @NonNull Map senderKeys, @NonNull Map> appStateKeys, @NonNull Map sessions, @NonNull Map> hashStates, @NonNull Map> groupsPreKeys, boolean registered, boolean businessCertificate, boolean initialAppSync) { super(uuid, phoneNumber, serializer, clientType, alias); this.registrationId = registrationId; this.noiseKeyPair = noiseKeyPair; @@ -187,7 +182,6 @@ public final class Keys extends Controller { this.signedKeyIndex = signedKeyIndex; this.signedKeyIndexTimestamp = signedKeyIndexTimestamp; this.preKeys = preKeys; - this.prologue = prologue; this.phoneId = phoneId; this.deviceId = deviceId; this.recoveryToken = recoveryToken; @@ -530,10 +524,6 @@ public SignalKeyPair companionKeyPair() { return this.companionKeyPair; } - public byte [] prologue() { - return this.prologue; - } - public String phoneId() { return this.phoneId; } diff --git a/src/main/java/it/auties/whatsapp/controller/KeysBuilder.java b/src/main/java/it/auties/whatsapp/controller/KeysBuilder.java index ab3f4f6c5..2b79e5128 100644 --- a/src/main/java/it/auties/whatsapp/controller/KeysBuilder.java +++ b/src/main/java/it/auties/whatsapp/controller/KeysBuilder.java @@ -5,7 +5,6 @@ import it.auties.whatsapp.model.signal.keypair.SignalKeyPair; import it.auties.whatsapp.model.signal.keypair.SignalSignedKeyPair; import it.auties.whatsapp.util.KeyHelper; -import it.auties.whatsapp.util.Spec; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -96,7 +95,6 @@ public Keys build() { null, null, new ArrayList<>(), - getDefaultPrologue(clientType), KeyHelper.phoneId(), KeyHelper.deviceId(), KeyHelper.identityId(), @@ -114,11 +112,4 @@ public Keys build() { return result; }); } - - private static byte[] getDefaultPrologue(ClientType clientType) { - return switch (clientType) { - case WEB -> Spec.Whatsapp.WEB_PROLOGUE; - case MOBILE -> Spec.Whatsapp.APP_PROLOGUE; - }; - } } \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/controller/StoreBuilder.java b/src/main/java/it/auties/whatsapp/controller/StoreBuilder.java index 971d8820c..bcc45276b 100644 --- a/src/main/java/it/auties/whatsapp/controller/StoreBuilder.java +++ b/src/main/java/it/auties/whatsapp/controller/StoreBuilder.java @@ -14,7 +14,7 @@ import it.auties.whatsapp.util.Clock; import it.auties.whatsapp.util.FutureReference; import it.auties.whatsapp.util.MetadataHelper; -import it.auties.whatsapp.util.Spec; +import it.auties.whatsapp.util.Specification; import java.net.URI; import java.util.*; @@ -201,7 +201,7 @@ public Optional deserialize() { public Store build() { return deserialize().orElseGet(() -> { if(device == null) { - device = Spec.Whatsapp.DEFAULT_MOBILE_DEVICE; + device = Specification.Whatsapp.DEFAULT_MOBILE_DEVICE; } var serializer = Objects.requireNonNullElseGet(this.serializer, DefaultControllerSerializer::instance); @@ -215,7 +215,7 @@ public Store build() { new FutureReference<>(version, () -> MetadataHelper.getVersion(getPlatform(clientType))), false, null, - Objects.requireNonNullElse(name, Spec.Whatsapp.DEFAULT_NAME), + Objects.requireNonNullElse(name, Specification.Whatsapp.DEFAULT_NAME), business, businessAddress, businessLongitude, diff --git a/src/main/java/it/auties/whatsapp/crypto/GroupCipher.java b/src/main/java/it/auties/whatsapp/crypto/GroupCipher.java index 3ba97f888..1812c8c95 100644 --- a/src/main/java/it/auties/whatsapp/crypto/GroupCipher.java +++ b/src/main/java/it/auties/whatsapp/crypto/GroupCipher.java @@ -5,7 +5,7 @@ import it.auties.whatsapp.model.signal.sender.SenderKeyName; import it.auties.whatsapp.model.signal.sender.SenderKeyState; import it.auties.whatsapp.model.signal.sender.SenderMessageKey; -import it.auties.whatsapp.util.Spec.Signal; +import it.auties.whatsapp.util.Specification.Signal; import org.checkerframework.checker.nullness.qual.NonNull; import java.util.NoSuchElementException; diff --git a/src/main/java/it/auties/whatsapp/crypto/Hkdf.java b/src/main/java/it/auties/whatsapp/crypto/Hkdf.java index 7e20a0fc3..654ca6548 100644 --- a/src/main/java/it/auties/whatsapp/crypto/Hkdf.java +++ b/src/main/java/it/auties/whatsapp/crypto/Hkdf.java @@ -9,7 +9,7 @@ import java.security.GeneralSecurityException; import java.util.Arrays; -import static it.auties.whatsapp.util.Spec.Signal.KEY_LENGTH; +import static it.auties.whatsapp.util.Specification.Signal.KEY_LENGTH; public final class Hkdf { private static final int ITERATION_START_OFFSET = 1; // v3 diff --git a/src/main/java/it/auties/whatsapp/crypto/SessionBuilder.java b/src/main/java/it/auties/whatsapp/crypto/SessionBuilder.java index dc21e07be..2ddba1320 100644 --- a/src/main/java/it/auties/whatsapp/crypto/SessionBuilder.java +++ b/src/main/java/it/auties/whatsapp/crypto/SessionBuilder.java @@ -8,7 +8,7 @@ import it.auties.whatsapp.model.signal.session.*; import it.auties.whatsapp.util.BytesHelper; import it.auties.whatsapp.util.KeyHelper; -import it.auties.whatsapp.util.Spec.Signal; +import it.auties.whatsapp.util.Specification.Signal; import it.auties.whatsapp.util.Validate; import org.checkerframework.checker.nullness.qual.NonNull; diff --git a/src/main/java/it/auties/whatsapp/crypto/SessionCipher.java b/src/main/java/it/auties/whatsapp/crypto/SessionCipher.java index 30a4fdd7c..10c2e258e 100644 --- a/src/main/java/it/auties/whatsapp/crypto/SessionCipher.java +++ b/src/main/java/it/auties/whatsapp/crypto/SessionCipher.java @@ -11,9 +11,8 @@ import it.auties.whatsapp.model.signal.session.SessionChain; import it.auties.whatsapp.model.signal.session.SessionState; import it.auties.whatsapp.util.BytesHelper; -import it.auties.whatsapp.util.Json; import it.auties.whatsapp.util.KeyHelper; -import it.auties.whatsapp.util.Spec.Signal; +import it.auties.whatsapp.util.Specification.Signal; import it.auties.whatsapp.util.Validate; import org.checkerframework.checker.nullness.qual.NonNull; @@ -24,7 +23,7 @@ import java.util.function.Supplier; import static it.auties.curve25519.Curve25519.sharedKey; -import static it.auties.whatsapp.util.Spec.Signal.*; +import static it.auties.whatsapp.util.Specification.Signal.*; public record SessionCipher(@NonNull SessionAddress address, @NonNull Keys keys) { public CipheredMessageResult encrypt(byte[] data) { diff --git a/src/main/java/it/auties/whatsapp/model/media/AttachmentType.java b/src/main/java/it/auties/whatsapp/model/media/AttachmentType.java index 5a1511cc9..2db5dd6ef 100644 --- a/src/main/java/it/auties/whatsapp/model/media/AttachmentType.java +++ b/src/main/java/it/auties/whatsapp/model/media/AttachmentType.java @@ -1,20 +1,35 @@ package it.auties.whatsapp.model.media; +import java.util.Optional; + /** * The constants of this enumerated type describe the various types of attachments supported by Whatsapp */ public enum AttachmentType { - NONE("", "", false), - IMAGE("mms/image", "WhatsApp Image Keys", false), + NONE(null, null, false), AUDIO("mms/audio", "WhatsApp Audio Keys", false), - VIDEO("mms/video", "WhatsApp Video Keys", false), DOCUMENT("mms/document", "WhatsApp Document Keys", false), - HISTORY_SYNC("mms/md-msg-hist", "WhatsApp History Keys", true), + GIF("mms/gif", "WhatsApp Video Keys", false), + IMAGE("mms/image", "WhatsApp Image Keys", false), + PROFILE_PICTURE("pps/photo", null, false), + PRODUCT("mms/image", "WhatsApp Image Keys", false), + VOICE("mms/ptt", "WhatsApp Audio Keys", false), + STICKER("mms/sticker", "WhatsApp Image Keys", false), THUMBNAIL_DOCUMENT("mms/thumbnail-document", "WhatsApp Document Thumbnail Keys", false), - THUMBNAIL_IMAGE("mms/thumbnail-image", "WhatsApp Image Thumbnail Keys", false), THUMBNAIL_LINK("mms/thumbnail-link", "WhatsApp Link Thumbnail Keys", false), - THUMBNAIL_VIDEO("mms/thumbnail-video", "WhatsApp Video Thumbnail Keys", false), - APP_STATE("mms/md-app-state", "WhatsApp App State Keys", false); + VIDEO("mms/video", "WhatsApp Video Keys", false), + APP_STATE("mms/md-app-state", "WhatsApp App State Keys", true), + HISTORY_SYNC(null, "WhatsApp History Keys", true), + PRODUCT_CATALOG_IMAGE("product/image", null, false), + BUSINESS_COVER_PHOTO("pps/biz-cover-photo", null, false), + NEWSLETTER_AUDIO("newsletter/newsletter-audio", null, false), + NEWSLETTER_IMAGE("newsletter/newsletter-image", null, false), + NEWSLETTER_DOCUMENT("newsletter/newsletter-document", null, false), + NEWSLETTER_GIF("newsletter/newsletter-gif", null, false), + NEWSLETTER_VOICE("newsletter/newsletter-ptt", null, false), + NEWSLETTER_STICKER("newsletter/newsletter-sticker", null, false), + NEWSLETTER_THUMBNAIL_LINK("newsletter/newsletter-thumbnail-link", null, false), + NEWSLETTER_VIDEO("newsletter/newsletter-video", null, false); private final String path; private final String keyName; @@ -26,12 +41,12 @@ public enum AttachmentType { this.inflatable = inflatable; } - public String path() { - return this.path; + public Optional path() { + return Optional.ofNullable(path); } - public String keyName() { - return this.keyName; + public Optional keyName() { + return Optional.ofNullable(keyName); } public boolean inflatable() { diff --git a/src/main/java/it/auties/whatsapp/model/media/MediaFile.java b/src/main/java/it/auties/whatsapp/model/media/MediaFile.java index d0b32218e..5c3e3a053 100644 --- a/src/main/java/it/auties/whatsapp/model/media/MediaFile.java +++ b/src/main/java/it/auties/whatsapp/model/media/MediaFile.java @@ -1,5 +1,6 @@ package it.auties.whatsapp.model.media; -public record MediaFile(byte[] fileSha256, byte[] fileEncSha256, byte[] mediaKey, long fileLength, String directPath, String url) { +public record MediaFile(byte[] encryptedFile, byte[] fileSha256, byte[] fileEncSha256, byte[] mediaKey, long fileLength, + String directPath, String url, String handle, Long timestamp) { } diff --git a/src/main/java/it/auties/whatsapp/model/media/MediaKeys.java b/src/main/java/it/auties/whatsapp/model/media/MediaKeys.java index 591a57020..68b8b307c 100644 --- a/src/main/java/it/auties/whatsapp/model/media/MediaKeys.java +++ b/src/main/java/it/auties/whatsapp/model/media/MediaKeys.java @@ -7,8 +7,8 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; -import static it.auties.whatsapp.util.Spec.Signal.IV_LENGTH; -import static it.auties.whatsapp.util.Spec.Signal.KEY_LENGTH; +import static it.auties.whatsapp.util.Specification.Signal.IV_LENGTH; +import static it.auties.whatsapp.util.Specification.Signal.KEY_LENGTH; public record MediaKeys(byte[] mediaKey, byte[] iv, byte[] cipherKey, byte[] macKey, byte[] ref) { private static final int EXPANDED_SIZE = 112; diff --git a/src/main/java/it/auties/whatsapp/model/media/MediaUpload.java b/src/main/java/it/auties/whatsapp/model/media/MediaUpload.java index e5df3f26c..06c298e12 100644 --- a/src/main/java/it/auties/whatsapp/model/media/MediaUpload.java +++ b/src/main/java/it/auties/whatsapp/model/media/MediaUpload.java @@ -2,6 +2,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; -public record MediaUpload(@JsonProperty("direct_path") String directPath, @JsonProperty("url") String url) { +public record MediaUpload(@JsonProperty("direct_path") String directPath, @JsonProperty("url") String url, @JsonProperty("handle") String handle) { } diff --git a/src/main/java/it/auties/whatsapp/model/media/MutableAttachmentProvider.java b/src/main/java/it/auties/whatsapp/model/media/MutableAttachmentProvider.java index 1938fd66b..c123b1903 100644 --- a/src/main/java/it/auties/whatsapp/model/media/MutableAttachmentProvider.java +++ b/src/main/java/it/auties/whatsapp/model/media/MutableAttachmentProvider.java @@ -55,6 +55,13 @@ public sealed interface MutableAttachmentProvider> implements MediaMessage permits AudioMessage, DocumentMessage, ImageMessage, StickerMessage, VideoOrGifMessage { private byte @Nullable [] decodedMedia; + private String handle; + + public Optional handle() { + return Optional.ofNullable(handle); + } public Optional decodedMedia() { return Optional.ofNullable(decodedMedia); @@ -18,4 +23,10 @@ public T setDecodedMedia(byte @Nullable [] decodedMedia) { this.decodedMedia = decodedMedia; return (T) this; } + + @SuppressWarnings("unchecked") + public T setHandle(String handle) { + this.handle = handle; + return (T) this; + } } diff --git a/src/main/java/it/auties/whatsapp/model/message/payment/PaymentInvoiceMessage.java b/src/main/java/it/auties/whatsapp/model/message/payment/PaymentInvoiceMessage.java index 9f76df9ef..29e37d440 100644 --- a/src/main/java/it/auties/whatsapp/model/message/payment/PaymentInvoiceMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/payment/PaymentInvoiceMessage.java @@ -41,7 +41,7 @@ public final class PaymentInvoiceMessage implements PaymentMessage, MediaMessage @ProtobufProperty(index = 6, type = ProtobufType.UINT64) @Nullable - private final Long mediaKeyTimestampSeconds; + private Long mediaKeyTimestampSeconds; @ProtobufProperty(index = 7, type = ProtobufType.BYTES) private byte @Nullable [] mediaSha256; @@ -106,6 +106,12 @@ public PaymentInvoiceMessage setMediaKey(byte[] bytes) { return this; } + @Override + public PaymentInvoiceMessage setMediaKeyTimestamp(Long timestamp) { + this.mediaKeyTimestampSeconds = timestamp; + return this; + } + @Override public OptionalLong mediaKeyTimestampSeconds() { return Clock.parseTimestamp(mediaKeyTimestampSeconds); diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/AudioMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/AudioMessage.java index 96280eab0..2ad79815d 100644 --- a/src/main/java/it/auties/whatsapp/model/message/standard/AudioMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/standard/AudioMessage.java @@ -56,7 +56,7 @@ public final class AudioMessage extends LocalMediaMessage implemen @ProtobufProperty(index = 10, type = ProtobufType.INT64) @Nullable - private final Long mediaKeyTimestampSeconds; + private Long mediaKeyTimestampSeconds; @ProtobufProperty(index = 17, type = ProtobufType.OBJECT) @Nullable @@ -93,7 +93,6 @@ public AudioMessage(@Nullable String mediaUrl, @Nullable String mimetype, byte @ @ProtobufBuilder(className = "AudioMessageSimpleBuilder") static AudioMessage customBuilder(byte[] media, ContextInfo contextInfo, String mimeType, boolean voiceMessage) { return new AudioMessageBuilder() - .mediaKeyTimestampSeconds(Clock.nowSeconds()) .contextInfo(Objects.requireNonNullElseGet(contextInfo, ContextInfo::empty)) .duration(Medias.getDuration(media)) .mimetype(getMimeType(media, mimeType)) @@ -166,6 +165,12 @@ public AudioMessage setMediaKey(byte[] bytes) { return this; } + @Override + public AudioMessage setMediaKeyTimestamp(Long timestamp) { + this.mediaKeyTimestampSeconds = timestamp; + return this; + } + @Override public Optional mediaSha256() { return Optional.ofNullable(mediaSha256); diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/DocumentMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/DocumentMessage.java index 40ed5157e..af65c4411 100644 --- a/src/main/java/it/auties/whatsapp/model/message/standard/DocumentMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/standard/DocumentMessage.java @@ -15,7 +15,7 @@ import it.auties.whatsapp.model.message.model.reserved.LocalMediaMessage; import it.auties.whatsapp.util.Clock; import it.auties.whatsapp.util.Medias; -import it.auties.whatsapp.util.Spec; +import it.auties.whatsapp.util.Specification; import it.auties.whatsapp.util.Validate; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -72,7 +72,7 @@ public final class DocumentMessage extends LocalMediaMessage @ProtobufProperty(index = 11, type = ProtobufType.UINT64) @Nullable - private final Long mediaKeyTimestampSeconds; + private Long mediaKeyTimestampSeconds; @ProtobufProperty(index = 16, type = ProtobufType.BYTES) private final byte @Nullable [] thumbnail; @@ -136,16 +136,16 @@ static DocumentMessage customBuilder(byte[] media, @NonNull String fileName, Str Validate.isTrue(extensionIndex != -1 && extensionIndex + 1 < fileName.length(), "Expected fileName to be formatted as name.extension"); var extension = fileName.substring(extensionIndex + 1); return new DocumentMessageBuilder() - .mediaKeyTimestampSeconds(Clock.nowSeconds()) .mimetype(getMimeType(media, fileName, mimeType)) .fileName(fileName) .pageCount(pageCount > 0 ? pageCount : Medias.getPagesCount(media, extension).orElse(1)) .title(title) .thumbnail(thumbnail != null ? null : Medias.getThumbnail(media, extension).orElse(null)) - .thumbnailWidth(Spec.Whatsapp.THUMBNAIL_WIDTH) - .thumbnailHeight(Spec.Whatsapp.THUMBNAIL_HEIGHT) + .thumbnailWidth(Specification.Whatsapp.THUMBNAIL_WIDTH) + .thumbnailHeight(Specification.Whatsapp.THUMBNAIL_HEIGHT) .contextInfo(Objects.requireNonNullElseGet(contextInfo, ContextInfo::empty)) - .build(); + .build() + .setDecodedMedia(media); } private static String getMimeType(byte[] media, @NonNull String fileName, String mimeType) { @@ -208,6 +208,12 @@ public DocumentMessage setMediaKey(byte[] bytes) { return this; } + @Override + public DocumentMessage setMediaKeyTimestamp(Long timestamp) { + this.mediaKeyTimestampSeconds = timestamp; + return this; + } + @Override public Optional mediaSha256() { return Optional.ofNullable(mediaSha256); diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/ImageMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/ImageMessage.java index 848638cf3..9835183a7 100644 --- a/src/main/java/it/auties/whatsapp/model/message/standard/ImageMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/standard/ImageMessage.java @@ -77,7 +77,7 @@ public final class ImageMessage extends LocalMediaMessage @ProtobufProperty(index = 12, type = ProtobufType.UINT64) @Nullable - private final Long mediaKeyTimestampSeconds; + private Long mediaKeyTimestampSeconds; @ProtobufProperty(index = 16, type = ProtobufType.BYTES) private final byte @Nullable [] thumbnail; @@ -173,7 +173,6 @@ public ImageMessage(@Nullable String mediaUrl, @Nullable String mimetype, @Nulla static ImageMessage simpleBuilder(byte @Nullable [] media, String mimeType, String caption, byte @Nullable [] thumbnail, ContextInfo contextInfo) { var dimensions = Medias.getDimensions(media, false); return new ImageMessageBuilder() - .mediaKeyTimestampSeconds(Clock.nowSeconds()) .mimetype(requireNonNullElse(mimeType, IMAGE.defaultMimeType())) .caption(caption) .width(dimensions.width()) @@ -218,6 +217,12 @@ public ImageMessage setMediaKey(byte[] bytes) { return this; } + @Override + public ImageMessage setMediaKeyTimestamp(Long timestamp) { + this.mediaKeyTimestampSeconds = timestamp; + return this; + } + @Override public Optional mediaSha256() { return Optional.ofNullable(mediaSha256); diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/StickerMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/StickerMessage.java index dfc32727f..e513efcf1 100644 --- a/src/main/java/it/auties/whatsapp/model/message/standard/StickerMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/standard/StickerMessage.java @@ -63,7 +63,7 @@ public final class StickerMessage extends LocalMediaMessage impl @ProtobufProperty(index = 10, type = ProtobufType.UINT64) @Nullable - private final Long mediaKeyTimestampSeconds; + private Long mediaKeyTimestampSeconds; @ProtobufProperty(index = 11, type = ProtobufType.UINT32) @Nullable @@ -112,7 +112,7 @@ public StickerMessage(@Nullable String mediaUrl, byte @Nullable [] mediaSha256, @ProtobufBuilder(className = "SimpleStickerMessageBuilder") static StickerMessage simpleBuilder(byte @Nullable [] media, String mimeType, byte @Nullable [] thumbnail, boolean animated, ContextInfo contextInfo) { return new StickerMessageBuilder() - .mediaKeyTimestampSeconds(Clock.nowSeconds()) + .mimetype(requireNonNullElse(mimeType, STICKER.defaultMimeType())) .thumbnail(thumbnail != null ? thumbnail : Medias.getThumbnail(media, PNG).orElse(null)) .animated(animated) @@ -170,6 +170,12 @@ public StickerMessage setMediaKey(byte[] bytes) { return this; } + @Override + public StickerMessage setMediaKeyTimestamp(Long timestamp) { + this.mediaKeyTimestampSeconds = timestamp; + return this; + } + @Override public Optional mediaSha256() { return Optional.ofNullable(mediaSha256); diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/VideoOrGifMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/VideoOrGifMessage.java index 7589fe04d..88220ab68 100644 --- a/src/main/java/it/auties/whatsapp/model/message/standard/VideoOrGifMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/standard/VideoOrGifMessage.java @@ -73,7 +73,7 @@ public final class VideoOrGifMessage extends LocalMediaMessage mediaSha256() { return Optional.ofNullable(mediaSha256); diff --git a/src/main/java/it/auties/whatsapp/model/request/Request.java b/src/main/java/it/auties/whatsapp/model/request/Request.java index 20911f12e..f0c2853d8 100644 --- a/src/main/java/it/auties/whatsapp/model/request/Request.java +++ b/src/main/java/it/auties/whatsapp/model/request/Request.java @@ -8,6 +8,7 @@ import it.auties.whatsapp.socket.SocketSession; import it.auties.whatsapp.util.BytesHelper; import it.auties.whatsapp.util.Exceptions; +import it.auties.whatsapp.util.Specification; import org.checkerframework.checker.nullness.qual.NonNull; import java.io.IOException; @@ -93,7 +94,7 @@ public CompletableFuture sendWithPrologue(@NonNull SocketSession session, public CompletableFuture send(@NonNull SocketSession session, @NonNull Keys keys, @NonNull Store store, boolean prologue, boolean response) { var ciphered = encryptMessage(keys); var buffer = BytesHelper.newBuffer(); - buffer.writeBytes(prologue ? keys.prologue() : new byte[0]); + buffer.writeBytes(prologue ? getPrologueData(store) : new byte[0]); buffer.writeInt(ciphered.length >> 16); buffer.writeShort(65535 & ciphered.length); buffer.writeBytes(ciphered); @@ -103,6 +104,14 @@ public CompletableFuture send(@NonNull SocketSession session, @NonNull Key return future; } + private byte[] getPrologueData(@NonNull Store store) { + return switch (store.clientType()) { + case WEB -> Specification.Whatsapp.WEB_PROLOGUE; + case MOBILE -> Specification.Whatsapp.MOBILE_PROLOGUE; + }; + } + + private byte[] encryptMessage(Keys keys) { var encodedBody = body(); var body = getBody(encodedBody); diff --git a/src/main/java/it/auties/whatsapp/model/signal/message/SenderKeyMessage.java b/src/main/java/it/auties/whatsapp/model/signal/message/SenderKeyMessage.java index d1e8b3c73..9b25cf6da 100644 --- a/src/main/java/it/auties/whatsapp/model/signal/message/SenderKeyMessage.java +++ b/src/main/java/it/auties/whatsapp/model/signal/message/SenderKeyMessage.java @@ -10,7 +10,7 @@ import java.util.Arrays; -import static it.auties.whatsapp.util.Spec.Signal.SIGNATURE_LENGTH; +import static it.auties.whatsapp.util.Specification.Signal.SIGNATURE_LENGTH; @ProtobufMessageName("SenderKeyMessage") public final class SenderKeyMessage extends SignalProtocolMessage { diff --git a/src/main/java/it/auties/whatsapp/model/signal/message/SignalMessage.java b/src/main/java/it/auties/whatsapp/model/signal/message/SignalMessage.java index d003543d3..f0bb9b163 100644 --- a/src/main/java/it/auties/whatsapp/model/signal/message/SignalMessage.java +++ b/src/main/java/it/auties/whatsapp/model/signal/message/SignalMessage.java @@ -11,7 +11,7 @@ import java.util.Arrays; import java.util.Objects; -import static it.auties.whatsapp.util.Spec.Signal.MAC_LENGTH; +import static it.auties.whatsapp.util.Specification.Signal.MAC_LENGTH; @ProtobufMessageName("SignalMessage") public final class SignalMessage extends SignalProtocolMessage { diff --git a/src/main/java/it/auties/whatsapp/model/signal/message/SignalProtocolMessage.java b/src/main/java/it/auties/whatsapp/model/signal/message/SignalProtocolMessage.java index cba6406b0..d6b8ad7d5 100644 --- a/src/main/java/it/auties/whatsapp/model/signal/message/SignalProtocolMessage.java +++ b/src/main/java/it/auties/whatsapp/model/signal/message/SignalProtocolMessage.java @@ -2,14 +2,14 @@ import it.auties.protobuf.model.ProtobufMessage; import it.auties.whatsapp.util.BytesHelper; -import it.auties.whatsapp.util.Spec; +import it.auties.whatsapp.util.Specification; public abstract sealed class SignalProtocolMessage> implements ProtobufMessage permits SenderKeyMessage, SignalDistributionMessage, SignalMessage, SignalPreKeyMessage { private int version; protected byte[] serialized; public SignalProtocolMessage() { - this.version = Spec.Signal.CURRENT_VERSION; + this.version = Specification.Signal.CURRENT_VERSION; } public SignalProtocolMessage(int version, byte[] serialized) { diff --git a/src/main/java/it/auties/whatsapp/model/signal/sender/SenderMessageKey.java b/src/main/java/it/auties/whatsapp/model/signal/sender/SenderMessageKey.java index b076862a6..dcb23c55b 100644 --- a/src/main/java/it/auties/whatsapp/model/signal/sender/SenderMessageKey.java +++ b/src/main/java/it/auties/whatsapp/model/signal/sender/SenderMessageKey.java @@ -2,7 +2,7 @@ import it.auties.whatsapp.crypto.Hkdf; import it.auties.whatsapp.util.BytesHelper; -import it.auties.whatsapp.util.Spec; +import it.auties.whatsapp.util.Specification; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -14,14 +14,14 @@ public SenderMessageKey(int iteration, byte[] seed) { private static byte[] createIv(byte[] seed) { var derivative = getDerivedSeed(seed); - return Arrays.copyOf(derivative[0], Spec.Signal.IV_LENGTH); + return Arrays.copyOf(derivative[0], Specification.Signal.IV_LENGTH); } private static byte[] createCipherKey(byte[] seed) { var derivative = getDerivedSeed(seed); - var data = Arrays.copyOfRange(derivative[0], Spec.Signal.IV_LENGTH, derivative[0].length); + var data = Arrays.copyOfRange(derivative[0], Specification.Signal.IV_LENGTH, derivative[0].length); var concat = BytesHelper.concat(data, derivative[1]); - return Arrays.copyOf(concat, Spec.Signal.KEY_LENGTH); + return Arrays.copyOf(concat, Specification.Signal.KEY_LENGTH); } private static byte[][] getDerivedSeed(byte[] seed) { diff --git a/src/main/java/it/auties/whatsapp/model/sync/ExternalBlobReference.java b/src/main/java/it/auties/whatsapp/model/sync/ExternalBlobReference.java index adc27b990..e53b3bc63 100644 --- a/src/main/java/it/auties/whatsapp/model/sync/ExternalBlobReference.java +++ b/src/main/java/it/auties/whatsapp/model/sync/ExternalBlobReference.java @@ -73,6 +73,11 @@ public ExternalBlobReference setMediaKey(byte[] bytes) { return this; } + @Override + public ExternalBlobReference setMediaKeyTimestamp(Long timestamp) { + return this; + } + @Override public Optional mediaSha256() { return Optional.ofNullable(mediaSha256); diff --git a/src/main/java/it/auties/whatsapp/model/sync/HistorySyncNotification.java b/src/main/java/it/auties/whatsapp/model/sync/HistorySyncNotification.java index d7ff84bd8..6ff4b4e17 100644 --- a/src/main/java/it/auties/whatsapp/model/sync/HistorySyncNotification.java +++ b/src/main/java/it/auties/whatsapp/model/sync/HistorySyncNotification.java @@ -88,6 +88,11 @@ public HistorySyncNotification setMediaKey(byte[] bytes) { return this; } + @Override + public HistorySyncNotification setMediaKeyTimestamp(Long timestamp) { + return this; + } + @Override public Optional mediaSha256() { return Optional.ofNullable(mediaSha256); diff --git a/src/main/java/it/auties/whatsapp/model/sync/MutationKeys.java b/src/main/java/it/auties/whatsapp/model/sync/MutationKeys.java index c8387cd11..021041497 100644 --- a/src/main/java/it/auties/whatsapp/model/sync/MutationKeys.java +++ b/src/main/java/it/auties/whatsapp/model/sync/MutationKeys.java @@ -1,7 +1,7 @@ package it.auties.whatsapp.model.sync; import it.auties.whatsapp.crypto.Hkdf; -import it.auties.whatsapp.util.Spec; +import it.auties.whatsapp.util.Specification; import org.checkerframework.checker.nullness.qual.NonNull; import java.nio.charset.StandardCharsets; @@ -13,11 +13,11 @@ public record MutationKeys(byte[] indexKey, byte[] encKey, byte[] macKey, byte[] public static MutationKeys of(byte @NonNull [] key) { var expanded = Hkdf.extractAndExpand(key, MUTATION_KEYS, EXPANDED_SIZE); - var indexKey = Arrays.copyOfRange(expanded, 0, Spec.Signal.KEY_LENGTH); - var encKey = Arrays.copyOfRange(expanded, Spec.Signal.KEY_LENGTH, Spec.Signal.KEY_LENGTH * 2); - var macKey = Arrays.copyOfRange(expanded, Spec.Signal.KEY_LENGTH * 2, Spec.Signal.KEY_LENGTH * 3); - var snapshotMacKey = Arrays.copyOfRange(expanded, Spec.Signal.KEY_LENGTH * 3, Spec.Signal.KEY_LENGTH * 4); - var patchMacKey = Arrays.copyOfRange(expanded, Spec.Signal.KEY_LENGTH * 4, expanded.length); + var indexKey = Arrays.copyOfRange(expanded, 0, Specification.Signal.KEY_LENGTH); + var encKey = Arrays.copyOfRange(expanded, Specification.Signal.KEY_LENGTH, Specification.Signal.KEY_LENGTH * 2); + var macKey = Arrays.copyOfRange(expanded, Specification.Signal.KEY_LENGTH * 2, Specification.Signal.KEY_LENGTH * 3); + var snapshotMacKey = Arrays.copyOfRange(expanded, Specification.Signal.KEY_LENGTH * 3, Specification.Signal.KEY_LENGTH * 4); + var patchMacKey = Arrays.copyOfRange(expanded, Specification.Signal.KEY_LENGTH * 4, expanded.length); return new MutationKeys(indexKey, encKey, macKey, snapshotMacKey, patchMacKey); } } diff --git a/src/main/java/it/auties/whatsapp/model/sync/PatchRequest.java b/src/main/java/it/auties/whatsapp/model/sync/PatchRequest.java index 437859a19..f65ae6876 100644 --- a/src/main/java/it/auties/whatsapp/model/sync/PatchRequest.java +++ b/src/main/java/it/auties/whatsapp/model/sync/PatchRequest.java @@ -2,7 +2,7 @@ import it.auties.whatsapp.model.sync.RecordSync.Operation; import it.auties.whatsapp.util.Json; -import it.auties.whatsapp.util.Spec; +import it.auties.whatsapp.util.Specification; import java.util.ArrayList; import java.util.Arrays; @@ -11,7 +11,7 @@ public record PatchRequest(PatchType type, List entries) { public record PatchEntry(ActionValueSync sync, String index, int version, Operation operation) { public static PatchEntry of(ActionValueSync sync, Operation operation) { - return of(sync, operation, Spec.Signal.CURRENT_VERSION); + return of(sync, operation, Specification.Signal.CURRENT_VERSION); } public static PatchEntry of(ActionValueSync sync, Operation operation, int version, String... args) { diff --git a/src/main/java/it/auties/whatsapp/socket/AppStateHandler.java b/src/main/java/it/auties/whatsapp/socket/AppStateHandler.java index 96c2d5e07..7cb744c5a 100644 --- a/src/main/java/it/auties/whatsapp/socket/AppStateHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/AppStateHandler.java @@ -32,7 +32,7 @@ import it.auties.whatsapp.model.sync.SnapshotSyncSpec; import it.auties.whatsapp.util.BytesHelper; import it.auties.whatsapp.util.Medias; -import it.auties.whatsapp.util.Spec; +import it.auties.whatsapp.util.Specification; import it.auties.whatsapp.util.Validate; import org.checkerframework.checker.nullness.qual.NonNull; @@ -540,7 +540,7 @@ private byte[][] getSyncMutationMac(PatchSync patch) { return patch.mutations() .stream() .map(mutation -> mutation.record().value().blob()) - .map(entry -> Arrays.copyOfRange(entry, entry.length - Spec.Signal.KEY_LENGTH, entry.length)) + .map(entry -> Arrays.copyOfRange(entry, entry.length - Specification.Signal.KEY_LENGTH, entry.length)) .toArray(byte[][]::new); } @@ -581,8 +581,8 @@ private Optional decodeMutation(Jid jid, RecordSync.Operation op return Optional.empty(); } var blob = sync.value().blob(); - var encryptedBlob = Arrays.copyOfRange(blob, 0, blob.length - Spec.Signal.KEY_LENGTH); - var encryptedMac = Arrays.copyOfRange(blob, blob.length - Spec.Signal.KEY_LENGTH, blob.length); + var encryptedBlob = Arrays.copyOfRange(blob, 0, blob.length - Specification.Signal.KEY_LENGTH); + var encryptedMac = Arrays.copyOfRange(blob, blob.length - Specification.Signal.KEY_LENGTH, blob.length); Validate.isTrue(!socketHandler.store().checkPatchMacs() || Arrays.equals(encryptedMac, generateMac(operation, encryptedBlob, sync.keyId().id(), mutationKeys.get().macKey())), "decode_mutation", HmacValidationException.class); var result = AesCbc.decrypt(encryptedBlob, mutationKeys.get().encKey()); @@ -595,11 +595,11 @@ private Optional decodeMutation(Jid jid, RecordSync.Operation op private byte[] generateMac(RecordSync.Operation operation, byte[] data, byte[] keyId, byte[] key) { var keyData = BytesHelper.concat(operation.content(), keyId); - var last = new byte[Spec.Signal.MAC_LENGTH]; + var last = new byte[Specification.Signal.MAC_LENGTH]; last[last.length - 1] = (byte) keyData.length; var total = BytesHelper.concat(keyData, data, last); var sha512 = Hmac.calculateSha512(total, key); - return Arrays.copyOfRange(sha512, 0, Spec.Signal.KEY_LENGTH); + return Arrays.copyOfRange(sha512, 0, Specification.Signal.KEY_LENGTH); } private byte[] generateSnapshotMac(byte[] ltHash, long version, PatchType patchType, byte[] key) { diff --git a/src/main/java/it/auties/whatsapp/socket/AuthHandler.java b/src/main/java/it/auties/whatsapp/socket/AuthHandler.java index a280ca16a..cb8b07fed 100644 --- a/src/main/java/it/auties/whatsapp/socket/AuthHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/AuthHandler.java @@ -11,7 +11,7 @@ import it.auties.whatsapp.model.signal.auth.UserAgent.PlatformType; import it.auties.whatsapp.model.sync.HistorySyncConfigBuilder; import it.auties.whatsapp.util.BytesHelper; -import it.auties.whatsapp.util.Spec; +import it.auties.whatsapp.util.Specification; import java.util.NoSuchElementException; import java.util.Optional; @@ -31,7 +31,7 @@ protected CompletableFuture login(SocketSession session, byte[] message return CompletableFuture.completedFuture(false); } - var handshake = new SocketHandshake(socketHandler.keys()); + var handshake = new SocketHandshake(socketHandler.keys(), getPrologueData()); handshake.updateHash(socketHandler.keys().ephemeralKeyPair().publicKey()); handshake.updateHash(serverHello.get().ephemeral()); var sharedEphemeral = Curve25519.sharedKey(serverHello.get().ephemeral(), socketHandler.keys().ephemeralKeyPair().privateKey()); @@ -55,6 +55,13 @@ protected CompletableFuture login(SocketSession session, byte[] message } } + private byte[] getPrologueData() { + return switch (socketHandler.store().clientType()) { + case WEB -> Specification.Whatsapp.WEB_PROLOGUE; + case MOBILE -> Specification.Whatsapp.MOBILE_PROLOGUE; + }; + } + private Optional readHandshake(byte[] message) { try { var handshakeMessage = HandshakeMessageSpec.decode(message); @@ -77,7 +84,7 @@ private boolean onHandshakeSent(SocketHandshake handshake) { } private WebInfo createWebInfo() { - if(socketHandler.store().historyLength().size() > Spec.Whatsapp.DEFAULT_HISTORY_SIZE) { + if(socketHandler.store().historyLength().isExtended()) { return null; } @@ -138,7 +145,7 @@ private PlatformType getUserAgentPlatform(boolean mobile) { return socketHandler.store().business() ? device.businessPlatform() : device.platform(); } - if(socketHandler.store().historyLength().size() > Spec.Whatsapp.DEFAULT_HISTORY_SIZE) { + if(socketHandler.store().historyLength().isExtended()) { return PlatformType.WINDOWS; } @@ -198,7 +205,7 @@ private CompanionRegistrationData createRegisterData() { var companion = new CompanionRegistrationDataBuilder() .buildHash(socketHandler.store().version().toHash()) .eRegid(socketHandler.keys().encodedRegistrationId()) - .eKeytype(BytesHelper.intToBytes(Spec.Signal.KEY_TYPE, 1)) + .eKeytype(BytesHelper.intToBytes(Specification.Signal.KEY_TYPE, 1)) .eIdent(socketHandler.keys().identityKeyPair().publicKey()) .eSkeyId(socketHandler.keys().signedKeyPair().encodedId()) .eSkeyVal(socketHandler.keys().signedKeyPair().keyPair().publicKey()) @@ -215,17 +222,16 @@ private CompanionRegistrationData createRegisterData() { private CompanionProperties createCompanionProps() { return switch (socketHandler.store().clientType()) { case WEB -> { - var syncSize = socketHandler.store().historyLength().size(); - var fullSync = syncSize > Spec.Whatsapp.DEFAULT_HISTORY_SIZE; + var historyLength = socketHandler.store().historyLength(); var config = new HistorySyncConfigBuilder() .inlineInitialPayloadInE2EeMsg(true) .supportBotUserAgentChatHistory(true) - .storageQuotaMb(syncSize) + .storageQuotaMb(historyLength.size()) .build(); yield new CompanionPropertiesBuilder() .os(socketHandler.store().name()) - .platformType(fullSync ? CompanionProperties.PlatformType.DESKTOP : CompanionProperties.PlatformType.CHROME) - .requireFullSync(fullSync) + .platformType(CompanionProperties.PlatformType.CHROME) + .requireFullSync(historyLength.isExtended()) .historySyncConfig(config) .build(); } diff --git a/src/main/java/it/auties/whatsapp/socket/MessageHandler.java b/src/main/java/it/auties/whatsapp/socket/MessageHandler.java index ebae39690..4f60117a0 100644 --- a/src/main/java/it/auties/whatsapp/socket/MessageHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/MessageHandler.java @@ -4,15 +4,17 @@ import it.auties.whatsapp.model.action.ContactAction; import it.auties.whatsapp.model.business.BusinessVerifiedNameCertificateSpec; import it.auties.whatsapp.model.chat.*; -import it.auties.whatsapp.model.contact.*; +import it.auties.whatsapp.model.contact.Contact; +import it.auties.whatsapp.model.contact.ContactStatus; import it.auties.whatsapp.model.info.MessageIndexInfo; import it.auties.whatsapp.model.info.MessageInfo; import it.auties.whatsapp.model.info.MessageInfoBuilder; -import it.auties.whatsapp.model.jid.JidType; import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.jid.JidServer; +import it.auties.whatsapp.model.jid.JidType; import it.auties.whatsapp.model.message.button.*; import it.auties.whatsapp.model.message.model.*; +import it.auties.whatsapp.model.message.model.reserved.LocalMediaMessage; import it.auties.whatsapp.model.message.payment.PaymentOrderMessage; import it.auties.whatsapp.model.message.server.DeviceSentMessage; import it.auties.whatsapp.model.message.server.ProtocolMessage; @@ -35,6 +37,7 @@ import it.auties.whatsapp.model.sync.PushName; import it.auties.whatsapp.util.*; +import java.io.ByteArrayOutputStream; import java.lang.System.Logger; import java.lang.System.Logger.Level; import java.nio.charset.StandardCharsets; @@ -48,7 +51,7 @@ import java.util.stream.Stream; import static it.auties.whatsapp.api.ErrorHandler.Location.*; -import static it.auties.whatsapp.util.Spec.Signal.*; +import static it.auties.whatsapp.util.Specification.Signal.*; class MessageHandler { private static final int HISTORY_SYNC_TIMEOUT = 25; @@ -72,11 +75,9 @@ protected MessageHandler(SocketHandler socketHandler) { } protected synchronized CompletableFuture encode(MessageSendRequest request) { - lock.lock(); return encodeMessage(request) .thenRunAsync(() -> attributeOutgoingMessage(request)) - .exceptionallyAsync(throwable -> onEncodeError(request, throwable)) - .thenRun(lock::unlock); + .exceptionallyAsync(throwable -> onEncodeError(request, throwable)); } private CompletableFuture encodeMessage(MessageSendRequest request) { @@ -85,29 +86,51 @@ private CompletableFuture encodeMessage(MessageSendRequest request) { private CompletableFuture encodePlainMessage(MessageSendRequest request) { var message = request.info().message(); - var messageAttributes = Attributes.ofNullable(request.additionalAttributes()) - .put("mediatype", getMediaType(message), Objects::nonNull) - .toMap(); - var messageNode = switch (message.content()) { - case TextMessage textMessage -> Node.of("plaintext", textMessage.text().getBytes(StandardCharsets.UTF_8)); - case ImageMessage imageMessage -> imageMessage.caption() - .map(caption -> Node.of("plaintext", messageAttributes, caption.getBytes(StandardCharsets.UTF_8))); - case VideoOrGifMessage imageMessage -> imageMessage.caption() - .map(caption -> Node.of("plaintext", messageAttributes, caption.getBytes(StandardCharsets.UTF_8))); - default -> throw new UnsupportedOperationException("Unsupported message type in channel: " + message.type()); - }; + var messageNode = getPlainMessageNode(message, request.additionalAttributes()); var type = message.content().type() == MessageType.TEXT ? "text" : "media"; var attributes = Attributes.of() .put("id", request.info().id()) - .put("to", request.info().chatJid().withServer(JidServer.WHATSAPP)) + .put("to", request.info().chatJid()) .put("type", type) + .put("media_id", getPlainMessageHandle(request), Objects::nonNull) .toMap(); return socketHandler.send(Node.of("message", attributes, messageNode)) .thenRunAsync(() -> attributeOutgoingMessage(request)); } + private String getPlainMessageHandle(MessageSendRequest request) { + var message = request.info().message().content(); + if (!(message instanceof LocalMediaMessage localMediaMessage)) { + return null; + } + + return localMediaMessage.handle().orElse(null); + } + + private Node getPlainMessageNode(MessageContainer message, Map additionalAttributes) { + var messageAttributes = Attributes.ofNullable(additionalAttributes) + .put("mediatype", getMediaType(message), Objects::nonNull) + .toMap(); + return switch (message.content()) { + case TextMessage textMessage -> { + var byteArrayOutputStream = new ByteArrayOutputStream(); + byteArrayOutputStream.write(10); + byteArrayOutputStream.writeBytes(BytesHelper.intToVarInt(textMessage.text().length())); + byteArrayOutputStream.writeBytes(textMessage.text().getBytes(StandardCharsets.UTF_8)); + yield Node.of("plaintext", byteArrayOutputStream.toByteArray()); + } + case ReactionMessage reactionMessage -> Node.of("reaction", Map.of("code", reactionMessage.content())); + default -> Node.of("plaintext", messageAttributes, MessageContainerSpec.encode(message)); + }; + } + private CompletableFuture encodeE2EMessage(MessageSendRequest request) { - return request.peer() || isConversation(request.info()) ? encodeConversation(request) : encodeGroup(request); + try { + lock.lock(); + return request.peer() || isConversation(request.info()) ? encodeConversation(request) : encodeGroup(request); + }finally { + lock.unlock(); + } } private Void onEncodeError(MessageSendRequest request, Throwable throwable) { @@ -711,7 +734,7 @@ private void onHistorySyncNotification(MessageInfo info, ProtocolMessage protoco } private boolean isZeroHistorySyncComplete() { - return socketHandler.store().historyLength().size() == 0 + return socketHandler.store().historyLength().isZero() && historySyncTypes.contains(Type.INITIAL_STATUS_V3) && historySyncTypes.contains(Type.PUSH_NAME) && historySyncTypes.contains(Type.INITIAL_BOOTSTRAP) @@ -799,7 +822,7 @@ private Contact createNewContact(Jid jid) { } private void handleInitialBootstrap(HistorySync history) { - if(socketHandler.store().historyLength().size() != 0){ + if(!socketHandler.store().historyLength().isZero()){ var jids = history.conversations() .stream() .map(Chat::jid) @@ -812,7 +835,7 @@ private void handleInitialBootstrap(HistorySync history) { } private void handleChatsSync(HistorySync history) { - if(socketHandler.store().historyLength().size() == 0){ + if(socketHandler.store().historyLength().isZero()){ return; } diff --git a/src/main/java/it/auties/whatsapp/socket/SocketHandler.java b/src/main/java/it/auties/whatsapp/socket/SocketHandler.java index ceb6c1125..e3b5efded 100644 --- a/src/main/java/it/auties/whatsapp/socket/SocketHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/SocketHandler.java @@ -11,7 +11,8 @@ import it.auties.whatsapp.model.business.BusinessCategory; import it.auties.whatsapp.model.call.Call; import it.auties.whatsapp.model.chat.*; -import it.auties.whatsapp.model.contact.*; +import it.auties.whatsapp.model.contact.Contact; +import it.auties.whatsapp.model.contact.ContactStatus; import it.auties.whatsapp.model.info.MessageIndexInfo; import it.auties.whatsapp.model.info.MessageInfo; import it.auties.whatsapp.model.info.MessageInfoBuilder; @@ -249,11 +250,16 @@ public synchronized CompletableFuture connect() { this.logoutFuture = new CompletableFuture<>(); } - this.session = new SocketSession(store.proxy().orElse(null), socketExecutor); + this.session = SocketSession.of(store.proxy().orElse(null), socketExecutor, isWebSocket()); return session.connect(this) .thenCompose(ignored -> loginFuture); } + private boolean isWebSocket() { + return store.clientType() == ClientType.WEB + && !store.historyLength().isExtended(); + } + public CompletableFuture loginFuture() { return loginFuture; } @@ -273,13 +279,13 @@ public synchronized CompletableFuture disconnect(DisconnectReason reason) return switch (reason) { case DISCONNECTED -> { if (session != null) { - session.close(); + session.disconnect(); } yield CompletableFuture.completedFuture(null); } case RECONNECTING -> { if (session != null) { - session.close(); + session.disconnect(); } yield connect(); } @@ -287,7 +293,7 @@ public synchronized CompletableFuture disconnect(DisconnectReason reason) store.deleteSession(); store.resolveAllPendingRequests(); if (session != null) { - session.close(); + session.disconnect(); } yield CompletableFuture.completedFuture(null); } @@ -296,7 +302,7 @@ public synchronized CompletableFuture disconnect(DisconnectReason reason) store.resolveAllPendingRequests(); var oldListeners = new ArrayList<>(store.listeners()); if (session != null) { - session.close(); + session.disconnect(); } var uuid = UUID.randomUUID(); var number = store.phoneNumber() diff --git a/src/main/java/it/auties/whatsapp/socket/SocketHandshake.java b/src/main/java/it/auties/whatsapp/socket/SocketHandshake.java index 4a360c9e5..eb5e1d3b7 100644 --- a/src/main/java/it/auties/whatsapp/socket/SocketHandshake.java +++ b/src/main/java/it/auties/whatsapp/socket/SocketHandshake.java @@ -5,33 +5,33 @@ import it.auties.whatsapp.crypto.Hkdf; import it.auties.whatsapp.crypto.Sha256; import it.auties.whatsapp.util.BytesHelper; -import it.auties.whatsapp.util.Spec; +import it.auties.whatsapp.util.Specification; import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Arrays; -public class SocketHandshake { +class SocketHandshake { private final Keys keys; private byte[] hash; private byte[] salt; private byte[] cryptoKey; private long counter; - public SocketHandshake(Keys keys) { + SocketHandshake(Keys keys, byte[] prologue) { this.keys = keys; - this.hash = Spec.Whatsapp.PROTOCOL; - this.salt = Spec.Whatsapp.PROTOCOL; - this.cryptoKey = Spec.Whatsapp.PROTOCOL; + this.hash = Specification.Whatsapp.NOISE_PROTOCOL; + this.salt = Specification.Whatsapp.NOISE_PROTOCOL; + this.cryptoKey = Specification.Whatsapp.NOISE_PROTOCOL; this.counter = 0; - updateHash(keys.prologue()); + updateHash(prologue); } - public void updateHash(byte @NonNull [] data) { + void updateHash(byte @NonNull [] data) { var input = BytesHelper.concat(hash, data); this.hash = Sha256.calculate(input); } - public byte[] cipher(byte @NonNull [] bytes, boolean encrypt) { + byte[] cipher(byte @NonNull [] bytes, boolean encrypt) { var cyphered = encrypt ? AesGcm.encrypt(counter++, bytes, cryptoKey, hash) : AesGcm.decrypt(counter++, bytes, cryptoKey, hash); if (!encrypt) { updateHash(bytes); @@ -41,24 +41,24 @@ public byte[] cipher(byte @NonNull [] bytes, boolean encrypt) { return cyphered; } - public void finish() { + void finish() { var expanded = Hkdf.extractAndExpand(new byte[0], salt, null, 64); keys.setWriteKey(Arrays.copyOfRange(expanded, 0, 32)); keys.setReadKey(Arrays.copyOfRange(expanded, 32, 64)); dispose(); } - private void dispose() { - this.hash = null; - this.salt = null; - this.cryptoKey = null; - this.counter = 0; - } - - public void mixIntoKey(byte @NonNull [] bytes) { + void mixIntoKey(byte @NonNull [] bytes) { var expanded = Hkdf.extractAndExpand(bytes, salt, null, 64); this.salt = Arrays.copyOfRange(expanded, 0, 32); this.cryptoKey = Arrays.copyOfRange(expanded, 32, 64); this.counter = 0; } + + void dispose() { + this.hash = null; + this.salt = null; + this.cryptoKey = null; + this.counter = 0; + } } \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/socket/SocketSession.java b/src/main/java/it/auties/whatsapp/socket/SocketSession.java index 6a04b2213..aaf31c754 100644 --- a/src/main/java/it/auties/whatsapp/socket/SocketSession.java +++ b/src/main/java/it/auties/whatsapp/socket/SocketSession.java @@ -1,164 +1,293 @@ package it.auties.whatsapp.socket; +import io.netty.buffer.ByteBuf; import it.auties.whatsapp.util.BytesHelper; import it.auties.whatsapp.util.ProxyAuthenticator; +import it.auties.whatsapp.util.Specification; +import jakarta.websocket.*; import java.io.DataInputStream; import java.io.IOException; import java.io.UncheckedIOException; import java.net.*; import java.net.Proxy.Type; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.OptionalInt; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; -import static it.auties.whatsapp.util.Spec.Whatsapp.APP_ENDPOINT_HOST; -import static it.auties.whatsapp.util.Spec.Whatsapp.APP_ENDPOINT_PORT; +import static it.auties.whatsapp.util.Specification.Whatsapp.SOCKET_ENDPOINT; +import static it.auties.whatsapp.util.Specification.Whatsapp.SOCKET_PORT; -public class SocketSession { - static { - Authenticator.setDefault(new ProxyAuthenticator()); - } - - private final URI proxy; - private final Executor executor; - private Socket socket; - private SocketListener listener; - private boolean closed; - - SocketSession(URI proxy, Executor executor) { +public abstract sealed class SocketSession permits SocketSession.WebSocketSession, SocketSession.RawSocketSession { + final URI proxy; + final Executor executor; + SocketListener listener; + private SocketSession(URI proxy, Executor executor) { this.proxy = proxy; this.executor = executor; } - CompletableFuture connect(SocketListener listener) { - if (socket != null && isOpen()) { - return CompletableFuture.completedFuture(null); - } + abstract CompletableFuture connect(SocketListener listener); + abstract CompletableFuture disconnect(); + public abstract CompletableFuture sendBinary(byte[] bytes); + abstract boolean isOpen(); - return CompletableFuture.runAsync(() -> { - try { - this.listener = listener; - this.socket = new Socket(getProxy()); - socket.setKeepAlive(true); - socket.connect(new InetSocketAddress(APP_ENDPOINT_HOST, APP_ENDPOINT_PORT)); - executor.execute(this::readMessages); - this.closed = false; - listener.onOpen(this); - } catch (IOException exception) { - throw new UncheckedIOException("Cannot connect to host", exception); - } - }, executor); + int decodeLength(ByteBuf buffer) { + return (buffer.readByte() << 16) | buffer.readUnsignedShort(); } - private Proxy getProxy() { - if (proxy == null) { - return Proxy.NO_PROXY; + static SocketSession of(URI proxy, Executor executor, boolean webSocket){ + if(webSocket) { + return new WebSocketSession(proxy, executor); } - var scheme = Objects.requireNonNull(proxy.getScheme(), "Invalid proxy, expected a scheme: %s".formatted(proxy)); - var host = Objects.requireNonNull(proxy.getHost(), "Invalid proxy, expected a host: %s".formatted(proxy)); - var port = getProxyPort(scheme).orElseThrow(() -> new NullPointerException("Invalid proxy, expected a port: %s".formatted(proxy))); - return switch (scheme) { - case "http", "https" -> new Proxy(Type.HTTP, new InetSocketAddress(host, port)); - case "socks4", "socks5" -> new Proxy(Type.SOCKS, new InetSocketAddress(host, port)); - default -> throw new IllegalStateException("Unexpected scheme: " + scheme); - }; + return new RawSocketSession(proxy, executor); } - private OptionalInt getProxyPort(String scheme) { - return proxy.getPort() != -1 ? OptionalInt.of(proxy.getPort()) : switch (scheme) { - case "http" -> OptionalInt.of(80); - case "https" -> OptionalInt.of(443); - default -> OptionalInt.empty(); - }; - } + @ClientEndpoint(configurator = WebSocketSession.OriginPatcher.class) + public static final class WebSocketSession extends SocketSession { + private Session session; - @SuppressWarnings("UnusedReturnValue") - CompletableFuture close() { - if (socket == null || !isOpen()) { - return CompletableFuture.completedFuture(null); + WebSocketSession(URI proxy, Executor executor) { + super(proxy, executor); } - return CompletableFuture.runAsync(() -> { + @Override + public CompletableFuture connect(SocketListener listener) { + return CompletableFuture.runAsync(() -> { + try { + this.listener = listener; + this.session = ContainerProvider.getWebSocketContainer().connectToServer(this, Specification.Whatsapp.WEB_SOCKET_ENDPOINT); + } catch (IOException exception) { + throw new UncheckedIOException(exception); + } catch (DeploymentException exception) { + throw new RuntimeException(exception); + } + }); + } + + @Override + public CompletableFuture disconnect() { try { - socket.close(); - closeResources(); + session.close(); + return CompletableFuture.completedFuture(null); } catch (IOException exception) { - throw new UncheckedIOException("Cannot close connection to host", exception); + throw new UncheckedIOException(exception); } - }, executor); - } - - public boolean isOpen() { - return socket != null && socket.isConnected(); - } + } - public CompletableFuture sendBinary(byte[] bytes) { - return CompletableFuture.runAsync(() -> { + @Override + public CompletableFuture sendBinary(byte[] bytes) { + var future = new CompletableFuture(); try { - if (socket == null) { - return; - } - var stream = socket.getOutputStream(); - stream.write(bytes); - stream.flush(); - }catch (SocketException exception) { - closeResources(); - } catch (IOException exception) { - throw new UncheckedIOException("Cannot send message", exception); + session.getAsyncRemote().sendBinary(ByteBuffer.wrap(bytes), result -> { + if (result.isOK()) { + future.complete(null); + return; + } + + future.completeExceptionally(result.getException()); + }); + } catch (Throwable throwable) { + future.completeExceptionally(throwable); } - }, executor); - } - private void readMessages() { - try (var input = new DataInputStream(socket.getInputStream())) { - while (isOpen()) { - var length = decodeLength(input); + return future; + } + + @Override + public boolean isOpen() { + return session == null || session.isOpen(); + } + + @OnOpen + @SuppressWarnings("unused") + public void onOpen(Session session) { + this.session = session; + listener.onOpen(this); + } + + @OnClose + @SuppressWarnings("unused") + public void onClose() { + listener.onClose(); + } + + @OnError + @SuppressWarnings("unused") + public void onError(Throwable throwable) { + listener.onError(throwable); + } + + @OnMessage + @SuppressWarnings("unused") + public void onBinary(byte[] message) { + var buffer = BytesHelper.newBuffer(message); + while (buffer.readableBytes() >= 3) { + var length = decodeLength(buffer); if (length < 0) { - break; - } - var message = new byte[length]; - if(isOpen()) { - input.readFully(message); + continue; } - listener.onMessage(message); + + var result = buffer.readBytes(length); + listener.onMessage(BytesHelper.readBuffer(result)); + result.release(); } - } catch(Throwable throwable) { - listener.onError(throwable); - } finally { - closeResources(); + buffer.release(); } - } - private int decodeLength(DataInputStream input) { - try { - var lengthBytes = new byte[3]; - input.readFully(lengthBytes); - return decodeLength(lengthBytes); - } catch (IOException exception) { - return -1; + public static class OriginPatcher extends ClientEndpointConfig.Configurator { + @Override + public void beforeRequest(Map> headers) { + headers.put("Origin", List.of(Specification.Whatsapp.WEB_ORIGIN)); + headers.put("Host", List.of(Specification.Whatsapp.WEB_HOST)); + } } } - private int decodeLength(byte[] input) { - var buffer = BytesHelper.newBuffer(input); - return (buffer.readByte() << 16) | buffer.readUnsignedShort(); - } + static final class RawSocketSession extends SocketSession { + static { + Authenticator.setDefault(new ProxyAuthenticator()); + } + + private Socket socket; + private boolean closed; + + RawSocketSession(URI proxy, Executor executor) { + super(proxy, executor); + } + + @Override + CompletableFuture connect(SocketListener listener) { + if (socket != null && isOpen()) { + return CompletableFuture.completedFuture(null); + } + + return CompletableFuture.runAsync(() -> { + try { + this.listener = listener; + this.socket = new Socket(getProxy()); + socket.setKeepAlive(true); + socket.connect(new InetSocketAddress(SOCKET_ENDPOINT, SOCKET_PORT)); + executor.execute(this::readMessages); + this.closed = false; + listener.onOpen(this); + } catch (IOException exception) { + throw new UncheckedIOException("Cannot connect to host", exception); + } + }, executor); + } + + private Proxy getProxy() { + if (proxy == null) { + return Proxy.NO_PROXY; + } + + var scheme = Objects.requireNonNull(proxy.getScheme(), "Invalid proxy, expected a scheme: %s".formatted(proxy)); + var host = Objects.requireNonNull(proxy.getHost(), "Invalid proxy, expected a host: %s".formatted(proxy)); + var port = getProxyPort(scheme).orElseThrow(() -> new NullPointerException("Invalid proxy, expected a port: %s".formatted(proxy))); + return switch (scheme) { + case "http", "https" -> new Proxy(Type.HTTP, new InetSocketAddress(host, port)); + case "socks4", "socks5" -> new Proxy(Type.SOCKS, new InetSocketAddress(host, port)); + default -> throw new IllegalStateException("Unexpected scheme: " + scheme); + }; + } + + private OptionalInt getProxyPort(String scheme) { + return proxy.getPort() != -1 ? OptionalInt.of(proxy.getPort()) : switch (scheme) { + case "http" -> OptionalInt.of(80); + case "https" -> OptionalInt.of(443); + default -> OptionalInt.empty(); + }; + } + + @Override + @SuppressWarnings("UnusedReturnValue") + CompletableFuture disconnect() { + if (socket == null || !isOpen()) { + return CompletableFuture.completedFuture(null); + } - private void closeResources() { - if(closed) { - return; + return CompletableFuture.runAsync(() -> { + try { + socket.close(); + closeResources(); + } catch (IOException exception) { + throw new UncheckedIOException("Cannot close connection to host", exception); + } + }, executor); + } + + @Override + public boolean isOpen() { + return socket != null && socket.isConnected(); + } + + @Override + public CompletableFuture sendBinary(byte[] bytes) { + return CompletableFuture.runAsync(() -> { + try { + if (socket == null) { + return; + } + var stream = socket.getOutputStream(); + stream.write(bytes); + stream.flush(); + }catch (SocketException exception) { + closeResources(); + } catch (IOException exception) { + throw new UncheckedIOException("Cannot send message", exception); + } + }, executor); } - this.closed = true; - this.socket = null; - if (executor instanceof ExecutorService service) { - service.shutdownNow(); + private void readMessages() { + try (var input = new DataInputStream(socket.getInputStream())) { + while (isOpen()) { + var length = decodeLength(input); + if (length < 0) { + break; + } + var message = new byte[length]; + if(isOpen()) { + input.readFully(message); + } + listener.onMessage(message); + } + } catch(Throwable throwable) { + listener.onError(throwable); + } finally { + closeResources(); + } } - listener.onClose(); + private int decodeLength(DataInputStream input) { + try { + var lengthBytes = new byte[3]; + input.readFully(lengthBytes); + return decodeLength(BytesHelper.newBuffer(lengthBytes)); + } catch (IOException exception) { + return -1; + } + } + + private void closeResources() { + if(closed) { + return; + } + + this.closed = true; + this.socket = null; + if (executor instanceof ExecutorService service) { + service.shutdownNow(); + } + + listener.onClose(); + } } } \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/socket/StreamHandler.java b/src/main/java/it/auties/whatsapp/socket/StreamHandler.java index 535edff06..74847fd9c 100644 --- a/src/main/java/it/auties/whatsapp/socket/StreamHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/StreamHandler.java @@ -72,9 +72,9 @@ import java.util.stream.Stream; import static it.auties.whatsapp.api.ErrorHandler.Location.*; -import static it.auties.whatsapp.util.Spec.Signal.KEY_BUNDLE_TYPE; -import static it.auties.whatsapp.util.Spec.Whatsapp.ACCOUNT_SIGNATURE_HEADER; -import static it.auties.whatsapp.util.Spec.Whatsapp.DEVICE_WEB_SIGNATURE_HEADER; +import static it.auties.whatsapp.util.Specification.Signal.KEY_BUNDLE_TYPE; +import static it.auties.whatsapp.util.Specification.Whatsapp.ACCOUNT_SIGNATURE_HEADER; +import static it.auties.whatsapp.util.Specification.Whatsapp.DEVICE_WEB_SIGNATURE_HEADER; class StreamHandler { private static final int REQUIRED_PRE_KEYS_SIZE = 5; @@ -706,9 +706,9 @@ private void digestIb(Node node) { if (!Objects.equals(type, "account_sync")) { return; } - var timestamp = dirty.get().attributes().getString("timestampSeconds"); + var timestamp = dirty.get().attributes().getString("timestamp"); socketHandler.sendQuery("set", "urn:xmpp:whatsapp:dirty", - Node.of("clean", Map.of("type", type, "timestampSeconds", timestamp))); + Node.of("clean", Map.of("type", type, "timestamp", timestamp))); } private void digestError(Node node) { @@ -877,7 +877,7 @@ private CompletableFuture queryRequiredMobileInfo() { .exceptionallyAsync(exception -> socketHandler.handleFailure(LOGIN, exception)); socketHandler.sendQuery("get", "urn:xmpp:whatsapp:push", Node.of("config", Map.of("version", 1))) .exceptionallyAsync(exception -> socketHandler.handleFailure(LOGIN, exception)); - socketHandler.sendQuery("set", "urn:xmpp:whatsapp:dirty", Node.of("clean", Map.of("timestampSeconds", 0, "type", "account_sync"))) + socketHandler.sendQuery("set", "urn:xmpp:whatsapp:dirty", Node.of("clean", Map.of("timestamp", 0, "type", "account_sync"))) .exceptionallyAsync(exception -> socketHandler.handleFailure(LOGIN, exception)); if(socketHandler.store().business()){ socketHandler.sendQuery("get", "fb:thrift_iq", Map.of("smax_id", 42), Node.of("linked_accounts")) @@ -1220,7 +1220,6 @@ private void sendConfirmNode(Node node, Node content) { private void saveCompanion(Node container) { var node = container.findNode("device") .orElseThrow(() -> new NoSuchElementException("Missing device")); - var isBusiness = container.hasNode("business"); var companion = node.attributes() .getJid("jid") .orElseThrow(() -> new NoSuchElementException("Missing companion")); @@ -1232,7 +1231,7 @@ private void saveCompanion(Node container) { .map(this::getCompanionOs) .orElseThrow(() -> new NoSuchElementException("Unknown platform: " + container)); socketHandler.store().setCompanionDeviceOs(companionOs); - socketHandler.store().setBusiness(isBusiness); + socketHandler.store().setBusiness(companionOs == PlatformType.SMB_ANDROID || companionOs == PlatformType.SMB_IOS); var me = new Contact(companion.withoutDevice(), socketHandler.store().name(), null, null, ContactStatus.AVAILABLE, ZonedDateTime.now(), false); socketHandler.store().addContact(me); } diff --git a/src/main/java/it/auties/whatsapp/util/BytesHelper.java b/src/main/java/it/auties/whatsapp/util/BytesHelper.java index ed3fc7e6f..1d055a07f 100644 --- a/src/main/java/it/auties/whatsapp/util/BytesHelper.java +++ b/src/main/java/it/auties/whatsapp/util/BytesHelper.java @@ -16,7 +16,7 @@ import java.util.zip.Deflater; import java.util.zip.Inflater; -import static it.auties.whatsapp.util.Spec.Signal.CURRENT_VERSION; +import static it.auties.whatsapp.util.Specification.Signal.CURRENT_VERSION; public final class BytesHelper { private static final String CROCKFORD_CHARACTERS = "123456789ABCDEFGHJKLMNPQRSTVWXYZ"; @@ -155,6 +155,17 @@ public static byte[] intToBytes(int input, int length) { return result; } + public static byte[] intToVarInt(int value) { + var out = new ByteArrayOutputStream(); + while ((value & 0xFFFFFF80) != 0L) { + out.write((byte) ((value & 0x7F) | 0x80)); + value >>>= 7; + } + out.write((byte) (value & 0x7F)); + return out.toByteArray(); + } + + public static int bytesToInt(byte[] bytes, int length) { var result = 0; for (var i = 0; i < length; i++) { diff --git a/src/main/java/it/auties/whatsapp/util/ConcurrentDoublyLinkedList.java b/src/main/java/it/auties/whatsapp/util/ConcurrentDoublyLinkedList.java index 60c9e04d7..8af704510 100644 --- a/src/main/java/it/auties/whatsapp/util/ConcurrentDoublyLinkedList.java +++ b/src/main/java/it/auties/whatsapp/util/ConcurrentDoublyLinkedList.java @@ -45,7 +45,7 @@ * @param * the type of elements held in this collection */ - +@SuppressWarnings("ALL") public class ConcurrentDoublyLinkedList extends AbstractCollection implements java.io.Serializable { @@ -686,17 +686,17 @@ public int hashCode() { * Linked Nodes. As a minor efficiency hack, this class opportunistically * inherits from AtomicReference, with the atomic ref used as the "next" * link. - * + *

* Nodes are in doubly-linked lists. There are three kinds of special nodes, * distinguished by: * The list header has a null prev link * The list * trailer has a null next link * A deletion marker has a prev link pointing * to itself. All three kinds of special nodes have null element fields. - * + *

* Regular nodes have non-null element, next, and prev fields. To avoid * visible inconsistencies when deletions overlap element replacement, * replacements are done by replacing the node, not just setting the * element. - * + *

* Nodes can be traversed by read-only ConcurrentLinkedDeque class * operations just by following raw next pointers, so long as they ignore * any special nodes seen along the way. (This is automated in method @@ -704,7 +704,7 @@ public int hashCode() { * all live nodes since a prev pointer of a deleted node can become * unrecoverably stale. */ - +@SuppressWarnings("ALL") class Node extends AtomicReference> { private volatile Node prev; diff --git a/src/main/java/it/auties/whatsapp/util/Medias.java b/src/main/java/it/auties/whatsapp/util/Medias.java index 943c906c2..4aa959b18 100644 --- a/src/main/java/it/auties/whatsapp/util/Medias.java +++ b/src/main/java/it/auties/whatsapp/util/Medias.java @@ -5,7 +5,7 @@ import it.auties.whatsapp.crypto.Sha256; import it.auties.whatsapp.exception.HmacValidationException; import it.auties.whatsapp.model.media.*; -import it.auties.whatsapp.util.Spec.Whatsapp; +import it.auties.whatsapp.util.Specification.Whatsapp; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.poi.hslf.usermodel.HSLFSlideShow; @@ -115,28 +115,50 @@ public static CompletableFuture downloadAsync(URI imageUri, boolean user public static CompletableFuture upload(byte[] file, AttachmentType type, MediaConnection mediaConnection) { var auth = URLEncoder.encode(mediaConnection.auth(), StandardCharsets.UTF_8); var uploadData = type.inflatable() ? BytesHelper.compress(file) : file; - var fileSha256 = Sha256.calculate(uploadData); - var keys = MediaKeys.random(type.keyName()); - var encryptedMedia = AesCbc.encrypt(keys.iv(), uploadData, keys.cipherKey()); - var hmac = calculateMac(encryptedMedia, keys); - var encrypted = BytesHelper.concat(encryptedMedia, hmac); - var fileEncSha256 = Sha256.calculate(encrypted); - var token = Base64.getUrlEncoder().withoutPadding().encodeToString(fileEncSha256); - var uri = URI.create("https://%s/%s/%s?auth=%s&token=%s".formatted(DEFAULT_HOST, type.path(), token, auth, token)); + var mediaFile = prepareMediaFile(type, uploadData); + var path = type.path().orElseThrow(() -> new UnsupportedOperationException(type + " cannot be uploaded")); + var token = Base64.getUrlEncoder() + .withoutPadding() + .encodeToString(Objects.requireNonNullElse(mediaFile.fileEncSha256(), mediaFile.fileSha256())); + var uri = URI.create("https://%s/%s/%s?auth=%s&token=%s".formatted(DEFAULT_HOST, path, token, auth, token)); var request = HttpRequest.newBuilder() - .POST(ofByteArray(encrypted)) + .POST(ofByteArray(Objects.requireNonNullElse(mediaFile.encryptedFile(), file))) .uri(uri) .header("Content-Type", "application/octet-stream") .header("Accept", "application/json") .header("Origin", Whatsapp.WEB_ORIGIN) .build(); return CLIENT.sendAsync(request, ofString()).thenApplyAsync(response -> { - Validate.isTrue(response.statusCode() == 200, "Invalid status countryCode: %s", response.statusCode()); + Validate.isTrue(response.statusCode() == 200, "Invalid status code: %s", response.statusCode()); var upload = Json.readValue(response.body(), MediaUpload.class); - return new MediaFile(fileSha256, fileEncSha256, keys.mediaKey(), uploadData.length, upload.directPath(), upload.url()); + return new MediaFile( + mediaFile.encryptedFile(), + mediaFile.fileSha256(), + mediaFile.fileEncSha256(), + mediaFile.mediaKey(), + mediaFile.fileLength(), + upload.directPath(), + upload.url(), + upload.handle(), + mediaFile.timestamp() + ); }); } + private static MediaFile prepareMediaFile(AttachmentType type, byte[] uploadData) { + var fileSha256 = Sha256.calculate(uploadData); + if(type.keyName().isEmpty()) { + return new MediaFile(null, fileSha256, null, null, uploadData.length, null, null, null, null); + } + + var keys = MediaKeys.random(type.keyName().orElseThrow()); + var encryptedMedia = AesCbc.encrypt(keys.iv(), uploadData, keys.cipherKey()); + var hmac = calculateMac(encryptedMedia, keys); + var encrypted = BytesHelper.concat(encryptedMedia, hmac); + var fileEncSha256 = Sha256.calculate(encrypted); + return new MediaFile(encrypted, fileSha256, fileEncSha256, keys.mediaKey(), uploadData.length, null, null, null, Clock.nowSeconds()); + } + private static byte[] calculateMac(byte[] encryptedMedia, MediaKeys keys) { var hmacInput = BytesHelper.concat(keys.iv(), encryptedMedia); var hmac = Hmac.calculateSha256(hmacInput, keys.macKey()); @@ -174,7 +196,13 @@ private static Optional handleResponse(MutableAttachmentProvider prov "Cannot decode media: Invalid sha256 signature", SecurityException.class); var encryptedMedia = Arrays.copyOf(body, body.length - 10); var mediaMac = Arrays.copyOfRange(body, body.length - 10, body.length); - var keys = MediaKeys.of(provider.mediaKey().orElseThrow(() -> new NoSuchElementException("Missing media key")), provider.attachmentType().keyName()); + var keyName = provider.attachmentType().keyName(); + if(keyName.isEmpty()) { + return Optional.of(encryptedMedia); + } + + var mediaKey = provider.mediaKey().orElseThrow(() -> new NoSuchElementException("Missing media key")); + var keys = MediaKeys.of(mediaKey, keyName.get()); var hmac = calculateMac(encryptedMedia, keys); Validate.isTrue(Arrays.equals(hmac, mediaMac), "media_decryption", HmacValidationException.class); var decrypted = AesCbc.decrypt(keys.iv(), encryptedMedia, keys.cipherKey()); @@ -372,7 +400,7 @@ private static Optional getPdfThumbnail(byte[] file) { try (var document = PDDocument.load(file); var outputStream = new ByteArrayOutputStream()) { var renderer = new PDFRenderer(document); var image = renderer.renderImage(0); - var thumb = new BufferedImage(Spec.Whatsapp.THUMBNAIL_WIDTH, Spec.Whatsapp.THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_RGB); + var thumb = new BufferedImage(Specification.Whatsapp.THUMBNAIL_WIDTH, Specification.Whatsapp.THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_RGB); var graphics2D = thumb.createGraphics(); graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); graphics2D.drawImage(image, 0, 0, thumb.getWidth(), thumb.getHeight(), null); @@ -389,7 +417,7 @@ private static Optional getPresentationThumbnail(byte[] file) { if (ppt.getSlides().isEmpty()) { return Optional.empty(); } - var thumb = new BufferedImage(Spec.Whatsapp.THUMBNAIL_WIDTH, Spec.Whatsapp.THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_RGB); + var thumb = new BufferedImage(Specification.Whatsapp.THUMBNAIL_WIDTH, Specification.Whatsapp.THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_RGB); var graphics2D = thumb.createGraphics(); graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); ppt.getSlides().get(0).draw(graphics2D); diff --git a/src/main/java/it/auties/whatsapp/util/MetadataHelper.java b/src/main/java/it/auties/whatsapp/util/MetadataHelper.java index cb84ece97..31a7a188e 100644 --- a/src/main/java/it/auties/whatsapp/util/MetadataHelper.java +++ b/src/main/java/it/auties/whatsapp/util/MetadataHelper.java @@ -5,7 +5,7 @@ import it.auties.whatsapp.model.signal.auth.UserAgent; import it.auties.whatsapp.model.signal.auth.UserAgent.PlatformType; import it.auties.whatsapp.model.signal.auth.Version; -import it.auties.whatsapp.util.Spec.Whatsapp; +import it.auties.whatsapp.util.Specification.Whatsapp; import net.dongliu.apk.parser.ByteArrayApkFile; import net.dongliu.apk.parser.bean.ApkSigner; import net.dongliu.apk.parser.bean.CertificateMeta; diff --git a/src/main/java/it/auties/whatsapp/util/RegistrationHelper.java b/src/main/java/it/auties/whatsapp/util/RegistrationHelper.java index 02991f3f4..dfd74a49a 100644 --- a/src/main/java/it/auties/whatsapp/util/RegistrationHelper.java +++ b/src/main/java/it/auties/whatsapp/util/RegistrationHelper.java @@ -13,7 +13,7 @@ import it.auties.whatsapp.model.mobile.VerificationCodeStatus; import it.auties.whatsapp.model.node.Attributes; import it.auties.whatsapp.model.signal.keypair.SignalKeyPair; -import it.auties.whatsapp.util.Spec.Whatsapp; +import it.auties.whatsapp.util.Specification.Whatsapp; import java.io.UncheckedIOException; import java.net.HttpURLConnection; diff --git a/src/main/java/it/auties/whatsapp/util/Spec.java b/src/main/java/it/auties/whatsapp/util/Specification.java similarity index 77% rename from src/main/java/it/auties/whatsapp/util/Spec.java rename to src/main/java/it/auties/whatsapp/util/Specification.java index 9204e88cf..fd5ac1d69 100644 --- a/src/main/java/it/auties/whatsapp/util/Spec.java +++ b/src/main/java/it/auties/whatsapp/util/Specification.java @@ -9,24 +9,23 @@ import java.util.Base64; import java.util.HexFormat; -public class Spec { +public class Specification { public final static class Whatsapp { public static final String DEFAULT_NAME = "Cobalt"; - public static final byte[] PROTOCOL = "Noise_XX_25519_AESGCM_SHA256\0\0\0\0".getBytes(StandardCharsets.UTF_8); + public static final byte[] NOISE_PROTOCOL = "Noise_XX_25519_AESGCM_SHA256\0\0\0\0".getBytes(StandardCharsets.UTF_8); public static final String WEB_ORIGIN = "https://web.whatsapp.com"; public static final String WEB_HOST = "web.whatsapp.com"; - public static final URI WEB_ENDPOINT = URI.create("wss://web.whatsapp.com/ws/chat"); - public static final String APP_ENDPOINT_HOST = "g.whatsapp.net"; - public static final int APP_ENDPOINT_PORT = 443; + public static final URI WEB_SOCKET_ENDPOINT = URI.create("wss://web.whatsapp.com/ws/chat"); + public static final String SOCKET_ENDPOINT = "g.whatsapp.net"; + public static final int SOCKET_PORT = 443; public static final String WEB_UPDATE_URL = "https://web.whatsapp.com/check-update?version=2.2245.9&platform=web"; public static final String MOBILE_REGISTRATION_ENDPOINT = "https://v.whatsapp.net/v2"; - public static final String IOS_UPDATE_URL = "https://www.whatsapp.com/download?lang=en"; public static final Version DEFAULT_MOBILE_IOS_VERSION = Version.of("2.23.12.75"); - private static final byte[] WHATSAPP_HEADER = "WA".getBytes(StandardCharsets.UTF_8); + private static final byte[] WHATSAPP_VERSION_HEADER = "WA".getBytes(StandardCharsets.UTF_8); private static final byte[] WEB_VERSION = new byte[]{6, BinaryTokens.DICTIONARY_VERSION}; - public static final byte[] WEB_PROLOGUE = BytesHelper.concat(WHATSAPP_HEADER, WEB_VERSION); + public static final byte[] WEB_PROLOGUE = BytesHelper.concat(WHATSAPP_VERSION_HEADER, WEB_VERSION); private static final byte[] MOBILE_VERSION = new byte[]{5, BinaryTokens.DICTIONARY_VERSION}; - public static final byte[] APP_PROLOGUE = BytesHelper.concat(WHATSAPP_HEADER, MOBILE_VERSION); + public static final byte[] MOBILE_PROLOGUE = BytesHelper.concat(WHATSAPP_VERSION_HEADER, MOBILE_VERSION); public static final byte[] ACCOUNT_SIGNATURE_HEADER = {6, 0}; public static final byte[] DEVICE_WEB_SIGNATURE_HEADER = {6, 1}; public static final byte[] DEVICE_MOBILE_SIGNATURE_HEADER = {6, 2}; @@ -36,7 +35,6 @@ public final static class Whatsapp { public static final String MOBILE_DOWNLOAD_URL = "https://www.whatsapp.com/android/current/WhatsApp.apk"; public static final String MOBILE_BUSINESS_DOWNLOAD_URL = "https://d.apkpure.com/b/APK/com.whatsapp.w4b?version=latest"; public static final byte[] MOBILE_ANDROID_SALT = Base64.getDecoder().decode("PkTwKSZqUfAUyR0rPQ8hYJ0wNsQQ3dW1+3SCnyTXIfEAxxS75FwkDf47wNv/c8pP3p0GXKR6OOQmhyERwx74fw1RYSU10I4r1gyBVDbRJ40pidjM41G1I1oN"); - public static final byte[] MOBILE_SALT = Base64.getDecoder().decode("PkTwKSZqUfAUyR0rPQ8hYJ0wNsQQ3dW1+3SCnyTXIfEAxxS75FwkDf47wNv/c8pP3p0GXKR6OOQmhyERwx74fw1RYSU10I4r1gyBVDbRJ40pidjM41G1I1oN"); public static final byte[] REGISTRATION_PUBLIC_KEY = HexFormat.of().parseHex("8e8c0f74c3ebc5d7a6865c6c3c843856b06121cce8ea774d22fb6f122512302d"); public static final String MOBILE_IOS_STATIC = "0a1mLfGUIBVrMKF1RdvLI5lkRBvof6vn0fD2QRSM"; public static final int COMPANION_PAIRING_TIMEOUT = 10; diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index e992da607..2a3d8640e 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,6 +1,7 @@ open module it.auties.cobalt { requires static jilt; requires transitive java.desktop; + requires jakarta.websocket.client; requires java.net.http; requires com.fasterxml.jackson.annotation; requires com.fasterxml.jackson.databind; diff --git a/src/test/java/it/auties/whatsapp/Test.java b/src/test/java/it/auties/whatsapp/Test.java index e2fa9b287..aab2827ef 100644 --- a/src/test/java/it/auties/whatsapp/Test.java +++ b/src/test/java/it/auties/whatsapp/Test.java @@ -1,15 +1,10 @@ package it.auties.whatsapp; -import it.auties.whatsapp.util.ConcurrentDoublyLinkedList; - -import java.util.Objects; +import it.auties.whatsapp.model.message.model.MessageContainerSpec; +import it.auties.whatsapp.util.Json; public class Test { public static void main(String[] args) { - var data = new ConcurrentDoublyLinkedList<>(); - data.add("abc"); - System.out.println(Objects.hashCode(data)); - data.add("def"); - System.out.println(Objects.hashCode(data)); + System.out.println(Json.writeValueAsString(MessageContainerSpec.decode(new byte[]{26, -87, 44, 18, 10, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 34, 32, -75, -27, -91, -94, -73, -127, 109, 3, -85, -47, -68, 80, -93, -72, 95, 2, 58, 44, 4, -4, 0, -101, 76, -97, 61, -17, -68, 71, -121, 50, -46, 31, 40, -36, -52, 2, 48, -32, 3, 56, -31, 3, 90, -15, 1, 47, 109, 49, 47, 118, 47, 116, 50, 52, 47, 65, 110, 56, 68, 114, 95, 81, 115, 76, 121, 109, 66, 74, 78, 82, 122, 116, 122, 87, 104, 104, 81, 45, 83, 78, 65, 101, 104, 121, 88, 120, 68, 85, 117, 53, 72, 108, 53, 117, 67, 79, 119, 90, 86, 111, 78, 81, 122, 55, 70, 120, 112, 69, 86, 81, 120, 51, 85, 49, 70, 80, 107, 113, 105, 115, 109, 113, 95, 87, 103, 74, 90, 105, 122, 72, 48, 52, 101, 122, 67, 106, 87, 114, 108, 70, 50, 54, 74, 67, 116, 101, 118, 71, 101, 98, 53, 85, 53, 49, 98, 55, 66, 109, 106, 53, 65, 102, 82, 72, 106, 86, 76, 117, 122, 86, 45, 100, 103, 49, 95, 73, 84, 77, 98, 79, 74, 95, 113, 102, 65, 63, 115, 116, 112, 61, 100, 115, 116, 45, 101, 110, 99, 38, 99, 99, 98, 61, 49, 48, 45, 53, 38, 111, 104, 61, 48, 49, 95, 65, 100, 83, 109, 78, 74, 89, 120, 55, 69, 90, 108, 113, 110, 88, 110, 88, 73, 105, 84, 102, 121, 90, 118, 70, 48, 75, 102, 70, 83, 110, 66, 88, 122, 111, 107, 110, 89, 108, 76, 116, 109, 106, 107, 102, 65, 38, 111, 101, 61, 54, 53, 52, 53, 51, 55, 51, 56, 38, 95, 110, 99, 95, 115, 105, 100, 61, 48, 48, 48, 48, 48, 48, -126, 1, -10, 41, -1, -40, -1, -32, 0, 16, 74, 70, 73, 70, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, -1, -30, 1, -40, 73, 67, 67, 95, 80, 82, 79, 70, 73, 76, 69, 0, 1, 1, 0, 0, 1, -56, 0, 0, 0, 0, 4, 48, 0, 0, 109, 110, 116, 114, 82, 71, 66, 32, 88, 89, 90, 32, 7, -32, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 97, 99, 115, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, -10, -42, 0, 1, 0, 0, 0, 0, -45, 45, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 100, 101, 115, 99, 0, 0, 0, -16, 0, 0, 0, 36, 114, 88, 89, 90, 0, 0, 1, 20, 0, 0, 0, 20, 103, 88, 89, 90, 0, 0, 1, 40, 0, 0, 0, 20, 98, 88, 89, 90, 0, 0, 1, 60, 0, 0, 0, 20, 119, 116, 112, 116, 0, 0, 1, 80, 0, 0, 0, 20, 114, 84, 82, 67, 0, 0, 1, 100, 0, 0, 0, 40, 103, 84, 82, 67, 0, 0, 1, 100, 0, 0, 0, 40, 98, 84, 82, 67, 0, 0, 1, 100, 0, 0, 0, 40, 99, 112, 114, 116, 0, 0, 1, -116, 0, 0, 0, 60, 109, 108, 117, 99, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 101, 110, 85, 83, 0, 0, 0, 8, 0, 0, 0, 28, 0, 115, 0, 82, 0, 71, 0, 66, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 111, -94, 0, 0, 56, -11, 0, 0, 3, -112, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 98, -103, 0, 0, -73, -123, 0, 0, 24, -38, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, 36, -96, 0, 0, 15, -124, 0, 0, -74, -49, 88, 89, 90, 32, 0, 0, 0, 0, 0, 0, -10, -42, 0, 1, 0, 0, 0, 0, -45, 45, 112, 97, 114, 97, 0, 0, 0, 0, 0, 4, 0, 0, 0, 2, 102, 102, 0, 0, -14, -89, 0, 0, 13, 89, 0, 0, 19, -48, 0, 0, 10, 91, 0, 0, 0, 0, 0, 0, 0, 0, 109, 108, 117, 99, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 12, 101, 110, 85, 83, 0, 0, 0, 32, 0, 0, 0, 28, 0, 71, 0, 111, 0, 111, 0, 103, 0, 108, 0, 101, 0, 32, 0, 73, 0, 110, 0, 99, 0, 46, 0, 32, 0, 50, 0, 48, 0, 49, 0, 54, -1, -37, 0, 67, 0, 3, 2, 2, 2, 2, 2, 3, 2, 2, 2, 3, 3, 3, 3, 4, 6, 4, 4, 4, 4, 4, 8, 6, 6, 5, 6, 9, 8, 10, 10, 9, 8, 9, 9, 10, 12, 15, 12, 10, 11, 14, 11, 9, 9, 13, 17, 13, 14, 15, 16, 16, 17, 16, 10, 12, 18, 19, 18, 16, 19, 15, 16, 16, 16, -1, -37, 0, 67, 1, 3, 3, 3, 4, 3, 4, 8, 4, 4, 8, 16, 11, 9, 11, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, -1, -64, 0, 17, 8, 0, 99, 0, 100, 3, 1, 34, 0, 2, 17, 1, 3, 17, 1, -1, -60, 0, 29, 0, 0, 1, 4, 3, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 5, 7, 8, 2, 3, 4, 9, 1, -1, -60, 0, 58, 16, 0, 2, 1, 3, 3, 3, 2, 4, 4, 4, 4, 6, 3, 0, 0, 0, 1, 2, 3, 4, 5, 17, 0, 18, 33, 6, 7, 49, 19, 65, 20, 34, 81, 97, 8, 35, 50, 113, 21, 66, -127, -79, 67, -111, -95, -63, 22, 51, 98, -47, -16, -15, 82, -110, -31, -1, -60, 0, 26, 1, 0, 2, 3, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 5, 2, 4, 6, 1, 0, -1, -60, 0, 48, 17, 0, 1, 4, 1, 1, 6, 5, 2, 6, 3, 0, 0, 0, 0, 0, 0, 1, 0, 2, 3, 17, 4, 33, 5, 18, 19, 49, 65, -127, 50, 81, 97, -111, -79, 34, 35, 20, 21, 66, 82, -63, -15, -95, -47, -31, -1, -38, 0, 12, 3, 1, 0, 2, 17, 3, 17, 0, 63, 0, -12, 75, -89, -90, -8, 122, 47, -122, -112, -126, 84, 1, -100, 123, -21, -90, 74, -36, 28, 70, -32, 107, -112, -62, -48, 47, -90, -119, -76, 30, 127, 113, -82, 11, -91, 101, -66, -37, 16, -98, -78, -83, 98, 36, -116, 2, -36, -109, -12, 31, 93, 37, 7, 117, -76, -104, 50, 39, 60, -48, 22, -100, 42, 107, 37, -63, -36, -4, -23, -106, -21, 126, -95, -75, -45, -75, 69, -62, -79, 33, 69, 25, 37, -37, 26, -120, -69, -103, -33, -54, -85, 13, 66, -39, -6, 103, -90, 46, -107, -75, -77, -113, -55, 45, 74, -24, -115, -19, -112, -52, 49, -96, 75, 103, 108, -69, -75, -35, -103, -65, -120, -9, 2, -25, 37, -66, -123, -50, -27, -93, -128, -111, -57, -36, -7, 58, 1, -98, -51, 51, 84, -23, -101, 37, -80, 48, 75, -101, 32, 96, 60, -121, 55, 30, -35, 59, -46, 44, -21, -97, -60, -27, -78, -110, 86, -78, 116, 93, 52, -73, -117, -117, 29, -86, -80, 2, -54, 15, -36, -8, 26, 19, -78, 118, -13, -70, -35, -44, -84, 55, 30, -67, -69, 61, -74, -35, 33, -36, -76, -112, -71, 4, -113, -95, -6, -22, 100, -24, -2, -47, 116, -97, 70, -45, 71, 77, 110, -76, -59, -22, 46, 51, 35, -88, 44, 79, -41, 71, 9, 72, 99, -116, 29, -69, 85, 7, -80, -58, 53, -34, 25, 113, -73, -107, -25, -19, 86, -29, -114, 22, 3, 55, 61, 78, -82, 61, -6, 118, -9, 65, -99, 23, -38, 62, -110, -24, -92, 87, -96, -73, 68, 100, 30, 100, 112, 9, 63, 125, 5, -9, -17, -71, 116, -16, 83, 30, -128, -74, -43, 50, -67, 92, 121, -72, 52, 14, 1, 16, -112, 64, -117, 35, -64, 110, 119, 120, 59, 70, 60, 54, -114, -70, -93, -84, -93, -94, 21, 54, -102, 25, 25, -86, 18, 50, 100, 120, -64, 62, -118, -5, -29, 56, 5, -79, -100, 12, -22, -93, 117, 21, -9, -8, -35, -34, -29, 94, -105, -120, 36, -81, -86, -109, 44, -49, 62, -33, 73, 7, -23, 64, -52, 65, 1, 84, 5, 28, -106, -32, 31, 109, 80, -55, -53, 105, -5, 49, 31, 79, -23, 15, 27, 29, -18, 119, 27, 34, -55, -25, -1, 0, 74, -77, -33, -121, -85, -11, 124, -3, 2, -108, 21, 14, 74, -38, 107, 101, -95, -127, -73, -18, 102, -124, 4, 116, -55, -5, 122, -104, 3, -64, 85, 94, 6, -92, -22, -51, -13, 5, 101, 124, -18, -43, 123, -4, 37, -34, -123, 68, 61, 73, -45, 13, 83, 28, -111, -45, 77, 13, 109, 56, 70, 44, 8, 117, 41, 33, 7, 39, 3, 49, -96, -28, -7, 39, -17, -87, 107, -83, -5, -107, -46, 29, 17, 72, -43, 55, -117, -84, 16, 122, 35, 36, 51, -128, 120, -43, -52, 87, 125, -127, 103, -110, -81, 62, 36, -45, -27, 24, -96, 105, 36, -7, 122, -89, -39, 20, 67, -55, 76, -3, 79, -45, 65, -3, 101, -35, -98, -112, -24, 24, 36, -86, -68, 92, -31, -120, -88, 63, 46, -15, -97, -37, 80, 79, 82, 126, 33, 59, -127, -36, -86, -26, -79, 118, -106, -61, 59, -63, 41, -40, 107, -27, 66, -79, -113, 108, -113, 115, -3, -76, -21, -47, 31, -123, 10, -53, -43, -63, 58, -117, -71, -41, -103, -18, -43, -92, -17, -12, -28, 111, -53, 66, 125, -126, -8, -44, -72, -82, 126, -111, 14, -23, -96, -39, 120, -101, 60, 111, 109, 41, 45, -33, -79, -70, -98, -25, -112, -7, -12, 92, 117, -1, 0, -116, 11, -3, -62, -87, -25, -23, 94, -119, -82, -83, -73, -125, -75, 38, 17, -128, 24, -113, 56, -49, -73, -115, 45, 89, 27, 103, 105, 122, 102, -41, 69, 29, 29, 29, -94, 20, -115, 7, 0, 32, -46, -41, 119, 38, -3, -22, 31, -100, 97, 13, 27, -118, -54, -11, -69, -7, 81, 15, 95, -2, 45, -83, 48, -36, 31, -89, 122, 2, -122, 91, -11, -41, 59, 2, -45, 13, -56, -121, -2, -90, -16, 52, 43, 103, -19, -9, 124, 59, -93, 119, -90, -22, 30, -71, -67, -67, -78, -122, -98, 100, -98, 42, 42, 102, 41, -56, 57, 27, -101, -55, -1, 0, -49, 58, -100, 58, 19, -16, -39, -47, 29, -70, -91, -114, 59, 61, 60, 66, 101, 31, 52, -89, -106, 39, -36, -25, 69, -23, 100, -120, 63, -96, -73, 7, 85, 83, -55, -53, 107, -90, 7, 60, 125, -49, 101, 9, 54, -60, 120, -19, -31, 108, -10, 6, 15, 51, -85, -113, 126, -99, -67, -41, -103, -76, -11, 23, -50, -102, -67, 84, -45, 85, -44, 87, 82, 93, -88, -86, -92, 73, -25, 21, 78, -107, 9, 50, -71, 86, 37, -55, -35, -69, 118, 119, 49, 57, -50, 114, 121, -50, -89, 46, -37, -2, 43, -6, -106, -59, 77, 75, 111, -22, -75, 75, -83, -72, -108, -120, 86, 51, 41, -88, -121, -37, 45, -56, 19, 120, -5, 30, 73, 36, -29, 5, -101, -15, -55, -37, -108, -23, 46, -89, -76, -11, -75, -119, 24, 82, -11, 36, -90, -110, -32, -55, 8, -38, -75, 106, 1, 83, -72, -99, -69, -28, 80, 72, 82, -66, 98, 102, -50, 114, 69, 110, -93, -87, -110, -5, 73, 53, 13, 11, -71, -119, 34, 102, -87, -85, 50, 9, 21, 51, -6, -117, 59, -2, -87, 15, -71, -25, 31, 124, -28, 40, 17, 63, 27, 120, -121, 106, 62, 21, -57, -55, 22, 92, 108, 115, -101, -95, -7, 94, -121, 84, 126, 33, -83, -14, 80, -46, 85, -46, -46, -63, 51, 86, 70, 30, 63, 74, 70, 108, 12, 18, 75, 0, -89, 24, -57, -116, -5, -125, -32, -116, -57, -99, 75, -33, -18, -96, -82, 8, 45, -77, -71, -89, -112, -128, 73, 66, -120, -82, 78, 48, 79, -98, 8, -2, -68, 28, -32, -15, 89, 44, -73, 100, -85, 51, -37, 97, 89, -128, 72, -61, -57, 33, -107, -107, -111, 87, 105, 56, -38, -64, -78, -80, -31, -105, 5, -58, -20, -126, 50, 85, -33, 104, 58, -126, -37, 69, -46, 115, -61, 56, 85, -127, 36, 102, -73, -42, -47, 75, -22, 41, -84, 13, -109, 27, 96, -106, 18, 72, 10, -108, -28, 46, -24, -58, 48, 64, -36, -106, 92, -36, -84, -126, 70, -10, -118, -36, 88, 56, -8, -32, 29, -35, 84, -101, 126, -22, 30, -90, -82, 118, -83, -118, -117, -45, -91, -106, 61, -16, 84, -91, 70, 89, 114, -72, 11, -61, -100, -7, -6, 103, 7, -125, -96, 25, -83, 20, -9, 24, 96, -82, -82, -76, 49, 53, -110, 21, 43, 39, 50, 17, -126, 3, 28, -28, 17, -50, -31, -111, -125, -72, 126, -38, -23, -23, -82, -79, -93, -21, 89, 58, 122, -110, -82, -22, 103, -94, 90, -54, -22, 106, -8, 94, 80, 38, -35, 36, 69, -111, -36, 58, -16, 81, -40, -27, 60, -122, 80, 118, -128, 27, 14, -3, 48, -47, -37, -37, -88, 104, 122, -90, -13, 76, 99, -77, -44, 71, 63, -58, -51, 58, -57, 31, -64, 73, -22, -108, 101, 98, 126, 95, 10, -83, -114, 73, 92, 121, -50, -86, -73, 29, -27, -63, -51, 58, -1, 0, 53, 104, -50, -107, -84, 105, 107, -121, 47, -117, -92, -15, -38, 99, 95, -45, -13, 77, 103, -24, -58, 22, -55, 46, 20, 50, 82, -59, 85, 56, 44, -79, -54, 89, 9, 115, -76, -115, -7, -40, 61, -72, -64, 28, -7, -44, -125, 96, -4, 39, 87, -34, 111, 9, -44, 125, -51, -22, 122, -117, -13, -17, -35, -23, 57, -4, -91, -3, -105, -12, -29, 64, 52, 125, -54, -19, -30, -42, -117, -113, 72, 94, 41, -70, -118, 0, 61, 57, 18, -36, -94, 103, 37, 49, -100, -116, -28, 1, -14, -27, -72, 30, 124, 112, 53, 100, 59, 17, -36, 107, 127, 86, -58, -42, 39, -84, 33, -30, -113, -44, -12, -86, 1, 89, -30, -1, 0, -95, -58, 6, 15, -97, 111, 99, -5, -105, -69, 50, 99, -60, -32, 78, 108, -12, 75, -14, -14, 114, 96, -57, 63, -122, 37, -83, 60, -4, -49, 126, 127, -62, 125, -89, -117, -96, -5, 121, 69, 29, 37, 61, 12, 20, 113, -96, -38, 8, 64, -65, -21, -89, -102, 62, -82, -23, -55, -102, 35, 79, 95, 30, 102, 25, 81, -111, -94, -101, -105, 73, -12, -11, -6, 15, -122, -83, -91, -114, 96, 7, -45, -50, -101, -32, -19, -17, 73, 90, 29, 42, 26, -123, 1, -113, -12, 12, 120, -42, -114, -98, 14, -107, 75, 34, 75, -119, -78, 86, 38, -7, 1, -1, 0, 21, -65, -6, -99, 45, 59, 9, 108, 107, -62, 82, -95, 3, 75, 82, -41, -51, 74, -126, -32, -102, -76, 67, 72, 26, 119, -37, 18, -114, 93, -113, 26, 18, -85, -22, -40, 119, -54, -76, -61, -14, -41, -125, 33, 30, -1, 0, 65, -11, -45, 39, 112, 122, -71, 41, 97, -98, 58, 70, 14, -112, 127, -122, 78, -36, -81, -41, 81, -1, 0, 75, 95, -18, -105, -28, -110, -66, -94, 1, 67, 74, -110, -6, 116, -28, -126, 90, 86, -3, -67, -121, -11, -46, 108, -99, -90, -23, 37, -32, -58, 83, -68, 109, -102, -42, -57, -59, -111, 16, -9, 35, -90, 41, -6, -17, -73, -3, 87, 109, -68, 62, -6, 42, -69, 124, -103, -114, 83, -6, 100, 82, 30, 55, 31, 77, -82, -86, -64, -126, 8, 32, 17, -126, 50, 60, -7, -113, -90, 111, 54, 123, -123, 101, -90, -94, 20, 88, 103, -100, 84, -56, -55, -112, -69, -128, 25, 57, 60, 12, -112, 120, -50, 57, -13, -17, -81, 71, 47, 85, 5, 58, 126, -74, -42, -11, 113, 75, 44, -108, -90, 73, 98, 31, -51, -58, 54, -29, 60, 12, 127, 125, 80, -34, -32, -11, 52, 116, -46, -35, -21, -51, 36, 114, -51, 43, 122, 48, 66, -64, -18, -7, -113, 3, -127, -62, -127, -113, 111, 30, 116, -69, 52, 22, 86, -23, -25, -49, -43, 49, -61, 112, 113, 45, 61, 57, 45, -106, -6, 122, -22, 90, 10, 90, -92, -102, -33, 73, 4, 37, 12, -83, 87, 10, -84, 75, -122, 36, -79, 35, 105, 4, 16, 88, 56, -25, 32, 114, 50, 116, -51, -37, -7, 45, 52, 116, -3, 71, -45, -99, 47, 119, -77, 93, -19, -41, 52, -122, -74, 122, 107, 125, 84, 83, 26, 106, -116, 114, -121, -109, 34, -31, -7, 86, 56, -28, 19, -116, -116, -24, 62, -15, -46, -9, 59, -3, -92, -59, 95, -43, 48, 84, 77, 69, 32, -105, -8, 68, -77, -76, 52, -45, -94, -115, -2, -114, 89, -74, -77, 103, 105, 82, -25, 110, -31, -29, 4, -24, 52, 116, -49, 117, -28, -85, -74, 93, 122, 39, -92, 110, -74, -122, -77, 87, -75, 104, -71, 75, 12, 84, -122, 20, -61, 23, -118, 49, -69, -13, 3, 110, 57, 80, 10, -110, 70, 114, 14, 116, 92, 72, 34, 115, 104, -70, -119, -1, 0, 10, 57, 83, 74, -41, 83, 91, 97, 79, 118, 123, 44, -13, 87, -44, -43, -45, 69, 42, 69, 49, 40, -110, 74, -1, 0, -14, 38, 42, 21, -78, -4, 23, 44, -86, -53, -13, 100, -79, 85, 62, 87, -104, -1, 0, -65, 21, -113, 100, -65, 88, 44, 125, 89, 75, 57, -89, -69, -37, -106, -90, 10, -31, 21, 68, -95, -82, 81, 85, -82, -40, -28, -118, 38, 6, 76, 36, 76, -95, 24, 17, -118, -119, 48, -95, -74, -78, -55, -35, 23, -41, -10, 59, -32, -118, -90, 90, 42, 122, 123, -116, -27, 42, 106, 68, 76, 51, 20, -22, -53, -22, 32, 39, -54, -109, -69, -40, 103, 3, -10, -45, -89, 86, -43, 60, -99, 107, 97, 123, 108, -16, 108, -118, 87, -12, 29, 91, 62, -116, -110, 40, 12, -118, -33, -89, 33, 0, -7, -63, 25, -31, 115, -63, 2, -85, 101, 110, 51, -116, -100, -6, 105, -45, -43, 21, -16, 63, 34, -104, -19, 58, -5, 33, -50, -126, -65, 79, -37, 126, -109, -66, 92, -81, 86, -7, -83, -105, 78, -90, -71, 83, -55, 108, -73, 85, 46, -55, -93, -90, -118, 5, -115, -90, -110, 16, 127, 40, -56, -32, 97, 28, 43, 16, 65, -37, -18, 39, 63, -61, -11, 93, 101, -61, -82, -19, 125, 71, 80, -21, 11, -44, 68, -79, -56, -22, -28, -103, 50, -91, 8, 124, 12, 22, 24, 7, -113, 26, -121, 47, -35, -68, -65, 92, 122, -126, 26, -97, -116, -102, -29, 13, 92, -78, 122, -26, 89, -46, 69, 118, -113, -106, 66, 23, -121, -38, -55, -110, 8, 7, 57, 30, 70, 117, 35, -2, 29, 45, -107, 87, 14, -28, -47, 8, 46, 82, 77, 5, 26, -76, -83, 10, 71, -124, 85, -12, -7, -25, 3, 102, -42, -38, -101, 2, -32, -112, 72, 56, 0, -79, 49, 90, -7, -25, 108, -124, 104, 8, 30, -54, -74, 99, -94, -122, 46, 19, 14, -75, -81, -6, 87, 111, -2, 41, 104, -25, 88, 41, -80, 118, -32, 103, 91, -22, 110, -75, -43, 72, -83, 34, 14, 124, 115, -95, -119, -19, -22, -82, -114, -103, 31, 54, -19, -64, -100, -2, -38, -34, -43, -46, -61, 8, 83, 55, 11, -29, -97, 26, -43, 21, -101, 3, -55, 56, 52, -20, -60, -29, -5, -23, 105, -84, -36, -40, 28, 25, 70, 127, 108, -23, 106, 43, -37, -91, 67, -99, -38, -85, -118, -37, 106, -83, 63, 25, 26, 78, 84, -6, -116, -72, -54, -125, -5, -7, -3, -75, -113, 111, -126, -75, -74, -126, -86, -94, -31, 35, 82, -63, 6, -10, 14, 2, -18, 45, -49, -111, -12, 0, -1, 0, -98, -123, -70, -114, -13, -45, 23, 27, 68, -45, 117, 45, 90, 50, -43, 74, 75, -122, 99, -75, -122, 120, 39, -19, -10, -47, 80, 123, 101, 21, -78, -35, 6, -32, -44, -116, -116, 85, 81, 74, 2, -95, 56, -6, 96, 96, 1, -84, -100, 0, 58, 82, -15, -28, -75, 111, 117, 69, -70, 83, 119, 86, -9, 10, -34, -67, 75, 78, -42, -116, 77, 2, -54, -90, -74, 69, 118, 42, 23, -7, -108, -115, -69, -113, 28, 113, -17, -86, -59, -36, 78, -109, -71, -57, -43, -9, 23, -110, -90, -100, -67, 48, 50, 81, -62, -54, -71, 116, -31, -111, -78, 70, 71, -73, -33, 71, -3, -21, -21, -118, 126, -108, -100, 82, 90, -31, 122, 84, -87, 82, -62, 70, 114, 20, 19, -128, 54, -81, -114, 62, -40, -15, -96, 57, 46, 54, -66, -29, -38, 41, 104, -27, -103, -115, -30, -124, 17, 21, 65, -50, 101, -116, -110, -58, 62, 79, 59, 64, -6, -98, 3, 125, -75, 125, -51, 19, 10, -26, 66, -90, -62, -24, 72, 119, 32, 80, 117, -33, -89, 41, 110, -92, 111, -72, -111, 83, -123, 32, -76, 10, 6, 71, -8, 98, 69, -15, -113, 24, 33, -80, 15, -41, -58, 22, 110, -126, -67, -57, 112, -122, 122, -40, -124, 116, -24, -20, 97, -11, 99, 3, 36, -28, -128, 64, 36, 123, -98, 114, 114, 49, -100, 19, -128, -11, 5, -126, -118, -47, 24, 70, -89, -115, 106, 28, -28, -54, 55, 50, 35, 21, 63, -83, -79, -97, 124, -16, 73, -54, 50, -29, -112, 116, 123, -45, -9, 58, -119, -24, 41, -23, -29, 74, -76, -86, -111, -35, -94, 72, -111, 34, -98, 24, 100, -57, -52, 29, -9, -104, -61, 101, 64, -56, -3, 32, 55, -13, 1, -87, -77, 1, -109, 52, -39, -94, -68, -20, -7, 32, 52, 53, 10, 55, -97, -96, 36, -110, 72, -89, -23, 38, -118, 11, -75, 54, 101, -114, 106, -62, -23, 21, 64, 44, 118, -86, -56, -69, -127, 12, 88, 124, -33, 49, -50, -31, -116, -18, -37, -93, -96, 58, 102, -2, 59, -111, 53, -5, -82, 107, 38, -95, 107, 37, 11, -57, 79, 70, -75, -117, 45, 60, -13, -49, -69, 107, -126, -118, 3, 70, 63, 49, -117, -107, 4, 59, 68, 91, 44, -95, 116, 125, 61, -38, -17, 78, -18, 104, -31, -122, -86, -94, -80, 101, 102, 89, -125, 48, 92, 16, 1, -112, -79, 124, -28, 46, 75, 31, -101, 4, 109, 24, -58, -102, 107, 44, 116, -15, -56, 47, 87, -37, -68, -122, 87, -7, 76, 49, 84, 48, -116, 38, 89, -120, 5, 113, -69, 7, 7, -100, -28, -13, -25, 26, 36, 88, 98, 38, -112, 84, -92, -38, 14, -112, 87, -10, -92, 43, 47, 85, 83, -37, 109, -41, 42, 40, 110, 37, 98, -88, -89, -110, -94, -81, -46, -109, -47, 105, 10, 66, 127, -101, -45, -109, 123, 51, 21, -115, 99, 121, 120, 96, -128, 21, 3, -103, -81, -79, 93, 59, 63, 75, -12, -5, -33, -21, 39, -106, -90, -21, 119, 27, -106, -82, -86, 71, 121, -66, 31, 63, -106, 27, 115, -80, -55, 82, 9, 0, -16, 120, -25, 25, -43, 68, -73, 95, -32, -76, 73, 21, 61, 34, -56, -80, -88, -33, -103, 100, -36, 73, -16, 119, 103, -11, 3, -18, 8, -25, -97, 110, 53, 113, -5, 59, 114, -98, -81, -73, -44, -107, 91, 23, 107, 51, -76, 81, 49, 45, -23, -89, -78, -116, -14, 64, -25, 25, -55, -5, -97, 36, -104, 50, 48, -53, -61, 29, 18, -20, -90, 59, 119, -120, 122, -93, -79, 124, -87, -123, -116, 75, 35, 41, 99, -13, 18, 78, 7, -12, -42, 54, -6, -120, -94, -86, 105, -85, 100, 121, -108, -74, 112, -57, 25, -41, 28, -76, 126, -80, -118, 78, 88, -80, -55, 95, 24, -1, 0, -50, 78, -98, -41, -89, -43, -87, -43, 89, -48, -126, -96, -25, 60, -125, -90, -124, -38, -93, 84, -75, -51, 123, -75, -68, -116, 66, -6, 99, 56, 11, -113, 26, 90, -51, -32, -126, 6, 49, 71, 72, -124, 15, 39, 25, -55, -46, -44, 105, 74, -107, 86, -90, -19, 103, -15, 126, -31, 65, 55, 80, 95, 42, 36, -73, -59, 48, -110, 10, 56, -27, 43, 27, -56, 27, -63, 10, 121, 81, -114, 65, -32, -5, -24, -69, -88, 106, -28, -71, -36, -29, -115, -44, 124, 12, 115, 72, 17, 64, 5, 91, 0, -88, -14, 60, 103, -5, 123, 107, 42, 115, 95, 110, -66, -61, 30, 4, -11, 71, -44, -90, 4, 100, -111, 52, -103, 28, 103, -114, 6, -101, 42, 125, 122, 106, -58, -114, -94, 102, 48, -63, 30, 55, 43, 54, 94, 99, -110, 125, -80, 0, 28, -25, 35, 89, 104, -94, 12, 20, 60, -42, -114, 89, 75, -64, 36, -12, 80, 87, 121, -103, 111, -26, 72, -18, 51, -58, 62, 13, -106, 55, 83, 33, 105, 74, -125, -126, 48, 72, 32, -29, -100, 112, 62, -29, 81, 61, -74, -23, 85, 96, -72, -61, 37, 50, -49, 75, 2, -84, 72, -24, -78, 21, -106, 69, 117, -53, 124, -5, 112, 20, -128, 72, -56, 35, -25, 94, 24, 101, -76, 101, -35, -38, -23, 106, 110, -90, -19, 111, 37, 61, 25, 0, -108, 6, -36, -52, 20, -8, 39, -64, -6, -25, -49, 58, -116, -23, -81, -76, -7, 101, -86, 77, -80, -45, -87, 112, 74, -27, 75, 96, -107, 31, 50, -107, 56, -13, -75, -80, 24, 41, 25, -55, 3, 83, -115, -18, 105, -80, -90, 99, 14, 101, 86, -118, 102, -96, -70, -43, -42, -37, 126, 41, -90, -122, 106, 111, 76, -123, -115, 36, 98, -37, -101, -64, -35, -126, 55, 96, 41, 57, -14, -93, 110, 114, 8, 31, 90, -16, -48, 69, 34, 71, 102, 119, 121, -92, 95, 84, -102, -80, -54, -18, -84, -1, 0, -104, -96, 12, 21, -38, -36, 121, 57, 36, -122, 0, -19, -48, 55, 73, -35, -91, 69, -89, -84, -89, -116, 4, 68, 102, 77, -78, 42, 72, -118, 54, -94, 18, 71, -110, 14, -45, -123, 25, 35, 36, -15, -13, 3, -118, 59, -68, 119, 6, 119, -111, 94, 64, 0, 72, 93, -7, 44, -65, -53, -17, -57, 7, -3, 52, -51, -77, -79, -64, 30, 69, 43, 124, 46, 105, 32, 46, 90, -6, 27, -3, -42, 121, 34, 74, 56, -88, -121, -86, 78, 33, 63, -53, -100, -19, 3, 3, 0, 28, -116, 15, 111, -12, -39, 79, -47, 17, 34, 122, -43, 46, -51, -23, -27, -79, 35, 18, -96, -3, 116, 90, 46, -112, -61, 74, 106, 89, 119, 62, -45, -67, -39, 118, -110, -7, 63, 55, -33, -97, 127, -21, -17, -90, 11, -121, 87, -63, -80, -57, 49, 85, -33, -116, -96, 25, 39, -33, 24, -3, -11, -39, -72, 97, -74, 74, 12, 108, -112, -70, -128, 66, -105, 26, 89, 63, -117, -60, -47, -62, -90, 53, 113, 25, 24, -50, 7, -3, -75, 119, -69, 101, 83, 81, 102, -23, 43, 101, 19, -47, -107, -52, 33, -40, -29, 3, -98, 117, 85, -5, 111, -46, -115, -43, -67, 105, 71, 69, 36, 50, 42, 23, -11, 68, 47, -29, 98, -4, -33, -19, -113, -21, -85, 119, 86, -77, 82, -37, -87, -94, -118, 65, 38, 0, 79, -121, 68, -37, -76, 99, -58, 116, 13, -103, 25, -33, 124, -67, -111, 115, -28, -6, 91, 24, 68, -12, -73, -120, -26, -87, 18, 51, -61, 10, -58, -101, -105, 121, -55, 111, -80, -42, -38, -101, -37, 74, 99, 74, 87, 69, 121, 27, -27, -1, 0, -30, 6, -126, -39, -2, 38, 7, -12, 32, -12, -26, -124, -120, -10, 49, -58, 52, -29, 12, 15, 72, -87, 29, 90, 122, -90, 33, -65, 10, 124, -109, -29, -99, 55, -6, -110, -30, 19, -11, 95, 83, 86, 82, -53, -24, -103, 16, -107, 28, -99, -128, -13, -3, 70, -106, -122, -88, -92, -88, -88, -115, -36, -47, -57, -61, -111, -13, 17, -1, 0, 125, 45, 75, 85, -53, 81, -83, -86, -13, 90, -43, -122, -72, -52, 22, 37, -107, -103, 112, -125, 39, 32, -27, -78, 126, -98, 52, 1, 95, -43, -122, -11, 21, 75, 77, 92, 41, 69, 29, 76, -111, -61, 22, -48, 11, -32, 16, 73, 44, 51, -49, 39, -113, -66, -92, 90, -5, -59, 5, 13, -127, -92, -84, 72, -41, -31, 41, -100, -31, 87, 0, 40, 98, 88, -2, -25, 62, 117, 94, 122, -63, 110, 18, 85, -98, -91, 68, 95, -125, -88, 97, 60, 108, 37, 27, -95, 83, -32, 54, 63, -73, -2, -75, -103, 54, 0, -42, -42, -111, -83, 14, 60, -87, 2, 117, 69, 87, -60, -42, -49, 44, 37, -34, 70, 109, -77, 9, 63, -100, 15, -73, -115, 1, 93, 41, 105, -42, -102, 82, -108, -46, 64, -45, 124, -52, 9, 62, -101, -114, 112, 54, -7, -32, -122, -25, -22, 71, 28, 104, -50, -7, 116, -90, -72, -53, 52, -12, -14, 71, 2, -16, -40, 35, -106, -57, -106, 56, -15, -1, 0, -67, 53, 73, 36, 49, -76, 102, 106, 111, 95, 43, -103, 20, -16, 118, -3, 72, -5, -97, -19, -82, 53, -76, -116, 95, 73, -106, -57, -15, -12, 113, 109, -118, 85, -105, 126, -46, -123, -92, 35, -45, 0, 29, -53, -127, -28, -109, -76, -25, 60, 99, -33, 58, 55, -95, -70, 95, 41, 105, -43, -38, 40, -29, 76, 42, -82, -42, -36, 84, 14, 15, -7, 115, -90, 107, 125, 53, -78, 73, 74, 71, 69, -23, 70, 67, 72, 89, 101, -61, -87, -64, -56, -57, -45, -12, -23, -46, 57, -23, 112, -87, 19, 29, -59, -65, -27, 103, -99, -61, -50, 113, -94, -122, 56, 4, 7, 72, 15, 68, 79, 21, 124, 117, 52, -53, -66, -78, 66, -17, -63, 118, -7, 118, -110, 60, 127, 81, -127, -3, 53, -58, -47, -47, -45, -43, 25, -108, -6, -18, -8, -61, -71, -25, 35, 76, -26, -70, 55, -87, 49, -62, -7, 88, -108, -18, 0, -28, 18, 113, -17, -12, -1, 0, -74, -69, 105, 37, 87, 112, -43, -118, -79, -100, 109, 8, 61, -121, -111, -57, -65, -1, 0, -102, -125, -126, 29, -46, -80, -1, 0, -123, 26, 85, -97, -83, -65, -118, 78, 3, 109, -119, -63, 45, -32, 19, -64, -29, 86, -26, -27, -46, 109, 88, 13, 66, 72, -79, 43, 28, 23, 43, -17, -10, -1, 0, -49, 125, 83, -114, -54, -36, 23, -89, 96, -89, -86, -126, 39, 18, 84, -50, -93, 57, -2, 81, -85, -91, 111, -87, -85, -85, -94, -118, 42, -87, -41, -46, -110, 53, -111, 8, 31, 54, -101, 108, -48, 4, 91, -67, 82, -68, -65, 29, -95, 83, -48, 115, 69, 48, -108, -44, 43, 7, -25, -17, -57, -41, 90, -33, -90, 107, -23, -93, -102, 89, -35, 84, -80, -64, 12, 8, -34, 62, -38, 47, -8, -118, -38, 38, -111, -28, -125, -28, 113, -124, -53, 2, 72, -6, -97, -90, -72, 100, -8, -85, -101, -95, -98, -92, 8, -45, 39, -45, 83, -110, 52, -62, -76, 85, 80, 53, 77, -78, 117, -104, -120, 36, 40, -98, -61, 39, -3, -76, -76, 75, 81, 110, -73, -68, -84, 97, -88, 27, 65, -58, 25, 121, 7, 75, 92, -91, -19, 20, 7, -44, -44, -108, -33, -62, -103, 125, 21, -61, -84, -118, -61, 28, 16, 7, -116, 106, -71, -55, 85, 81, 53, 112, -92, -106, 102, 104, 29, 25, 12, 103, -12, -128, 16, -111, -127, -19, -49, -45, 75, 75, 89, -71, 5, 16, -76, -111, 120, 74, 15, -83, -94, -92, -98, 72, -124, -76, -24, -39, 56, 60, 96, -97, 62, -6, 85, 52, -76, -17, 75, -67, -95, 82, -63, 50, 15, -72, -32, -97, -10, 26, 90, 90, 52, 0, 16, -123, -112, 72, 114, 17, 53, -107, 34, -43, 61, 64, -108, -119, 84, -54, 3, 96, 103, 25, -58, -120, 34, -114, 52, -110, 9, 82, 53, 12, 99, -107, -55, 10, 63, 86, -52, -25, -4, -64, -46, -46, -47, 74, 13, -81, -74, 104, -45, -29, -92, -117, 111, -54, -50, 20, -113, -74, -33, 26, -20, -78, 19, 53, -56, -76, -89, 121, 57, 25, 60, -8, 36, 13, 45, 45, 1, -2, 18, -68, 124, 74, 113, -23, -41, 104, -22, 45, 91, 24, -81, 10, 120, -5, -99, 93, -5, 35, -78, 88, 45, -45, 41, -7, -52, 96, 18, 121, -10, -46, -46, -45, 13, -101, -6, -69, 37, -7, -97, -91, 102, -45, -53, 83, 14, 103, 125, -25, 118, 57, 30, -40, 58, -62, -113, -14, -29, -100, -89, 7, 110, 116, -76, -76, -63, 86, 11, 11, 101, 52, 19, 82, 9, 101, -116, 51, -79, 36, -109, -17, -91, -91, -91, -81, 41, 47, -1, -39, -56, 1, 0}), true)); } } diff --git a/src/test/java/it/auties/whatsapp/local/WebTest.java b/src/test/java/it/auties/whatsapp/local/WebTest.java index 66d6e0e68..04a5da95a 100644 --- a/src/test/java/it/auties/whatsapp/local/WebTest.java +++ b/src/test/java/it/auties/whatsapp/local/WebTest.java @@ -4,6 +4,10 @@ import it.auties.whatsapp.api.WebHistoryLength; import it.auties.whatsapp.api.Whatsapp; import it.auties.whatsapp.model.info.MessageInfo; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.message.standard.ImageMessageSimpleBuilder; +import it.auties.whatsapp.model.signal.auth.UserAgent; +import it.auties.whatsapp.utils.MediaUtils; import org.junit.jupiter.api.Test; // Just used for testing locally @@ -12,9 +16,14 @@ public class WebTest { public void run() { var whatsapp = Whatsapp.webBuilder() .lastConnection() - .historyLength(WebHistoryLength.extended()) + .historyLength(WebHistoryLength.zero()) + .releaseChannel(UserAgent.ReleaseChannel.BETA) .unregistered(QrHandler.toTerminal()) - .addLoggedInListener(api -> System.out.printf("Connected: %s%n", api.store().privacySettings())) + .addLoggedInListener(api -> { + System.out.printf("Connected: %s%n", api.store().privacySettings()); + var chat = Jid.of("120363185259738473@newsletter"); + api.sendMessage(chat, new ImageMessageSimpleBuilder().media(MediaUtils.readBytes("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/481px-Cat03.jpg")).caption("Test").build()).join(); + }) .addFeaturesListener(features -> System.out.printf("Received features: %s%n", features)) .addNewMessageListener((api, message, offline) -> System.out.println(message.toJson())) .addContactsListener((api, contacts) -> System.out.printf("Contacts: %s%n", contacts.size())) diff --git a/src/test/java/it/auties/whatsapp/update/UpdateBinaryTokensTest.java b/src/test/java/it/auties/whatsapp/update/UpdateBinaryTokensTest.java index ee5bd3839..d440cf66b 100644 --- a/src/test/java/it/auties/whatsapp/update/UpdateBinaryTokensTest.java +++ b/src/test/java/it/auties/whatsapp/update/UpdateBinaryTokensTest.java @@ -5,7 +5,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import it.auties.whatsapp.github.GithubActions; -import it.auties.whatsapp.util.Spec.Whatsapp; +import it.auties.whatsapp.util.Specification.Whatsapp; import org.junit.jupiter.api.Test; import java.io.IOException;