Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support experimental direct cheer usernotice #566

Merged
merged 2 commits into from May 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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());
iProdigy marked this conversation as resolved.
Show resolved Hide resolved
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);
}

}