From 6a4b97b3848492fa6ba9662b2a16136d7abfba31 Mon Sep 17 00:00:00 2001
From: iProdigy <8106344+iProdigy@users.noreply.github.com>
Date: Mon, 31 Jul 2023 14:08:38 -0500
Subject: [PATCH] feat(helix): add content classification and branded content
management (#811)
---
.../domain/ContentClassification.java | 35 ++++++++++---
.../github/twitch4j/helix/TwitchHelix.java | 17 +++++++
.../helix/domain/ChannelInformation.java | 42 ++++++++++++++++
.../domain/ContentClassificationInfo.java | 38 ++++++++++++++
.../domain/ContentClassificationList.java | 25 ++++++++++
.../domain/ContentClassificationState.java | 32 ++++++++++++
...tentClassificationStateListSerializer.java | 36 +++++++++++++
.../helix/domain/ChannelInformationTest.java | 50 +++++++++++++++++++
8 files changed, 268 insertions(+), 7 deletions(-)
create mode 100644 rest-helix/src/main/java/com/github/twitch4j/helix/domain/ContentClassificationInfo.java
create mode 100644 rest-helix/src/main/java/com/github/twitch4j/helix/domain/ContentClassificationList.java
create mode 100644 rest-helix/src/main/java/com/github/twitch4j/helix/domain/ContentClassificationState.java
create mode 100644 rest-helix/src/main/java/com/github/twitch4j/helix/interceptor/ContentClassificationStateListSerializer.java
create mode 100644 rest-helix/src/test/java/com/github/twitch4j/helix/domain/ChannelInformationTest.java
diff --git a/eventsub-common/src/main/java/com/github/twitch4j/eventsub/domain/ContentClassification.java b/eventsub-common/src/main/java/com/github/twitch4j/eventsub/domain/ContentClassification.java
index 65b142000..c990c375d 100644
--- a/eventsub-common/src/main/java/com/github/twitch4j/eventsub/domain/ContentClassification.java
+++ b/eventsub-common/src/main/java/com/github/twitch4j/eventsub/domain/ContentClassification.java
@@ -2,12 +2,19 @@
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.github.twitch4j.util.EnumUtil;
+import lombok.RequiredArgsConstructor;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
/**
* Content classification tags that indicate that a stream may not be suitable for certain viewers.
*
* @see Official Guidelines
*/
+@RequiredArgsConstructor
public enum ContentClassification {
/**
@@ -15,14 +22,14 @@ public enum ContentClassification {
* legal drug and alcohol induced intoxication, discussions of illegal drugs.
*/
@JsonProperty("DrugsIntoxication")
- DRUGS,
+ DRUGS("DrugsIntoxication"),
/**
* Participating in online or in-person gambling, poker or fantasy sports,
* that involve the exchange of real money.
*/
@JsonProperty("Gambling")
- GAMBLING,
+ GAMBLING("Gambling"),
/**
* Games that are rated Mature or less suitable for a younger audience.
@@ -30,32 +37,46 @@ public enum ContentClassification {
* This tag is automatically applied based on the stream category.
*/
@JsonProperty("MatureGame")
- MATURE_GAME,
+ MATURE_GAME("MatureGame"),
/**
* Prolonged, and repeated use of obscenities, profanities, and vulgarities,
* especially as a regular part of speech.
*/
@JsonProperty("ProfanityVulgarity")
- PROFANITY,
+ PROFANITY("ProfanityVulgarity"),
/**
* Content that focuses on sexualized physical attributes and activities, sexual topics, or experiences.
*/
@JsonProperty("SexualThemes")
- SEXUAL,
+ SEXUAL("SexualThemes"),
/**
* Simulations and/or depictions of realistic violence, gore, extreme injury, or death.
*/
@JsonProperty("ViolentGraphic")
- VIOLENCE,
+ VIOLENCE("ViolentGraphic"),
/**
* The channel has a content classification label that is unrecognized by the library;
* Please file an issue on our GitHub repository.
*/
@JsonEnumDefaultValue
- UNKNOWN;
+ UNKNOWN("Unknown");
+
+ private static final Map MAPPINGS = EnumUtil.buildMapping(values());
+
+ private final String twitchString;
+
+ @Override
+ public String toString() {
+ return this.twitchString;
+ }
+ @NotNull
+ @ApiStatus.Internal
+ public static ContentClassification parse(@NotNull String id) {
+ return MAPPINGS.getOrDefault(id, UNKNOWN);
+ }
}
diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/TwitchHelix.java b/rest-helix/src/main/java/com/github/twitch4j/helix/TwitchHelix.java
index 841d24235..b280aa3ac 100644
--- a/rest-helix/src/main/java/com/github/twitch4j/helix/TwitchHelix.java
+++ b/rest-helix/src/main/java/com/github/twitch4j/helix/TwitchHelix.java
@@ -1363,6 +1363,23 @@ HystrixCommand getClips(
@Param("ended_at") Instant endedAt
);
+ /**
+ * Gets information about Twitch content classification labels.
+ *
+ * @param authToken App Access Token or User Access Token.
+ * @param locale Locale for the Content Classification Labels. Default: "en-US".
+ * Supported locales: "bg-BG", "cs-CZ", "da-DK", "da-DK", "de-DE", "el-GR", "en-GB", "en-US", "es-ES", "es-MX",
+ * "fi-FI", "fr-FR", "hu-HU", "it-IT", "ja-JP", "ko-KR", "nl-NL", "no-NO", "pl-PL", "pt-BT", "pt-PT",
+ * "ro-RO", "ru-RU", "sk-SK", "sv-SE", "th-TH", "tr-TR", "vi-VN", "zh-CN", "zh-TW"
+ * @return ContentClassificationList
+ */
+ @RequestLine("GET /content_classification_labels?locale={locale}")
+ @Headers("Authorization: Bearer {token}")
+ HystrixCommand getContentClassificationLabels(
+ @Param("token") String authToken,
+ @Param("locale") String locale
+ );
+
/**
* Creates a URL where you can upload a manifest file and notify users that they have an entitlement
*
diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ChannelInformation.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ChannelInformation.java
index 37d8aec84..6c9476d8c 100644
--- a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ChannelInformation.java
+++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ChannelInformation.java
@@ -1,20 +1,32 @@
package com.github.twitch4j.helix.domain;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.github.twitch4j.eventsub.domain.ContentClassification;
+import com.github.twitch4j.helix.interceptor.ContentClassificationStateListSerializer;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Setter;
+import lombok.Singular;
import lombok.With;
+import lombok.experimental.Accessors;
import org.jetbrains.annotations.Nullable;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
@Data
@With
@Setter(AccessLevel.PRIVATE)
+@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChannelInformation {
/**
@@ -76,4 +88,34 @@ public class ChannelInformation {
*/
private List tags;
+ /**
+ * The CCLs applied to the channel.
+ */
+ @Singular
+ @JsonSerialize(using = ContentClassificationStateListSerializer.class)
+ private Collection contentClassificationLabels;
+
+ /**
+ * Whether the channel has branded content.
+ */
+ @Accessors(fluent = true)
+ @JsonProperty("is_branded_content")
+ private Boolean isBrandedContent;
+
+ /**
+ * Converts the {@code content_classification_labels} list from {@link com.github.twitch4j.helix.TwitchHelix#getChannelInformation(String, List)}
+ * into a list of {@link ContentClassificationState}, so that {@link ChannelInformation} can be passed to
+ * {@link com.github.twitch4j.helix.TwitchHelix#updateChannelInformation(String, String, ChannelInformation)},
+ * since the PATCH endpoint expects an array of objects (with {@code is_enabled} boolean flag)
+ * rather than an array of strings (that the GET endpoint yields).
+ *
+ * @param labels collection of {@link ContentClassification}'s
+ */
+ @JsonProperty("content_classification_labels")
+ private void setContentClassificationLabels(Collection labels) {
+ if (labels == null) return;
+ this.contentClassificationLabels = new ArrayList<>(labels.size());
+ labels.forEach(label -> contentClassificationLabels.add(new ContentClassificationState(label, true)));
+ }
+
}
diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ContentClassificationInfo.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ContentClassificationInfo.java
new file mode 100644
index 000000000..e7a7a4966
--- /dev/null
+++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ContentClassificationInfo.java
@@ -0,0 +1,38 @@
+package com.github.twitch4j.helix.domain;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.github.twitch4j.eventsub.domain.ContentClassification;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Data
+@Setter(AccessLevel.PRIVATE)
+@NoArgsConstructor
+public class ContentClassificationInfo {
+
+ /**
+ * Unique identifier for the CCL.
+ */
+ private String id;
+
+ /**
+ * Localized description of the CCL.
+ */
+ private String description;
+
+ /**
+ * Localized name of the CCL.
+ */
+ private String name;
+
+ /**
+ * @return {@link #getId()} parsed as {@link ContentClassification}.
+ */
+ @JsonIgnore
+ public ContentClassification getLabel() {
+ return ContentClassification.parse(id);
+ }
+
+}
diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ContentClassificationList.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ContentClassificationList.java
new file mode 100644
index 000000000..9f622016e
--- /dev/null
+++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ContentClassificationList.java
@@ -0,0 +1,25 @@
+package com.github.twitch4j.helix.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * Information about the available content classification labels.
+ */
+@Data
+@Setter(AccessLevel.PRIVATE)
+@NoArgsConstructor
+public class ContentClassificationList {
+
+ /**
+ * The list of CCLs available.
+ */
+ @JsonProperty("data")
+ private List labels;
+
+}
diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ContentClassificationState.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ContentClassificationState.java
new file mode 100644
index 000000000..9c6aaf425
--- /dev/null
+++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ContentClassificationState.java
@@ -0,0 +1,32 @@
+package com.github.twitch4j.helix.domain;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.github.twitch4j.eventsub.domain.ContentClassification;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.With;
+import org.jetbrains.annotations.NotNull;
+
+@Data
+@With
+@Setter(AccessLevel.PRIVATE)
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@AllArgsConstructor
+public class ContentClassificationState {
+
+ /**
+ * ID of the Content Classification Labels that must be added/removed from the channel.
+ */
+ @NotNull
+ private ContentClassification id;
+
+ /**
+ * Whether the label should be enabled (true) or disabled for the channel.
+ */
+ @JsonProperty("is_enabled")
+ private boolean isEnabled;
+
+}
diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/interceptor/ContentClassificationStateListSerializer.java b/rest-helix/src/main/java/com/github/twitch4j/helix/interceptor/ContentClassificationStateListSerializer.java
new file mode 100644
index 000000000..b8e953fd0
--- /dev/null
+++ b/rest-helix/src/main/java/com/github/twitch4j/helix/interceptor/ContentClassificationStateListSerializer.java
@@ -0,0 +1,36 @@
+package com.github.twitch4j.helix.interceptor;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.github.twitch4j.eventsub.domain.ContentClassification;
+import com.github.twitch4j.helix.domain.ChannelInformation;
+import com.github.twitch4j.helix.domain.ContentClassificationState;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * Serializes {@code Collection} within {@link com.github.twitch4j.helix.domain.ChannelInformation}
+ * for {@link com.github.twitch4j.helix.TwitchHelix#updateChannelInformation(String, String, ChannelInformation)}
+ * where {@link ContentClassification#MATURE_GAME} is not included in {@link ChannelInformation#getContentClassificationLabels()}
+ * since this label is controlled by the game category (rather than the user).
+ */
+@ApiStatus.Internal
+public class ContentClassificationStateListSerializer extends JsonSerializer> {
+ @Override
+ public void serialize(Collection value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+ if (value != null) {
+ gen.writeStartArray();
+ for (ContentClassificationState ccl : value) {
+ if (ccl == null) continue;
+ if (ccl.getId() == ContentClassification.MATURE_GAME) continue;
+ gen.writeObject(ccl);
+ }
+ gen.writeEndArray();
+ } else {
+ gen.writeNull();
+ }
+ }
+}
diff --git a/rest-helix/src/test/java/com/github/twitch4j/helix/domain/ChannelInformationTest.java b/rest-helix/src/test/java/com/github/twitch4j/helix/domain/ChannelInformationTest.java
new file mode 100644
index 000000000..f3eced46d
--- /dev/null
+++ b/rest-helix/src/test/java/com/github/twitch4j/helix/domain/ChannelInformationTest.java
@@ -0,0 +1,50 @@
+package com.github.twitch4j.helix.domain;
+
+import com.github.twitch4j.common.util.TypeConvert;
+import com.github.twitch4j.eventsub.domain.ContentClassification;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ChannelInformationTest {
+
+ @Test
+ void deserializeLabels() {
+ String json = "{\"content_classification_labels\":[\"Gambling\",\"DrugsIntoxication\",\"MatureGame\"],\"is_branded_content\":true}";
+ ChannelInformation info = TypeConvert.jsonToObject(json, ChannelInformation.class);
+ assertNotNull(info);
+ assertEquals(
+ Arrays.asList(
+ new ContentClassificationState(ContentClassification.GAMBLING, true),
+ new ContentClassificationState(ContentClassification.DRUGS, true),
+ new ContentClassificationState(ContentClassification.MATURE_GAME, true)
+ ),
+ info.getContentClassificationLabels()
+ );
+ assertTrue(info.isBrandedContent());
+ }
+
+ @Test
+ void serializeLabels() {
+ ChannelInformation info = ChannelInformation.builder()
+ .contentClassificationLabel(new ContentClassificationState(ContentClassification.PROFANITY, true))
+ .contentClassificationLabel(new ContentClassificationState(ContentClassification.SEXUAL, false))
+ .build();
+ String expected = "{\"content_classification_labels\":[{\"id\":\"ProfanityVulgarity\",\"is_enabled\":true},{\"id\":\"SexualThemes\",\"is_enabled\":false}]}";
+ assertEquals(expected, TypeConvert.objectToJson(info));
+ }
+
+ @Test
+ void serializeWithoutMature() {
+ ChannelInformation info = ChannelInformation.builder()
+ .contentClassificationLabel(new ContentClassificationState(ContentClassification.MATURE_GAME, true))
+ .build();
+ String expected = "{\"content_classification_labels\":[]}";
+ assertEquals(expected, TypeConvert.objectToJson(info));
+ }
+
+}