From 02d34ad2038a27986d93aed07ea2902b1eba3939 Mon Sep 17 00:00:00 2001 From: Sidd Date: Thu, 5 May 2022 13:55:58 -0700 Subject: [PATCH 1/2] feat: support new direct cheer usernotice --- .../chat/events/AbstractChannelEvent.java | 37 +++-- .../twitch4j/chat/events/IRCEventHandler.java | 13 ++ .../chat/events/channel/CheerEvent.java | 47 +++--- .../chat/events/channel/DirectCheerEvent.java | 137 ++++++++++++++++++ 4 files changed, 197 insertions(+), 37 deletions(-) create mode 100644 chat/src/main/java/com/github/twitch4j/chat/events/channel/DirectCheerEvent.java diff --git a/chat/src/main/java/com/github/twitch4j/chat/events/AbstractChannelEvent.java b/chat/src/main/java/com/github/twitch4j/chat/events/AbstractChannelEvent.java index 1ec44d8b7..af6dd9d1e 100644 --- a/chat/src/main/java/com/github/twitch4j/chat/events/AbstractChannelEvent.java +++ b/chat/src/main/java/com/github/twitch4j/chat/events/AbstractChannelEvent.java @@ -1,9 +1,12 @@ package com.github.twitch4j.chat.events; import com.github.twitch4j.common.events.domain.EventChannel; +import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import java.time.Duration; @@ -12,30 +15,32 @@ */ @Data @Getter +@Setter(AccessLevel.PRIVATE) +@NoArgsConstructor @EqualsAndHashCode(callSuper = false) public class AbstractChannelEvent extends TwitchEvent { - /** - * Event Channel - */ - private final EventChannel channel; + /** + * Event Channel + */ + private EventChannel channel; - /** - * Event Constructor - * - * @param channel The channel that this event originates from. - */ - public AbstractChannelEvent(EventChannel channel) { - super(); - this.channel = channel; - } + /** + * Event Constructor + * + * @param channel The channel that this event originates from. + */ + public AbstractChannelEvent(EventChannel channel) { + super(); + this.channel = channel; + } /** * Timeout a user * - * @param user username + * @param user username * @param duration duration - * @param reason reason + * @param reason reason */ public void timeout(String user, Duration duration, String reason) { StringBuilder sb = new StringBuilder() @@ -49,7 +54,7 @@ public void timeout(String user, Duration duration, String reason) { /** * Ban a user * - * @param user username + * @param user username * @param reason reason */ public void ban(String user, String reason) { diff --git a/chat/src/main/java/com/github/twitch4j/chat/events/IRCEventHandler.java b/chat/src/main/java/com/github/twitch4j/chat/events/IRCEventHandler.java index d2194c52c..1c57d1985 100644 --- a/chat/src/main/java/com/github/twitch4j/chat/events/IRCEventHandler.java +++ b/chat/src/main/java/com/github/twitch4j/chat/events/IRCEventHandler.java @@ -62,6 +62,7 @@ public IRCEventHandler(TwitchChat twitchChat) { eventManager.onEvent("twitch4j-chat-announcement-trigger", IRCMessageEvent.class, this::onAnnouncement); eventManager.onEvent("twitch4j-chat-bits-badge-trigger", IRCMessageEvent.class, this::onBitsBadgeTier); eventManager.onEvent("twitch4j-chat-cheer-trigger", IRCMessageEvent.class, this::onChannelCheer); + eventManager.onEvent("twitch4j-chat-direct-cheer-trigger", IRCMessageEvent.class, this::onDirectCheer); eventManager.onEvent("twitch4j-chat-sub-trigger", IRCMessageEvent.class, this::onChannelSubscription); eventManager.onEvent("twitch4j-chat-clearchat-trigger", IRCMessageEvent.class, this::onClearChat); eventManager.onEvent("twitch4j-chat-clearmsg-trigger", IRCMessageEvent.class, this::onClearMsg); @@ -177,6 +178,18 @@ public void onChannelCheer(IRCMessageEvent event) { } } + /** + * ChatChannel Direct Cheer (Currency) Event + * + * @param event IRCMessageEvent + */ + @Unofficial + public void onDirectCheer(IRCMessageEvent event) { + if ("USERNOTICE".equals(event.getCommandType()) && "midnightsquid".equals(event.getTags().get("msg-id"))) { + eventManager.publish(new DirectCheerEvent(event)); + } + } + /** * ChatChannel Subscription Event * diff --git a/chat/src/main/java/com/github/twitch4j/chat/events/channel/CheerEvent.java b/chat/src/main/java/com/github/twitch4j/chat/events/channel/CheerEvent.java index bc38fbeb0..e57a4fd87 100644 --- a/chat/src/main/java/com/github/twitch4j/chat/events/channel/CheerEvent.java +++ b/chat/src/main/java/com/github/twitch4j/chat/events/channel/CheerEvent.java @@ -5,17 +5,22 @@ import com.github.twitch4j.common.annotation.Unofficial; import com.github.twitch4j.common.events.domain.EventChannel; import com.github.twitch4j.common.events.domain.EventUser; +import lombok.AccessLevel; +import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.Value; +import lombok.NoArgsConstructor; +import lombok.Setter; import java.util.List; /** * This event gets called when a user receives bits. */ -@Value +@Data @Getter +@Setter(AccessLevel.PRIVATE) +@NoArgsConstructor @EqualsAndHashCode(callSuper = false) public class CheerEvent extends AbstractChannelEvent implements ReplyableEvent { @@ -24,20 +29,20 @@ public class CheerEvent extends AbstractChannelEvent implements ReplyableEvent { */ IRCMessageEvent messageEvent; - /** - * Event Target User - */ - private EventUser user; + /** + * Event Target User + */ + private EventUser user; - /** - * Message - */ - private String message; + /** + * Message + */ + private String message; - /** - * Amount of Bits - */ - private Integer bits; + /** + * Amount of Bits + */ + private Integer bits; /** * The exact number of months the user has been a subscriber, or zero if not subscribed @@ -67,14 +72,14 @@ public class CheerEvent extends AbstractChannelEvent implements ReplyableEvent { * @param subscriptionTier The tier at which the user is subscribed. * @param flags The regions of the message that were flagged by AutoMod. */ - public CheerEvent(IRCMessageEvent event, EventChannel channel, EventUser user, String message, Integer bits, int subscriberMonths, int subscriptionTier, List flags) { - super(channel); - this.messageEvent = event; - this.user = user; - this.message = message; - this.bits = bits; + public CheerEvent(IRCMessageEvent event, EventChannel channel, EventUser user, String message, Integer bits, int subscriberMonths, int subscriptionTier, List flags) { + super(channel); + this.messageEvent = event; + this.user = user; + this.message = message; + this.bits = bits; this.subscriberMonths = subscriberMonths; this.subscriptionTier = subscriptionTier; this.flags = flags; - } + } } diff --git a/chat/src/main/java/com/github/twitch4j/chat/events/channel/DirectCheerEvent.java b/chat/src/main/java/com/github/twitch4j/chat/events/channel/DirectCheerEvent.java new file mode 100644 index 000000000..ab2796cb0 --- /dev/null +++ b/chat/src/main/java/com/github/twitch4j/chat/events/channel/DirectCheerEvent.java @@ -0,0 +1,137 @@ +package com.github.twitch4j.chat.events.channel; + +import com.github.twitch4j.common.annotation.Unofficial; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Value; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Currency; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * This event gets called when a user does a direct cheer in an eligible channel for this experiment. + * + * @see Twitch Information + */ +@Value +@Unofficial +@EqualsAndHashCode(callSuper = true) +public class DirectCheerEvent extends CheerEvent { + + private static final Map CURRENCY_MAP; + + /** + * Creator receives 80% of the amount after fees, during the experiment. + * + * @see Help Article + */ + private static final float CREATOR_REVENUE_SPLIT = 0.8F; + + /** + * The amount of the direct cheer. + *

+ * For example, $1 = 100 + *

+ * Note: this is before adjustment for twitch's revenue split. + * Use {@link #getBits()} for the adjusted value. + */ + Integer amount; + + /** + * The twitch notification text for this event. + *

+ * For example: DisplayName Cheered with $50 + *

+ * Note: this is distinct from the user's message attached to the cheer ({@link #getMessage()}) + */ + @NotNull + String systemMessage; + + /** + * Parses the currency used in this direct cheer, or null if unknown. + * Currently, this always resolves to USD due to the experiment restrictions. + */ + @Nullable + @Getter(lazy = true) + Currency currency = parseCurrency(getSystemMessage()); + + /** + * Parses the monetary value that was directly cheered. + *

+ * For example, $50 is parsed to 50 + */ + @Nullable + @Getter(lazy = true) + BigDecimal monetaryValue = parseValue(getSystemMessage()); + + /** + * Event Constructor + * + * @param event The raw message event. + */ + public DirectCheerEvent(IRCMessageEvent event) { + this( + event, + event.getTagValue("msg-param-amount").map(Integer::parseInt).orElse(null), + event.getTagValue("system-msg").map(String::trim).orElse("") + ); + } + + DirectCheerEvent(IRCMessageEvent event, Integer amount, @NotNull String systemMessage) { + super(event, event.getChannel(), event.getUser(), event.getMessage().orElse(""), Math.round(amount * CREATOR_REVENUE_SPLIT), event.getSubscriberMonths().orElse(0), event.getSubscriptionTier().orElse(0), event.getFlags()); + this.amount = amount; + this.systemMessage = systemMessage; + } + + private static Currency parseCurrency(String systemMsg) { + int start = systemMsg.lastIndexOf(' '); + if (start < 0) return null; // empty message + if (Character.isDigit(systemMsg.charAt(start + 1))) { + start = systemMsg.substring(0, start).lastIndexOf(' '); // workaround for currencies that have a space between symbol and monetary value + } + start = start + 1; // skip the space character + + int end = start + 1; // default symbol has length 1 + for (; end < systemMsg.length(); end++) { + if (Character.isDigit(systemMsg.charAt(end))) + break; // symbol ends before digits start + } + + return CURRENCY_MAP.get(systemMsg.substring(start, end).trim()); + } + + private static BigDecimal parseValue(String systemMsg) { + int start = systemMsg.lastIndexOf(' '); + if (start < 0) return null; // empty message + + for (; start < systemMsg.length(); start++) { + if (Character.isDigit(systemMsg.charAt(start))) + break; // value starts at first digit + } + + int end = systemMsg.indexOf(' ', start); // value ends at next space + if (end < 0) { + end = systemMsg.length(); // or end of string if no spaces remaining + } + + return new BigDecimal(systemMsg.substring(start, end)); + } + + static { + Set currencies = Currency.getAvailableCurrencies(); + + final Map map = new HashMap<>(currencies.size() * 3); + currencies.forEach(c -> map.put(c.getSymbol(), c)); + currencies.forEach(c -> map.putIfAbsent(c.getCurrencyCode(), c)); // future proofing + currencies.forEach(c -> map.putIfAbsent(c.getDisplayName(), c)); // future proofing + + CURRENCY_MAP = Collections.unmodifiableMap(map); + } + +} From 88880b0b65a98830f4d656a7b17ab292f9b6d643 Mon Sep 17 00:00:00 2001 From: Sidd Date: Sun, 15 May 2022 11:01:04 -0700 Subject: [PATCH 2/2] fix: avoid possible npe found in review --- .../github/twitch4j/chat/events/channel/DirectCheerEvent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat/src/main/java/com/github/twitch4j/chat/events/channel/DirectCheerEvent.java b/chat/src/main/java/com/github/twitch4j/chat/events/channel/DirectCheerEvent.java index ab2796cb0..69f58b151 100644 --- a/chat/src/main/java/com/github/twitch4j/chat/events/channel/DirectCheerEvent.java +++ b/chat/src/main/java/com/github/twitch4j/chat/events/channel/DirectCheerEvent.java @@ -78,7 +78,7 @@ public class DirectCheerEvent extends CheerEvent { public DirectCheerEvent(IRCMessageEvent event) { this( event, - event.getTagValue("msg-param-amount").map(Integer::parseInt).orElse(null), + event.getTagValue("msg-param-amount").map(Integer::parseInt).orElse(0), event.getTagValue("system-msg").map(String::trim).orElse("") ); }