Skip to content

Commit

Permalink
feat(helix): add content classification and branded content management (
Browse files Browse the repository at this point in the history
  • Loading branch information
iProdigy committed Jul 31, 2023
1 parent 8c78266 commit 6a4b97b
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 7 deletions.
Expand Up @@ -2,60 +2,81 @@

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 <a href="https://safety.twitch.tv/s/article/Content-Classification-Guidelines?language=en_US">Official Guidelines</a>
*/
@RequiredArgsConstructor
public enum ContentClassification {

/**
* Excessive tobacco glorification or promotion, any marijuana consumption/use,
* 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.
* <p>
* 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<String, ContentClassification> 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);
}
}
Expand Up @@ -1363,6 +1363,23 @@ HystrixCommand<ClipList> 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<ContentClassificationList> 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
*
Expand Down
@@ -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 {

/**
Expand Down Expand Up @@ -76,4 +88,34 @@ public class ChannelInformation {
*/
private List<String> tags;

/**
* The CCLs applied to the channel.
*/
@Singular
@JsonSerialize(using = ContentClassificationStateListSerializer.class)
private Collection<ContentClassificationState> 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<ContentClassification> labels) {
if (labels == null) return;
this.contentClassificationLabels = new ArrayList<>(labels.size());
labels.forEach(label -> contentClassificationLabels.add(new ContentClassificationState(label, true)));
}

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

}
@@ -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<ContentClassificationInfo> labels;

}
@@ -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;

}
@@ -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<ContentClassificationState>} 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<Collection<ContentClassificationState>> {
@Override
public void serialize(Collection<ContentClassificationState> 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();
}
}
}
@@ -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));
}

}

0 comments on commit 6a4b97b

Please sign in to comment.