Skip to content

Commit

Permalink
feat: implement beta charity api (#628)
Browse files Browse the repository at this point in the history
* feat(eventsub): add charity campaign donate type

* feat(helix): add get charity campaign endpoint

* feat(chat): add charity donation event

* feat(pubsub): add unofficial charity topic

* refactor(pubsub): avoid wildcard import
  • Loading branch information
iProdigy committed Aug 30, 2022
1 parent edaf9a0 commit 2bef42d
Show file tree
Hide file tree
Showing 17 changed files with 446 additions and 2 deletions.
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 @@ -514,7 +518,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;
}

0 comments on commit 2bef42d

Please sign in to comment.