Skip to content

Commit

Permalink
feat: support experimental direct cheer usernotice (#566)
Browse files Browse the repository at this point in the history
  • Loading branch information
iProdigy committed May 15, 2022
1 parent c2a4fc0 commit 3ba1e1f
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 37 deletions.
@@ -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;

Expand All @@ -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()
Expand All @@ -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) {
Expand Down
Expand Up @@ -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);
Expand Down Expand Up @@ -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
*
Expand Down
Expand Up @@ -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 {

Expand All @@ -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
Expand Down Expand Up @@ -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<AutoModFlag> 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<AutoModFlag> flags) {
super(channel);
this.messageEvent = event;
this.user = user;
this.message = message;
this.bits = bits;
this.subscriberMonths = subscriberMonths;
this.subscriptionTier = subscriptionTier;
this.flags = flags;
}
}
}
@@ -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 <a href="https://help.twitch.tv/s/article/cheering-experiment-2022">Twitch Information</a>
*/
@Value
@Unofficial
@EqualsAndHashCode(callSuper = true)
public class DirectCheerEvent extends CheerEvent {

private static final Map<String, Currency> CURRENCY_MAP;

/**
* Creator receives 80% of the amount after fees, during the experiment.
*
* @see <a href="https://help.twitch.tv/s/article/cheering-experiment-2022?language=en_US#StreamerFAQ">Help Article</a>
*/
private static final float CREATOR_REVENUE_SPLIT = 0.8F;

/**
* The amount of the direct cheer.
* <p>
* For example, $1 = 100
* <p>
* 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.
* <p>
* For example: DisplayName Cheered with $50
* <p>
* 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.
* <p>
* 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(0),
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<Currency> currencies = Currency.getAvailableCurrencies();

final Map<String, Currency> 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);
}

}

0 comments on commit 3ba1e1f

Please sign in to comment.