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: implement beta charity api #628

Merged
merged 5 commits into from Aug 30, 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
Expand Up @@ -15,6 +15,7 @@ public enum TwitchScopes {
HELIX_BITS_READ("bits:read"),
HELIX_CLIPS_EDIT("clips:edit"),
HELIX_CHANNEL_BROADCAST_MANAGE("channel:manage:broadcast"),
HELIX_CHANNEL_CHARITY_READ("channel:read:charity"),
HELIX_CHANNEL_COMMERCIALS_EDIT("channel:edit:commercial"),
HELIX_CHANNEL_EXTENSION_MANAGE("channel:manage:extensions"),
HELIX_CHANNEL_EDITORS_READ("channel:read:editors"),
Expand Down
Expand Up @@ -78,6 +78,7 @@ public IRCEventHandler(TwitchChat twitchChat) {
eventManager.onEvent("twitch4j-chat-roomstate-trigger", IRCMessageEvent.class, this::onChannelState);
eventManager.onEvent("twitch4j-chat-gift-trigger", IRCMessageEvent.class, this::onGiftReceived);
eventManager.onEvent("twitch4j-chat-payforward-trigger", IRCMessageEvent.class, this::onPayForward);
eventManager.onEvent("twitch4j-chat-charity-trigger", IRCMessageEvent.class, this::onCharityDonation);
eventManager.onEvent("twitch4j-chat-raid-trigger", IRCMessageEvent.class, this::onRaid);
eventManager.onEvent("twitch4j-chat-unraid-trigger", IRCMessageEvent.class, this::onUnraid);
eventManager.onEvent("twitch4j-chat-rewardgift-trigger", IRCMessageEvent.class, this::onRewardGift);
Expand Down Expand Up @@ -348,6 +349,13 @@ public void onPayForward(IRCMessageEvent event) {
}
}

@Unofficial
public void onCharityDonation(IRCMessageEvent event) {
if ("USERNOTICE".equals(event.getCommandType()) && "charitydonation".equalsIgnoreCase(event.getTags().get("msg-id"))) {
eventManager.publish(new CharityDonationEvent(event));
}
}

/**
* ChatChannel Raid Event (receiving)
* @param event IRCMessageEvent
Expand Down
@@ -0,0 +1,60 @@
package com.github.twitch4j.chat.events.channel;

import com.github.twitch4j.chat.events.AbstractChannelEvent;
import com.github.twitch4j.common.annotation.Unofficial;
import com.github.twitch4j.common.enums.CommandPermission;
import com.github.twitch4j.common.events.domain.EventUser;
import com.github.twitch4j.common.util.DonationAmount;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.jetbrains.annotations.NotNull;

import java.util.Set;

@Value
@Unofficial
@EqualsAndHashCode(callSuper = true)
public class CharityDonationEvent extends AbstractChannelEvent {

@NotNull
@EqualsAndHashCode.Exclude
IRCMessageEvent messageEvent;

String userId;

String userLogin;

String userName;

Set<CommandPermission> badges;

String charityName;

DonationAmount amount;

String systemMessage;

public CharityDonationEvent(@NotNull IRCMessageEvent rawEvent) {
super(rawEvent.getChannel());
this.messageEvent = rawEvent;
this.userId = rawEvent.getTagValue("user-id").orElse(null);
this.userLogin = rawEvent.getTagValue("login").orElse(null);
this.userName = rawEvent.getTagValue("display-name").orElse(userLogin);
this.badges = rawEvent.getClientPermissions();
this.charityName = rawEvent.getTagValue("msg-param-charity-name").orElse(null);

Long amount = Long.parseLong(rawEvent.getTags().get("msg-param-donation-amount"));
String currency = rawEvent.getTagValue("msg-param-donation-currency").orElse("USD");
Integer decimals = Integer.parseInt(rawEvent.getTags().getOrDefault("msg-param-exponent", "2"));
this.amount = new DonationAmount(amount, decimals, currency);

this.systemMessage = rawEvent.getTagValue("system-msg").orElseGet(
() -> String.format("%s donated %s %s to support %s", userName, currency, this.amount.getParsedValue().toPlainString(), charityName)
);
}

public EventUser getUser() {
return new EventUser(userId, userLogin);
}

}
@@ -0,0 +1,58 @@
package com.github.twitch4j.common.util;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.math.BigDecimal;
import java.util.Currency;

@Data
@Setter(AccessLevel.PRIVATE)
@NoArgsConstructor
@AllArgsConstructor
public class DonationAmount {

/**
* The monetary amount.
* <p>
* The amount is specified in the currency’s minor unit.
* <p>
* For example, the minor units for USD is cents, so if the amount is $5.50 USD, value is set to 550.
*/
private Long value;

/**
* The number of decimal places used by the currency.
* <p>
* For example, USD uses two decimal places.
*/
private Integer decimalPlaces;

/**
* The ISO-4217 three-letter currency code that identifies the type of currency in {@link #getValue()}.
*/
private String currency;

/**
* The {@link Currency} corresponding to the ISO-4217 code contained in {@link #getCurrency()}.
*/
@JsonIgnore
@Getter(lazy = true)
@EqualsAndHashCode.Exclude
private final Currency parsedCurrency = Currency.getInstance(getCurrency());

/**
* The donation amount, with the appropriate decimals, based on {@link #getValue()}.
*/
@JsonIgnore
@Getter(lazy = true)
@EqualsAndHashCode.Exclude
private final BigDecimal parsedValue = BigDecimal.valueOf(getValue(), getDecimalPlaces());

}
@@ -0,0 +1,12 @@
package com.github.twitch4j.eventsub.condition;

import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;

@SuperBuilder
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Jacksonized
public class ChannelCharityCampaignCondition extends ChannelEventSubCondition {}
@@ -0,0 +1,30 @@
package com.github.twitch4j.eventsub.events;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.twitch4j.common.util.DonationAmount;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Data
@Setter(AccessLevel.PRIVATE)
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class ChannelCharityDonateEvent extends EventSubUserChannelEvent {

/**
* An ID that uniquely identifies the charity campaign.
*/
@JsonProperty("id")
private String campaignId;

/**
* An object that contains the amount of the user’s donation.
*/
private DonationAmount amount;

}
@@ -0,0 +1,39 @@
package com.github.twitch4j.eventsub.subscriptions;

import com.github.twitch4j.common.annotation.Unofficial;
import com.github.twitch4j.eventsub.condition.ChannelCharityCampaignCondition;
import com.github.twitch4j.eventsub.events.ChannelCharityDonateEvent;

/**
* Channel Charity Campaign Donate
* <p>
* Sends an event notification when a user donates to the broadcaster’s charity campaign.
* <p>
* This subscription type is currently in open beta.
*
* @see com.github.twitch4j.auth.domain.TwitchScopes#HELIX_CHANNEL_CHARITY_READ
*/
@Unofficial
public class ChannelCharityDonateType implements SubscriptionType<ChannelCharityCampaignCondition, ChannelCharityCampaignCondition.ChannelCharityCampaignConditionBuilder<?, ?>, ChannelCharityDonateEvent> {

@Override
public String getName() {
return "channel.charity_campaign.donate";
}

@Override
public String getVersion() {
return "1";
}

@Override
public ChannelCharityCampaignCondition.ChannelCharityCampaignConditionBuilder<?, ?> getConditionBuilder() {
return ChannelCharityCampaignCondition.builder();
}

@Override
public Class<ChannelCharityDonateEvent> getEventClass() {
return ChannelCharityDonateEvent.class;
}

}
Expand Up @@ -12,7 +12,9 @@
@UtilityClass
public class SubscriptionTypes {
private final Map<String, SubscriptionType<?, ?, ?>> SUBSCRIPTION_TYPES;

public final ChannelBanType CHANNEL_BAN;
@Unofficial public final ChannelCharityDonateType CHANNEL_CHARITY_DONATE;
public final ChannelCheerType CHANNEL_CHEER;
public final ChannelFollowType CHANNEL_FOLLOW;
public final ChannelGoalBeginType CHANNEL_GOAL_BEGIN;
Expand Down Expand Up @@ -58,6 +60,7 @@ public class SubscriptionTypes {
SUBSCRIPTION_TYPES = Collections.unmodifiableMap(
Stream.of(
CHANNEL_BAN = new ChannelBanType(),
CHANNEL_CHARITY_DONATE = new ChannelCharityDonateType(),
CHANNEL_CHEER = new ChannelCheerType(),
CHANNEL_FOLLOW = new ChannelFollowType(),
CHANNEL_GOAL_BEGIN = new ChannelGoalBeginType(),
Expand Down
Expand Up @@ -300,7 +300,7 @@ default PubSubSubscription listenForLeaderboardMonthlyEvents(OAuth2Credential cr

@Unofficial
default PubSubSubscription listenForLeaderboardAllTimeEvents(OAuth2Credential credential, String channelId) {
return listenForLeaderboardEvents(credential, channelId, "ALLTIME");
return listenForLeaderboardEvents(credential, channelId, "ALLTIME");
}

@Unofficial
Expand Down Expand Up @@ -355,6 +355,11 @@ default PubSubSubscription listenForChannelExtensionEvents(OAuth2Credential cred
return listenOnTopic(PubSubType.LISTEN, credential, "channel-ext-v1." + channelId);
}

@Unofficial
default PubSubSubscription listenForCharityCampaignEvents(OAuth2Credential credential, String channelId) {
return listenOnTopic(PubSubType.LISTEN, credential, "charity-campaign-donation-events-v1." + channelId);
}

@Unofficial
@Deprecated
default PubSubSubscription listenForExtensionControlEvents(OAuth2Credential credential, String channelId) {
Expand Down
Expand Up @@ -22,6 +22,8 @@
import com.github.twitch4j.pubsub.domain.ChannelPointsRedemption;
import com.github.twitch4j.pubsub.domain.ChannelPointsReward;
import com.github.twitch4j.pubsub.domain.ChannelTermsAction;
import com.github.twitch4j.pubsub.domain.CharityCampaignStatus;
import com.github.twitch4j.pubsub.domain.CharityDonationData;
import com.github.twitch4j.pubsub.domain.ChatModerationAction;
import com.github.twitch4j.pubsub.domain.CheerbombData;
import com.github.twitch4j.pubsub.domain.ClaimData;
Expand Down Expand Up @@ -73,6 +75,8 @@
import com.github.twitch4j.pubsub.events.ChannelTermsEvent;
import com.github.twitch4j.pubsub.events.ChannelUnbanRequestCreateEvent;
import com.github.twitch4j.pubsub.events.ChannelUnbanRequestUpdateEvent;
import com.github.twitch4j.pubsub.events.CharityCampaignDonationEvent;
import com.github.twitch4j.pubsub.events.CharityCampaignStatusEvent;
import com.github.twitch4j.pubsub.events.ChatModerationEvent;
import com.github.twitch4j.pubsub.events.CheerbombEvent;
import com.github.twitch4j.pubsub.events.ClaimAvailableEvent;
Expand Down Expand Up @@ -511,7 +515,20 @@ protected void onTextMessage(String text) {
log.warn("Unparsable Message: " + message.getType() + "|" + message.getData());
break;
}

} else if ("charity-campaign-donation-events-v1".equals(topicName) && topicParts.length > 1) {
switch (type) {
case "charity_campaign_donation":
CharityDonationData donation = TypeConvert.jsonToObject(rawMessage, CharityDonationData.class);
eventManager.publish(new CharityCampaignDonationEvent(lastTopicIdentifier, donation));
break;
case "charity_campaign_status":
CharityCampaignStatus status = TypeConvert.jsonToObject(rawMessage, CharityCampaignStatus.class);
eventManager.publish(new CharityCampaignStatusEvent(lastTopicIdentifier, status));
break;
default:
log.warn("Unparsable Message: " + message.getType() + "|" + message.getData());
break;
}
} else if ("chat_moderator_actions".equals(topicName) && topicParts.length > 1) {
switch (type) {
case "moderation_action":
Expand Down
@@ -0,0 +1,27 @@
package com.github.twitch4j.pubsub.domain;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;

@Data
@Setter(AccessLevel.PRIVATE)
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class CharityCampaignStatus extends CharityDonationData {

@Accessors(fluent = true)
@JsonProperty("is_active")
private Boolean isActive;

@JsonProperty("campaign_name")
private String charityName;

@JsonProperty("campaign_description")
private String charityDescription;

}
@@ -0,0 +1,32 @@
package com.github.twitch4j.pubsub.domain;

import com.github.twitch4j.common.util.DonationAmount;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;

import java.util.Optional;

@Data
@Setter(AccessLevel.PRIVATE)
public class CharityDonationData {

private String campaignId;

private String campaignCurrency = "USD";

private Long donationTotal;

private Long goalTarget;

public DonationAmount getTotal() {
return new DonationAmount(donationTotal, 2, campaignCurrency);
}

public Optional<DonationAmount> getTarget() {
return Optional.ofNullable(goalTarget)
.filter(l -> l > 0)
.map(target -> new DonationAmount(target, 2, campaignCurrency));
}

}
@@ -0,0 +1,13 @@
package com.github.twitch4j.pubsub.events;

import com.github.twitch4j.common.events.TwitchEvent;
import com.github.twitch4j.pubsub.domain.CharityDonationData;
import lombok.EqualsAndHashCode;
import lombok.Value;

@Value
@EqualsAndHashCode(callSuper = false)
public class CharityCampaignDonationEvent extends TwitchEvent {
String channelId;
CharityDonationData data;
}