From 6baa68b9f006dc2680e5785a42365007504a9a2e Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 18 May 2021 23:47:36 -0400 Subject: [PATCH 1/2] feat: support crowd chant chat and pubsub experiment --- .../events/channel/ChannelMessageEvent.java | 11 +++ .../twitch4j/chat/util/ChatCrowdChant.java | 89 +++++++++++++++++++ .../github/twitch4j/pubsub/ITwitchPubSub.java | 5 ++ .../github/twitch4j/pubsub/TwitchPubSub.java | 7 ++ .../twitch4j/pubsub/domain/CrowdChant.java | 19 ++++ .../pubsub/events/CrowdChantCreatedEvent.java | 18 ++++ 6 files changed, 149 insertions(+) create mode 100644 chat/src/main/java/com/github/twitch4j/chat/util/ChatCrowdChant.java create mode 100644 pubsub/src/main/java/com/github/twitch4j/pubsub/domain/CrowdChant.java create mode 100644 pubsub/src/main/java/com/github/twitch4j/pubsub/events/CrowdChantCreatedEvent.java diff --git a/chat/src/main/java/com/github/twitch4j/chat/events/channel/ChannelMessageEvent.java b/chat/src/main/java/com/github/twitch4j/chat/events/channel/ChannelMessageEvent.java index 7076e5acd..f42fc81e0 100644 --- a/chat/src/main/java/com/github/twitch4j/chat/events/channel/ChannelMessageEvent.java +++ b/chat/src/main/java/com/github/twitch4j/chat/events/channel/ChannelMessageEvent.java @@ -2,6 +2,7 @@ import com.github.twitch4j.chat.events.AbstractChannelEvent; import com.github.twitch4j.chat.flag.AutoModFlag; +import com.github.twitch4j.chat.util.ChatCrowdChant; import com.github.twitch4j.common.annotation.Unofficial; import com.github.twitch4j.common.enums.CommandPermission; import com.github.twitch4j.common.events.domain.EventChannel; @@ -10,6 +11,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Value; +import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Optional; @@ -63,10 +65,19 @@ public class ChannelMessageEvent extends AbstractChannelEvent { /** * Information regarding the parent message being replied to, if applicable. */ + @Nullable @Unofficial @Getter(lazy = true) private ChatReply replyInfo = ChatReply.parse(getMessageEvent().getTags()); + /** + * Information regarding any associated Crowd Chant for this message, if applicable. + */ + @Nullable + @Unofficial + @Getter(lazy = true) + ChatCrowdChant chantInfo = ChatCrowdChant.parse(getMessageEvent()); + /** * Event Constructor * diff --git a/chat/src/main/java/com/github/twitch4j/chat/util/ChatCrowdChant.java b/chat/src/main/java/com/github/twitch4j/chat/util/ChatCrowdChant.java new file mode 100644 index 000000000..c65d8a59c --- /dev/null +++ b/chat/src/main/java/com/github/twitch4j/chat/util/ChatCrowdChant.java @@ -0,0 +1,89 @@ +package com.github.twitch4j.chat.util; + +import com.github.twitch4j.chat.TwitchChat; +import com.github.twitch4j.chat.events.channel.IRCMessageEvent; +import com.github.twitch4j.common.annotation.Unofficial; +import com.github.twitch4j.common.util.CryptoUtils; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Value; +import org.jetbrains.annotations.Nullable; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static com.github.twitch4j.chat.events.channel.IRCMessageEvent.NONCE_TAG_NAME; + +/** + * Information regarding a Crowd Chant participation or initiation. + * + * @see Related Uservoice + */ +@Value +@Unofficial +public class ChatCrowdChant { + + public static final String CHANT_MSG_ID_TAG_NAME = "crowd-chant-parent-msg-id"; + + /** + * The id for the parent message in the Crowd Chant. + */ + String messageId; + + /** + * The text being chanted. + */ + String text; + + /** + * Whether this message initiated a crowd chat. + * When false, the message is simply participating in the chant, rather than initiating. + */ + boolean initiator; + + /** + * The name of the channel where the Crowd Chant took place. + */ + @Getter(AccessLevel.PRIVATE) + String channelName; + + /** + * Sends the same message in the same channel to participate in the Crowd Chant, with the proper chat tag. + * + * @param chat an authenticated TwitchChat instance. + */ + @Unofficial + public void participate(TwitchChat chat) { + Map tags = new LinkedHashMap<>(); + tags.put(NONCE_TAG_NAME, CryptoUtils.generateNonce(32)); + tags.put(CHANT_MSG_ID_TAG_NAME, getMessageId()); + + chat.sendMessage(channelName, text, tags); + } + + /** + * Attempts to parse the {@link ChatCrowdChant} information from a chat event. + * + * @param event the raw IRCMessageEvent. + * @return ChatCrowdChant (or null if parsing was unsuccessful) + */ + @Nullable + public static ChatCrowdChant parse(IRCMessageEvent event) { + String channelName = event.getChannelName().orElse(null); + if (channelName == null) return null; + + String message = event.getMessage().orElse(null); + if (message == null) return null; + + if ("crowd-chant".equals(event.getTags().get("msg-id"))) { + return event.getMessageId() + .map(id -> new ChatCrowdChant(id, message, true, channelName)) + .orElse(null); + } + + return event.getTagValue(CHANT_MSG_ID_TAG_NAME) + .map(id -> new ChatCrowdChant(id, message, false, channelName)) + .orElse(null); + } + +} diff --git a/pubsub/src/main/java/com/github/twitch4j/pubsub/ITwitchPubSub.java b/pubsub/src/main/java/com/github/twitch4j/pubsub/ITwitchPubSub.java index a1a3be0da..8454ae205 100644 --- a/pubsub/src/main/java/com/github/twitch4j/pubsub/ITwitchPubSub.java +++ b/pubsub/src/main/java/com/github/twitch4j/pubsub/ITwitchPubSub.java @@ -195,6 +195,11 @@ default PubSubSubscription listenForCommunityBoostEvents(OAuth2Credential creden return listenOnTopic(PubSubType.LISTEN, credential, "community-boost-events-v1." + channelId); } + @Unofficial + default PubSubSubscription listenForCrowdChantEvents(OAuth2Credential credential, String channelId) { + return listenOnTopic(PubSubType.LISTEN, credential, "crowd-chant-channel-v1." + channelId); + } + @Unofficial default PubSubSubscription listenForUserChannelPointsEvents(OAuth2Credential credential, String userId) { return listenOnTopic(PubSubType.LISTEN, credential, "community-points-user-v1." + userId); diff --git a/pubsub/src/main/java/com/github/twitch4j/pubsub/TwitchPubSub.java b/pubsub/src/main/java/com/github/twitch4j/pubsub/TwitchPubSub.java index c1664a6a8..da4caa689 100644 --- a/pubsub/src/main/java/com/github/twitch4j/pubsub/TwitchPubSub.java +++ b/pubsub/src/main/java/com/github/twitch4j/pubsub/TwitchPubSub.java @@ -461,6 +461,13 @@ public void onTextMessage(WebSocket ws, String text) { break; } + } else if (topic.startsWith("crowd-chant-channel-v1")) { + if ("crowd-chant-created".equals(type)) { + CrowdChantCreatedEvent event = TypeConvert.convertValue(msgData, CrowdChantCreatedEvent.class); + eventManager.publish(event); + } else { + log.warn("Unparsable Message: " + message.getType() + "|" + message.getData()); + } } else if (topic.startsWith("raid")) { switch (type) { case "raid_go_v2": diff --git a/pubsub/src/main/java/com/github/twitch4j/pubsub/domain/CrowdChant.java b/pubsub/src/main/java/com/github/twitch4j/pubsub/domain/CrowdChant.java new file mode 100644 index 000000000..0f6b196ff --- /dev/null +++ b/pubsub/src/main/java/com/github/twitch4j/pubsub/domain/CrowdChant.java @@ -0,0 +1,19 @@ +package com.github.twitch4j.pubsub.domain; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; + +import java.time.Instant; + +@Data +@Setter(AccessLevel.PRIVATE) +public class CrowdChant { + private String id; + private String channelId; + private ChannelPointsUser user; + private String chatMessageId; + private String text; + private Instant createdAt; + private Instant endsAt; +} diff --git a/pubsub/src/main/java/com/github/twitch4j/pubsub/events/CrowdChantCreatedEvent.java b/pubsub/src/main/java/com/github/twitch4j/pubsub/events/CrowdChantCreatedEvent.java new file mode 100644 index 000000000..7b29cf5df --- /dev/null +++ b/pubsub/src/main/java/com/github/twitch4j/pubsub/events/CrowdChantCreatedEvent.java @@ -0,0 +1,18 @@ +package com.github.twitch4j.pubsub.events; + +import com.github.twitch4j.common.events.TwitchEvent; +import com.github.twitch4j.pubsub.domain.CrowdChant; +import lombok.AccessLevel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Setter; + +import java.time.Instant; + +@Data +@Setter(AccessLevel.PRIVATE) +@EqualsAndHashCode(callSuper = false) +public class CrowdChantCreatedEvent extends TwitchEvent { + private Instant timestamp; + private CrowdChant crowdChant; +} From 8f3b3601d3c652f8bdb822f5e17302e5c6f95f3d Mon Sep 17 00:00:00 2001 From: Sidd Date: Sat, 22 May 2021 01:04:12 -0400 Subject: [PATCH 2/2] refactor: add ITwitchChat#sendMessage(String, String, Map) --- .../com/github/twitch4j/chat/ITwitchChat.java | 25 ++++++- .../com/github/twitch4j/chat/TwitchChat.java | 26 -------- .../chat/TwitchChatConnectionPool.java | 66 +++++++++---------- .../twitch4j/chat/util/ChatCrowdChant.java | 4 +- 4 files changed, 56 insertions(+), 65 deletions(-) diff --git a/chat/src/main/java/com/github/twitch4j/chat/ITwitchChat.java b/chat/src/main/java/com/github/twitch4j/chat/ITwitchChat.java index e9ead40b3..692707709 100644 --- a/chat/src/main/java/com/github/twitch4j/chat/ITwitchChat.java +++ b/chat/src/main/java/com/github/twitch4j/chat/ITwitchChat.java @@ -3,8 +3,11 @@ import com.github.philippheuer.events4j.core.EventManager; import com.github.twitch4j.chat.events.channel.IRCMessageEvent; import com.github.twitch4j.common.annotation.Unofficial; +import com.github.twitch4j.common.util.ChatReply; +import org.jetbrains.annotations.Nullable; import java.time.Duration; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -35,7 +38,9 @@ public interface ITwitchChat extends AutoCloseable { * @param message The message to be sent. * @return whether the message was added to the queue */ - boolean sendMessage(String channel, String message); + default boolean sendMessage(String channel, String message) { + return this.sendMessage(channel, message, null); + } /** * Sends a message to the channel while including an optional nonce and/or reply parent. @@ -47,7 +52,23 @@ public interface ITwitchChat extends AutoCloseable { * @return whether the message was added to the queue */ @Unofficial - boolean sendMessage(String channel, String message, String nonce, String replyMsgId); + default boolean sendMessage(String channel, String message, String nonce, String replyMsgId) { + final Map tags = new LinkedHashMap<>(); // maintain insertion order + if (nonce != null) tags.put(IRCMessageEvent.NONCE_TAG_NAME, nonce); + if (replyMsgId != null) tags.put(ChatReply.REPLY_MSG_ID_TAG_NAME, replyMsgId); + return this.sendMessage(channel, message, tags); + } + + /** + * Sends a message to the channel while including the specified message tags. + * + * @param channel the name of the channel to send the message to. + * @param message the message to be sent. + * @param tags the message tags (unofficial). + * @return whether the message was added to the queue + */ + @Unofficial + boolean sendMessage(String channel, String message, @Unofficial @Nullable Map tags); /** * Returns a set of all currently joined channels (without # prefix) diff --git a/chat/src/main/java/com/github/twitch4j/chat/TwitchChat.java b/chat/src/main/java/com/github/twitch4j/chat/TwitchChat.java index b972be66e..774befd3f 100644 --- a/chat/src/main/java/com/github/twitch4j/chat/TwitchChat.java +++ b/chat/src/main/java/com/github/twitch4j/chat/TwitchChat.java @@ -665,33 +665,7 @@ private void issuePart(String channelName) { ); } - /** - * Sending message to the joined channel - * - * @param channel channel name - * @param message message - */ - @Override - public boolean sendMessage(String channel, String message) { - return this.sendMessage(channel, message, null); - } - @Override - @Unofficial - public boolean sendMessage(String channel, String message, String nonce, String replyMsgId) { - final Map tags = new LinkedHashMap<>(); // maintain insertion order - if (nonce != null) tags.put(IRCMessageEvent.NONCE_TAG_NAME, nonce); - if (replyMsgId != null) tags.put(ChatReply.REPLY_MSG_ID_TAG_NAME, replyMsgId); - return this.sendMessage(channel, message, tags); - } - - /** - * Sends a message to the channel while including the specified message tags. - * - * @param channel the name of the channel to send the message to. - * @param message the message to be sent. - * @param tags the message tags (unofficial). - */ public boolean sendMessage(String channel, String message, @Unofficial Map tags) { StringBuilder sb = new StringBuilder(); if (tags != null && !tags.isEmpty()) { diff --git a/chat/src/main/java/com/github/twitch4j/chat/TwitchChatConnectionPool.java b/chat/src/main/java/com/github/twitch4j/chat/TwitchChatConnectionPool.java index 5eb51e8ef..40ae52e34 100644 --- a/chat/src/main/java/com/github/twitch4j/chat/TwitchChatConnectionPool.java +++ b/chat/src/main/java/com/github/twitch4j/chat/TwitchChatConnectionPool.java @@ -2,15 +2,19 @@ import com.github.philippheuer.credentialmanager.domain.OAuth2Credential; import com.github.twitch4j.chat.events.channel.ChannelNoticeEvent; +import com.github.twitch4j.chat.events.channel.IRCMessageEvent; import com.github.twitch4j.common.annotation.Unofficial; import com.github.twitch4j.common.pool.TwitchModuleConnectionPool; +import com.github.twitch4j.common.util.ChatReply; import lombok.Builder; import lombok.NonNull; import lombok.experimental.SuperBuilder; import org.apache.commons.lang3.RandomStringUtils; +import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.function.Consumer; @@ -60,56 +64,51 @@ public class TwitchChatConnectionPool extends TwitchModuleConnectionPool tags) { + return this.sendMessage(channel, channel, message, tags); } /** * Sends a message from the {@link TwitchChat} identified either to a channel or directly on the socket. * * @param channelToIdentifyChatInstance the channel used to identify which {@link TwitchChat} instance should be used to send the message; the instance must be subscribed to this channel. - * @param targetChannel the channel to send the message to, if not null (otherwise it is sent directly on the socket) - * @param message the message to be sent + * @param targetChannel the channel to send the message to, if not null (otherwise it is sent directly on the socket). + * @param message the message to be sent. * @return whether a {@link TwitchChat} instance was found and used to send the message */ public boolean sendMessage(final String channelToIdentifyChatInstance, final String targetChannel, final String message) { - return this.sendMessage(channelToIdentifyChatInstance, targetChannel, message, null, null); + return this.sendMessage(channelToIdentifyChatInstance, targetChannel, message, Collections.emptyMap()); } /** * Sends a message from the identified {@link TwitchChat} instance with an optional nonce or reply parent. * * @param channelToIdentifyChatInstance the channel used to identify which {@link TwitchChat} instance should be used to send the message; the instance must be subscribed to this channel. - * @param targetChannel the channel to send the message to, if not null (otherwise it is sent directly on the socket) - * @param message the message to be sent + * @param targetChannel the channel to send the message to, if not null (otherwise it is sent directly on the socket). + * @param message the message to be sent. * @param nonce the cryptographic nonce (optional). * @param replyMsgId the msgId of the parent message being replied to (optional). * @return whether a {@link TwitchChat} instance was found and used to send the message */ @Unofficial public boolean sendMessage(final String channelToIdentifyChatInstance, final String targetChannel, final String message, final String nonce, final String replyMsgId) { + final Map tags = new LinkedHashMap<>(); + if (nonce != null) tags.put(IRCMessageEvent.NONCE_TAG_NAME, nonce); + if (replyMsgId != null) tags.put(ChatReply.REPLY_MSG_ID_TAG_NAME, replyMsgId); + return this.sendMessage(channelToIdentifyChatInstance, targetChannel, message, tags); + } + + /** + * Sends a message from the identified {@link TwitchChat} instance with the specified tags. + * + * @param channelToIdentifyChatInstance the channel used to identify which {@link TwitchChat} instance should be used to send the message; the instance must be subscribed to this channel. + * @param targetChannel the channel to send the message to, if not null (otherwise it is sent directly on the socket). + * @param message the message to be sent. + * @param tags the message tags (unofficial). + * @return whether a {@link TwitchChat} instance was found and used to send the message + */ + public boolean sendMessage(String channelToIdentifyChatInstance, String targetChannel, String message, @Unofficial @Nullable Map tags) { if (channelToIdentifyChatInstance == null) return false; @@ -118,10 +117,7 @@ public boolean sendMessage(final String channelToIdentifyChatInstance, final Str return false; if (targetChannel != null) { - if (nonce == null && replyMsgId == null) - chat.sendMessage(targetChannel, message); - else - chat.sendMessage(targetChannel, message, nonce, replyMsgId); + chat.sendMessage(targetChannel, message, tags); } else { chat.sendRaw(message); } @@ -133,9 +129,9 @@ public boolean sendMessage(final String channelToIdentifyChatInstance, final Str * Sends a whisper. * * @param channelToIdentifyChatInstance the channel used to identify which {@link TwitchChat} instance should be used to send the message; the instance must be subscribed to this channel. - * @param toChannel the channel to send the whisper to - * @param message the message to send in the whisper - * @return whether a {@link TwitchChat} instance was identified to send the message from + * @param toChannel the channel to send the whisper to. + * @param message the message to send in the whisper. + * @return whether a {@link TwitchChat} instance was identified to send the message from. * @throws NullPointerException if the identified {@link TwitchChat} does not have a valid chatCredential */ public boolean sendPrivateMessage(final String channelToIdentifyChatInstance, final String toChannel, final String message) { diff --git a/chat/src/main/java/com/github/twitch4j/chat/util/ChatCrowdChant.java b/chat/src/main/java/com/github/twitch4j/chat/util/ChatCrowdChant.java index c65d8a59c..c1cd2520a 100644 --- a/chat/src/main/java/com/github/twitch4j/chat/util/ChatCrowdChant.java +++ b/chat/src/main/java/com/github/twitch4j/chat/util/ChatCrowdChant.java @@ -1,6 +1,6 @@ package com.github.twitch4j.chat.util; -import com.github.twitch4j.chat.TwitchChat; +import com.github.twitch4j.chat.ITwitchChat; import com.github.twitch4j.chat.events.channel.IRCMessageEvent; import com.github.twitch4j.common.annotation.Unofficial; import com.github.twitch4j.common.util.CryptoUtils; @@ -53,7 +53,7 @@ public class ChatCrowdChant { * @param chat an authenticated TwitchChat instance. */ @Unofficial - public void participate(TwitchChat chat) { + public void participate(ITwitchChat chat) { Map tags = new LinkedHashMap<>(); tags.put(NONCE_TAG_NAME, CryptoUtils.generateNonce(32)); tags.put(CHANT_MSG_ID_TAG_NAME, getMessageId());