From 6f78f0b34440115b1ae21e92260db5f607b69c65 Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Tue, 24 Oct 2023 23:55:40 +0200 Subject: [PATCH] More work for API stability --- .../whatsapp/model/info/ChatMessageInfo.java | 1 - .../model/info/NewsletterMessageInfo.java | 7 +- .../message/standard/StickerMessage.java | 2 +- .../whatsapp/socket/MessageHandler.java | 72 +++++++++++-------- .../it/auties/whatsapp/util/BytesHelper.java | 10 +++ .../util/DefaultControllerSerializer.java | 42 +++++++++-- .../it/auties/whatsapp/local/WebRunner.java | 11 ++- 7 files changed, 108 insertions(+), 37 deletions(-) diff --git a/src/main/java/it/auties/whatsapp/model/info/ChatMessageInfo.java b/src/main/java/it/auties/whatsapp/model/info/ChatMessageInfo.java index 40b2d59b..8d5a9803 100644 --- a/src/main/java/it/auties/whatsapp/model/info/ChatMessageInfo.java +++ b/src/main/java/it/auties/whatsapp/model/info/ChatMessageInfo.java @@ -117,7 +117,6 @@ public final class ChatMessageInfo implements MessageInfo, MessageStatusInfo { @JsonBackReference - private final Newsletter newsletter; + private Newsletter newsletter; private final String id; private final int serverId; private final Long timestampSeconds; @@ -33,6 +33,11 @@ public NewsletterMessageInfo(Newsletter newsletter, String id, int serverId, Lon this.status = status; } + public NewsletterMessageInfo setNewsletter(Newsletter newsletter) { + this.newsletter = newsletter; + return this; + } + public Jid newsletterJid() { return newsletter.jid(); } 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 bf6e5b86..5adb4b83 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 @@ -79,7 +79,7 @@ public final class StickerMessage extends LocalMediaMessage impl private final boolean avatar; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public StickerMessage(String mediaUrl, byte[] mediaSha256, byte[] mediaEncryptedSha256, byte[] mediaKey, String mimetype, Integer height, Integer width, String mediaDirectPath, Long mediaSize, Long mediaKeyTimestampSeconds, Integer firstFrameLength, byte[] firstFrameSidecar, boolean animated, byte[] thumbnail, ContextInfo contextInfo, long stickerSentTimestamp, boolean avatar) { + public StickerMessage(String mediaUrl, byte[] mediaSha256, byte[] mediaEncryptedSha256, byte[] mediaKey, String mimetype, Integer height, Integer width, String mediaDirectPath, Long mediaSize, Long mediaKeyTimestampSeconds, Integer firstFrameLength, byte[] firstFrameSidecar, boolean animated, byte[] thumbnail, ContextInfo contextInfo, Long stickerSentTimestamp, boolean avatar) { this.mediaUrl = mediaUrl; this.mediaSha256 = mediaSha256; this.mediaEncryptedSha256 = mediaEncryptedSha256; diff --git a/src/main/java/it/auties/whatsapp/socket/MessageHandler.java b/src/main/java/it/auties/whatsapp/socket/MessageHandler.java index 5a7a04de..4c609089 100644 --- a/src/main/java/it/auties/whatsapp/socket/MessageHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/MessageHandler.java @@ -47,6 +47,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; @@ -365,6 +366,15 @@ private Node getPlainMessageNode(MessageContainer message) { return Node.of("reaction", Map.of("code", reactionMessage.content())); } + if(message.content() instanceof TextMessage textMessage && textMessage.thumbnail().isEmpty()) { + var byteArrayOutputStream = new ByteArrayOutputStream(); + byteArrayOutputStream.write(10); + var encoded = textMessage.text().getBytes(StandardCharsets.UTF_8); + byteArrayOutputStream.writeBytes(BytesHelper.intToVarInt(encoded.length)); + byteArrayOutputStream.writeBytes(encoded); + return Node.of("plaintext", byteArrayOutputStream.toByteArray()); + } + var messageAttributes = Attributes.of() .put("mediatype", getMediaType(message), Objects::nonNull) .toMap(); @@ -1278,17 +1288,24 @@ private void handlePastParticipants(GroupPastParticipants pastParticipants) { @SafeVarargs private List toSingleList(List... all) { - return Stream.of(all) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .toList(); + return switch (all.length) { + case 0 -> List.of(); + case 1 -> all[0]; + default -> Stream.of(all) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .toList(); + }; } - private ChatMessageKey attributeSender(ChatMessageInfo info, Jid senderJid) { + private void attributeSender(ChatMessageInfo info, Jid senderJid) { + if(senderJid.server() != JidServer.WHATSAPP && senderJid.server() != JidServer.USER) { + return; + } + var contact = socketHandler.store().findContactByJid(senderJid) .orElseGet(() -> socketHandler.store().addContact(new Contact(senderJid))); info.setSender(contact); - return info.key(); } private void attributeContext(ContextInfo contextInfo) { @@ -1308,13 +1325,30 @@ private void attributeContextSender(ContextInfo contextInfo, Jid senderJid) { contextInfo.setQuotedMessageSender(contact); } - private void processMessage(ChatMessageInfo info) { + protected ChatMessageInfo attributeChatMessage(ChatMessageInfo info) { + var chat = socketHandler.store().findChatByJid(info.chatJid()) + .orElseGet(() -> socketHandler.store().addNewChat(info.chatJid())); + info.setChat(chat); + var me = socketHandler.store().jid().orElse(null); + if (info.fromMe() && me != null) { + info.key().setSenderJid(me.withoutDevice()); + } + + attributeSender(info, info.senderJid()); + info.message() + .contentWithContext() + .flatMap(ContextualMessage::contextInfo) + .ifPresent(this::attributeContext); + processMessageWithSecret(info); + return info; + } + + private void processMessageWithSecret(ChatMessageInfo info) { switch (info.message().content()) { case PollCreationMessage pollCreationMessage -> handlePollCreation(info, pollCreationMessage); case PollUpdateMessage pollUpdateMessage -> handlePollUpdate(info, pollUpdateMessage); case ReactionMessage reactionMessage -> handleReactionMessage(info, reactionMessage); - default -> { - } + default -> {} } } @@ -1384,26 +1418,6 @@ private void handleReactionMessage(ChatMessageInfo info, ReactionMessage reactio .ifPresent(message -> message.reactions().add(reactionMessage)); } - protected ChatMessageInfo attributeChatMessage(ChatMessageInfo info) { - var chat = socketHandler.store().findChatByJid(info.chatJid()) - .orElseGet(() -> socketHandler.store().addNewChat(info.chatJid())); - info.setChat(chat); - var me = socketHandler.store().jid().orElse(null); - if (info.fromMe() && me != null) { - info.key().setSenderJid(me.withoutDevice()); - } - - info.key() - .senderJid() - .ifPresent(senderJid -> attributeSender(info, senderJid)); - info.message() - .contentWithContext() - .flatMap(ContextualMessage::contextInfo) - .ifPresent(this::attributeContext); - processMessage(info); - return info; - } - protected void dispose() { historyCache.clear(); historySyncTask = null; diff --git a/src/main/java/it/auties/whatsapp/util/BytesHelper.java b/src/main/java/it/auties/whatsapp/util/BytesHelper.java index 8d2c483c..a20b9b1c 100644 --- a/src/main/java/it/auties/whatsapp/util/BytesHelper.java +++ b/src/main/java/it/auties/whatsapp/util/BytesHelper.java @@ -169,4 +169,14 @@ public static String bytesToCrockford(byte[] bytes) { return crockford.toString(); } + + 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(); + } } diff --git a/src/main/java/it/auties/whatsapp/util/DefaultControllerSerializer.java b/src/main/java/it/auties/whatsapp/util/DefaultControllerSerializer.java index 55764674..e4dc85b4 100644 --- a/src/main/java/it/auties/whatsapp/util/DefaultControllerSerializer.java +++ b/src/main/java/it/auties/whatsapp/util/DefaultControllerSerializer.java @@ -7,9 +7,12 @@ import it.auties.whatsapp.controller.Store; import it.auties.whatsapp.model.chat.Chat; import it.auties.whatsapp.model.chat.ChatBuilder; +import it.auties.whatsapp.model.info.ContextInfo; import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.message.model.ContextualMessage; import it.auties.whatsapp.model.mobile.PhoneNumber; import it.auties.whatsapp.model.newsletter.Newsletter; +import it.auties.whatsapp.model.sync.HistorySyncMessage; import java.io.IOException; import java.io.UncheckedIOException; @@ -331,7 +334,8 @@ public CompletableFuture attributeStore(Store store) { var futures = walker.map(entry -> handleStoreFile(store, entry)) .filter(Objects::nonNull) .toArray(CompletableFuture[]::new); - var result = CompletableFuture.allOf(futures); + var result = CompletableFuture.allOf(futures) + .thenRun(() -> attributeStoreContextualMessages(store)); attributeStoreSerializers.put(store.uuid(), result); return result; } catch (IOException exception) { @@ -339,11 +343,33 @@ public CompletableFuture attributeStore(Store store) { } } + // Do this after we have all the chats, or it won't work for obvious reasons + private void attributeStoreContextualMessages(Store store) { + store.chats() + .stream() + .flatMap(chat -> chat.messages().stream()) + .forEach(message -> attributeStoreContextualMessage(store, message)); + } + + private void attributeStoreContextualMessage(Store store, HistorySyncMessage message) { + message.messageInfo() + .message() + .contentWithContext() + .flatMap(ContextualMessage::contextInfo) + .ifPresent(contextInfo -> attributeStoreContextInfo(store, contextInfo)); + } + + private void attributeStoreContextInfo(Store store, ContextInfo contextInfo) { + contextInfo.quotedMessageChatJid() + .flatMap(store::findChatByJid) + .ifPresent(contextInfo::setQuotedMessageChat); + } + private CompletableFuture handleStoreFile(Store store, Path entry) { return switch (FileType.of(entry)) { - case UNKNOWN -> null; case NEWSLETTER -> CompletableFuture.runAsync(() -> deserializeNewsletter(store, entry)); case CHAT -> CompletableFuture.runAsync(() -> deserializeChat(store, entry)); + case UNKNOWN -> null; }; } @@ -421,7 +447,11 @@ 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.addChatDirect(Smile.readValue(input, Chat.class)); + var chat = Smile.readValue(input, Chat.class); + for (var message : chat.messages()) { + message.messageInfo().setChat(chat); + } + store.addChatDirect(chat); } catch (IOException exception) { store.addChatDirect(rescueChat(chatFile)); } @@ -444,7 +474,11 @@ private Chat rescueChat(Path entry) { private void deserializeNewsletter(Store store, Path newsletterFile) { try (var input = new GZIPInputStream(Files.newInputStream(newsletterFile))) { - store.addNewsletter(Smile.readValue(input, Newsletter.class)); + var newsletter = Smile.readValue(input, Newsletter.class); + for (var message : newsletter.messages()) { + message.setNewsletter(newsletter); + } + store.addNewsletter(newsletter); } catch (IOException exception) { store.addNewsletter(rescueNewsletter(newsletterFile)); } diff --git a/src/test/java/it/auties/whatsapp/local/WebRunner.java b/src/test/java/it/auties/whatsapp/local/WebRunner.java index 086ba573..ff4e8fef 100644 --- a/src/test/java/it/auties/whatsapp/local/WebRunner.java +++ b/src/test/java/it/auties/whatsapp/local/WebRunner.java @@ -4,6 +4,7 @@ import it.auties.whatsapp.api.WebHistoryLength; import it.auties.whatsapp.api.Whatsapp; import it.auties.whatsapp.model.info.ChatMessageInfo; +import it.auties.whatsapp.model.newsletter.NewsletterViewerRole; // Just used for testing locally public class WebRunner { @@ -17,7 +18,15 @@ public static void main(String[] args) { .addNewChatMessageListener((api, message) -> System.out.println(message.toJson())) .addContactsListener((api, contacts) -> System.out.printf("Contacts: %s%n", contacts.size())) .addChatsListener(chats -> System.out.printf("Chats: %s%n", chats.size())) - .addNewslettersListener((newsletters) -> System.out.printf("Newsletters: %s%n", newsletters.size())) + .addNewslettersListener((api, newsletters) -> { + System.out.printf("Newsletters: %s%n", newsletters.size()); + var jid = newsletters.stream() + .filter(entry -> entry.viewerMetadata().isPresent() && entry.viewerMetadata().get().role() == NewsletterViewerRole.OWNER) + .findFirst() + .orElseThrow() + .jid(); + api.sendMessage(jid, "Hello").join(); + }) .addNodeReceivedListener(incoming -> System.out.printf("Received node %s%n", incoming)) .addNodeSentListener(outgoing -> System.out.printf("Sent node %s%n", outgoing)) .addActionListener ((action, info) -> System.out.printf("New action: %s, info: %s%n", action, info))