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)); + } + +}