diff --git a/src/main/java/it/auties/whatsapp/api/Whatsapp.java b/src/main/java/it/auties/whatsapp/api/Whatsapp.java index b8c05869..2b621338 100644 --- a/src/main/java/it/auties/whatsapp/api/Whatsapp.java +++ b/src/main/java/it/auties/whatsapp/api/Whatsapp.java @@ -34,10 +34,7 @@ import it.auties.whatsapp.model.message.model.reserved.ExtendedMediaMessage; import it.auties.whatsapp.model.message.server.ProtocolMessage; import it.auties.whatsapp.model.message.server.ProtocolMessageBuilder; -import it.auties.whatsapp.model.message.standard.CallMessageBuilder; -import it.auties.whatsapp.model.message.standard.NewsletterAdminInviteMessageBuilder; -import it.auties.whatsapp.model.message.standard.ReactionMessageBuilder; -import it.auties.whatsapp.model.message.standard.TextMessage; +import it.auties.whatsapp.model.message.standard.*; import it.auties.whatsapp.model.newsletter.*; import it.auties.whatsapp.model.node.Attributes; import it.auties.whatsapp.model.node.Node; @@ -674,6 +671,44 @@ public CompletableFuture editMessage(T oldMessage, Me }; } + /** + * Pin a message + * + * @param messageKey non-null message key to pin + * @param pinTimer the default timer that message will be pinned + * @return a CompletableFuture + */ + public CompletableFuture pinMessage(ChatMessageKey messageKey, ChatMessagePinTimer pinTimer) { + if (messageKey.fromMe()) { + messageKey.setSenderJid(null); + } + var message = new PinInChatMessageBuilder() + .key(messageKey) + .pinType(PinInChatMessage.Type.PIN_FOR_ALL) + .senderTimestampMilliseconds(Clock.nowMilliseconds()) + .build(); + var deviceInfo = new DeviceContextInfoBuilder() + .messageAddOnDurationInSecs(pinTimer.periodSeconds()) + .build(); + var sender = messageKey.chatJid().hasServer(JidServer.GROUP) ? jidOrThrowError() : null; + var key = new ChatMessageKeyBuilder() + .id(ChatMessageKey.randomIdV2(sender, store().clientType())) + .chatJid(messageKey.chatJid()) + .fromMe(true) + .senderJid(sender) + .build(); + var pinInfo = new ChatMessageInfoBuilder() + .status(MessageStatus.PENDING) + .senderJid(sender) + .key(key) + .message(MessageContainer.of(message).withDeviceInfo(deviceInfo)) + .timestampSeconds(Clock.nowSeconds()) + .build(); + var attrs = Map.of("edit", 2); + var request = new MessageSendRequest.Chat(pinInfo, null, false, false, attrs); + return socketHandler.sendMessage(request); + } + public CompletableFuture sendStatus(String message) { return sendStatus(MessageContainer.of(message)); } diff --git a/src/main/java/it/auties/whatsapp/model/chat/ChatMessagePinTimer.java b/src/main/java/it/auties/whatsapp/model/chat/ChatMessagePinTimer.java new file mode 100644 index 00000000..aa5c5f94 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/ChatMessagePinTimer.java @@ -0,0 +1,63 @@ +package it.auties.whatsapp.model.chat; + +import it.auties.protobuf.annotation.ProtobufConverter; +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; + +import java.time.Duration; +import java.util.Arrays; + +/** + * Enum representing the ChatMessagePinTimer period. Each constant is associated with a specific + * duration period. + */ +public enum ChatMessagePinTimer implements ProtobufEnum { + /** + * ChatMessagePinTimer with duration of 0 days. + */ + OFF(0, Duration.ofDays(0)), + + /** + * ChatMessagePinTimer with duration of 1 day. + */ + ONE_DAY(1, Duration.ofDays(1)), + + /** + * ChatMessagePinTimer with duration of 7 days. + */ + ONE_WEEK(2, Duration.ofDays(7)), + + /** + * ChatMessagePinTimer with duration of 30 days. + */ + ONE_MONTH(3, Duration.ofDays(30)); + + private final Duration period; + final int index; + + ChatMessagePinTimer(@ProtobufEnumIndex int index, Duration period) { + this.index = index; + this.period = period; + } + + public int index() { + return index; + } + + public Duration period() { + return period; + } + + @ProtobufConverter + public static ChatMessagePinTimer of(int value) { + return Arrays.stream(values()) + .filter(entry -> entry.period().toSeconds() == value || entry.period().toDays() == value) + .findFirst() + .orElse(OFF); + } + + @ProtobufConverter + public int periodSeconds() { + return (int) period.toSeconds(); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/info/DeviceContextInfo.java b/src/main/java/it/auties/whatsapp/model/info/DeviceContextInfo.java index da626a36..38070165 100644 --- a/src/main/java/it/auties/whatsapp/model/info/DeviceContextInfo.java +++ b/src/main/java/it/auties/whatsapp/model/info/DeviceContextInfo.java @@ -23,12 +23,16 @@ public final class DeviceContextInfo implements Info, ProtobufMessage { @ProtobufProperty(index = 4, type = ProtobufType.BYTES) private final byte[] paddingBytes; + @ProtobufProperty(index = 5, type = ProtobufType.UINT32) + private final int messageAddOnDurationInSecs; + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public DeviceContextInfo(DeviceListMetadata deviceListMetadata, int deviceListMetadataVersion, byte[] messageSecret, byte[] paddingBytes) { + public DeviceContextInfo(DeviceListMetadata deviceListMetadata, int deviceListMetadataVersion, byte[] messageSecret, byte[] paddingBytes, int messageAddOnDurationInSecs) { this.deviceListMetadata = deviceListMetadata; this.deviceListMetadataVersion = deviceListMetadataVersion; this.messageSecret = messageSecret; this.paddingBytes = paddingBytes; + this.messageAddOnDurationInSecs = messageAddOnDurationInSecs; } public Optional deviceListMetadata() { @@ -50,4 +54,8 @@ public void setMessageSecret(byte[] messageSecret) { public Optional paddingBytes() { return Optional.ofNullable(paddingBytes); } -} \ No newline at end of file + + public int messageAddOnDurationInSecs() { + return messageAddOnDurationInSecs; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/message/model/Message.java b/src/main/java/it/auties/whatsapp/model/message/model/Message.java index 662cae3a..450eabb3 100644 --- a/src/main/java/it/auties/whatsapp/model/message/model/Message.java +++ b/src/main/java/it/auties/whatsapp/model/message/model/Message.java @@ -6,7 +6,7 @@ /** * A model interface that represents a message sent by a contact or by Whatsapp. */ -public sealed interface Message extends ProtobufMessage permits ButtonMessage, ContextualMessage, PaymentMessage, ServerMessage, CallMessage, EmptyMessage, KeepInChatMessage, NewsletterAdminInviteMessage, PollUpdateMessage, ReactionMessage { +public sealed interface Message extends ProtobufMessage permits ButtonMessage, ContextualMessage, PaymentMessage, ServerMessage, CallMessage, EmptyMessage, KeepInChatMessage, NewsletterAdminInviteMessage, PinInChatMessage, PollUpdateMessage, ReactionMessage { /** * Return message type * diff --git a/src/main/java/it/auties/whatsapp/model/message/model/MessageContainer.java b/src/main/java/it/auties/whatsapp/model/message/model/MessageContainer.java index 421c4b50..68d86ed4 100644 --- a/src/main/java/it/auties/whatsapp/model/message/model/MessageContainer.java +++ b/src/main/java/it/auties/whatsapp/model/message/model/MessageContainer.java @@ -126,6 +126,8 @@ public record MessageContainer( Optional editedMessage, @ProtobufProperty(index = 59, type = ProtobufType.OBJECT) Optional viewOnceV2ExtensionMessage, + @ProtobufProperty(index = 63, type = ProtobufType.OBJECT) + Optional pinInChatMessage, @ProtobufProperty(index = 78, type = ProtobufType.OBJECT) Optional newsletterAdminInviteMessage, @ProtobufProperty(index = 35, type = ProtobufType.OBJECT) @@ -210,6 +212,7 @@ public static MessageContainerBuilder ofBuilder(T message) { builder.pollCreationMessage(Optional.of(pollCreationMessage)); case PollUpdateMessage pollUpdateMessage -> builder.pollUpdateMessage(Optional.of(pollUpdateMessage)); case KeepInChatMessage keepInChatMessage -> builder.keepInChatMessage(Optional.of(keepInChatMessage)); + case PinInChatMessage pinInChatMessage -> builder.pinInChatMessage(Optional.of(pinInChatMessage)); case RequestPhoneNumberMessage requestPhoneNumberMessage -> builder.requestPhoneNumberMessage(Optional.of(requestPhoneNumberMessage)); case EncryptedReactionMessage encReactionMessage -> @@ -420,6 +423,9 @@ public Message content() { if (keepInChatMessage.isPresent()) { return keepInChatMessage.get(); } + if (pinInChatMessage.isPresent()) { + return pinInChatMessage.get(); + } if (documentWithCaptionMessage.isPresent()) { return documentWithCaptionMessage.get().unbox(); } @@ -668,4 +674,4 @@ public boolean isEmpty() { public String toString() { return Objects.toString(content()); } -} \ No newline at end of file +} diff --git a/src/main/java/it/auties/whatsapp/model/message/model/MessageType.java b/src/main/java/it/auties/whatsapp/model/message/model/MessageType.java index 317004ff..a572f577 100644 --- a/src/main/java/it/auties/whatsapp/model/message/model/MessageType.java +++ b/src/main/java/it/auties/whatsapp/model/message/model/MessageType.java @@ -185,6 +185,10 @@ public enum MessageType { * Text edit */ EDITED, + /** + * Pin in chat + */ + PIN_IN_CHAT, /** * Newsletter admin invite */ diff --git a/src/main/java/it/auties/whatsapp/model/message/model/PinInChat.java b/src/main/java/it/auties/whatsapp/model/message/model/PinInChat.java new file mode 100644 index 00000000..43b829cb --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/message/model/PinInChat.java @@ -0,0 +1,47 @@ +package it.auties.whatsapp.model.message.model; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.info.DeviceContextInfo; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.Optional; + + +/** + * A model class that represents an ephemeral message that was saved manually by the user in a chat + */ +@ProtobufMessageName("PinInChat") +public record PinInChat( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + Type pinType, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + ChatMessageKey key, + @ProtobufProperty(index = 3, type = ProtobufType.INT64) + long clientTimestampInMilliseconds, + @ProtobufProperty(index = 4, type = ProtobufType.INT64) + long serverTimestampMilliseconds, + @ProtobufProperty(index = 5, type = ProtobufType.OBJECT) + DeviceContextInfo messageAddOnContextInfo +) implements ProtobufMessage { + public Optional serverTimestamp() { return Clock.parseMilliseconds(serverTimestampMilliseconds); } + + public Optional clientTimestamp() { return Clock.parseMilliseconds(clientTimestampInMilliseconds); } + + public enum Type implements ProtobufEnum { + UNKNOWN_TYPE(0), + PIN_FOR_ALL(1), + UNDO_PIN_FOR_ALL(2); + + final int index; + + Type(@ProtobufEnumIndex int index) { this.index = index; } + + public int index() { return index; } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/PinInChatMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/PinInChatMessage.java new file mode 100644 index 00000000..8291a0ff --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/message/standard/PinInChatMessage.java @@ -0,0 +1,37 @@ +package it.auties.whatsapp.model.message.standard; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.message.model.*; + + +@ProtobufMessageName("Message.PinInChatMessage") +public record PinInChatMessage( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + ChatMessageKey key, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + Type pinType, + @ProtobufProperty(index = 3, type = ProtobufType.INT64) + long senderTimestampMilliseconds +) implements Message { + @Override + public MessageType type() { return MessageType.PIN_IN_CHAT; } + + @Override + public MessageCategory category() { return MessageCategory.STANDARD; } + + @ProtobufMessageName("Message.PinInChatMessage.Type") + public enum Type implements ProtobufEnum { + UNKNOWN_TYPE(0), + PIN_FOR_ALL(1), + UNPIN_FOR_ALL(2); + + final int index; + + Type(int index) { this.index = index; } + + public int index() { return index; } + } +}