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 8b902e7b6..adc49d60d 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 @@ -105,6 +105,40 @@ HystrixCommand getCheermotes( @Param("broadcaster_id") String broadcasterId ); + /** + * Gets a list of Bits products that belongs to an Extension. + * + * @param authToken App Access Token associated with the Extension client ID + * @param includeAll Optional: Whether Bits products that are disabled/expired should be included in the response. Default: false + * @return ExtensionBitsProductList + */ + @RequestLine("GET /bits/extensions?should_include_all={should_include_all}") + @Headers("Authorization: Bearer {token}") + HystrixCommand getExtensionBitsProducts( + @Param("token") String authToken, + @Param("should_include_all") Boolean includeAll + ); + + /** + * Add or update a Bits products that belongs to an Extension. + *

+ * Required body fields: sku, cost.amount, cost.type, display_name. + * Optional fields: in_development, expiration, is_broadcast. + * + * @param authToken App Access Token associated with the Extension client ID + * @param product The extension bits product to add or update + * @return ExtensionBitsProductList + */ + @RequestLine("PUT /bits/extensions") + @Headers({ + "Authorization: Bearer {token}", + "Content-Type: application/json" + }) + HystrixCommand updateExtensionBitsProduct( + @Param("token") String authToken, + ExtensionBitsProduct product + ); + /** * Gets a ranked list of Bits leaderboard information for an authorized broadcaster. * @@ -544,6 +578,246 @@ HystrixCommand getEventSubSubscriptions( @Param("first") Integer limit ); + /** + * Gets information about your Extensions; either the current version or a specified version. + * + * @param jwtToken Signed JWT with role set to "external". + * @param extensionId ID of the Extension. + * @param extensionVersion The specific version of the Extension to return. If not provided, the current version is returned. + * @return ReleasedExtensionList + */ + @RequestLine("GET /extensions?extension_id={extension_id}&extension_version={extension_version}") + @Headers({ + "Authorization: Bearer {token}", + "Client-Id: {extension_id}" + }) + HystrixCommand getExtensions( + @Param("token") String jwtToken, + @Param("extension_id") String extensionId, + @Param("extension_version") String extensionVersion + ); + + /** + * Sends a specified chat message to a specified channel. + *

+ * The message will appear in the channel’s chat as a normal message. + * The “username” of the message is the Extension name. + *

+ * There is a limit of 12 messages per minute, per channel. + * + * @param jwtToken Signed JWT with user_id and role (set to "external"). + * @param extensionId Client ID associated with the Extension. + * @param extensionVersion Version of the Extension sending this message. + * @param broadcasterId User ID of the broadcaster whose channel has the Extension activated. + * @param text Message for Twitch chat. Maximum: 280 characters. + * @return 204 No Content upon a successful request + */ + @RequestLine("POST /extensions/chat?broadcaster_id={broadcaster_id}") + @Headers({ + "Authorization: Bearer {token}", + "Client-Id: {extension_id}", + "Content-Type: application/json" + }) + @Body("%7B\"extension_id\":\"{extension_id}\",\"extension_version\":\"{extension_version}\",\"text\":\"{text}\"%7D") + HystrixCommand sendExtensionChatMessage( + @Param("token") String jwtToken, + @Param("extension_id") String extensionId, + @Param("extension_version") String extensionVersion, + @Param("broadcaster_id") String broadcasterId, + @Param("text") String text + ); + + /** + * Gets the specified configuration segment from the specified extension. + *

+ * You can retrieve each segment a maximum of 20 times per minute. + * If you exceed the limit, the request returns HTTP status code 429. + * + * @param jwtToken Signed JWT with exp, user_id, and role (set to "external"). + * @param extensionId The ID of the extension that contains the configuration segment you want to get. + * @param segment The type of configuration segment to get. + * @param broadcasterId The ID of the broadcaster for the configuration returned. This parameter is required if you set the segment parameter to broadcaster or developer. Do not specify this parameter if you set segment to global. + * @return ExtensionConfigurationSegmentList + */ + @RequestLine("GET /extensions/configurations?broadcaster_id={broadcaster_id}&extension_id={extension_id}&segment={segment}") + @Headers({ + "Authorization: Bearer {token}", + "Client-Id: {extension_id}" + }) + HystrixCommand getExtensionConfigurationSegment( + @Param("token") String jwtToken, + @Param("extension_id") String extensionId, + @Param("segment") List segment, + @Param("broadcaster_id") String broadcasterId + ); + + /** + * Sets a single configuration segment of any type. + *

+ * Each segment is limited to 5 KB and can be set at most 20 times per minute. + * Updates to this data are not delivered to Extensions that have already been rendered. + * + * @param jwtToken Signed JWT with exp, user_id, and role (set to "external"). + * @param extensionId ID for the Extension which the configuration is for. + * @param input Segment configuration info. + * @return 204 No Content upon a successful request. + */ + @RequestLine("PUT /extensions/configurations") + @Headers({ + "Authorization: Bearer {token}", + "Client-Id: {extension_id}", + "Content-Type: application/json" + }) + HystrixCommand setExtensionConfigurationSegment( + @Param("token") String jwtToken, + @Param("extension_id") String extensionId, + ExtensionConfigurationSegmentInput input + ); + + /** + * Retrieves a specified Extension’s secret data consisting of a version and an array of secret objects. + *

+ * Each secret object contains a base64-encoded secret, a UTC timestamp when the secret becomes active, and a timestamp when the secret expires. + *

+ * Signed JWT created by an Extension Backend Service (EBS), following the requirements documented in Signing the JWT. + * A signed JWT must include the exp, user_id, and role fields documented in JWT Schema, and role must be set to "external". + * + * @param jwtToken Signed JWT with exp, user_id, and role (set to "external"). + * @param extensionId The Client ID associated with the extension. + * @return ExtensionSecretsList + */ + @RequestLine("GET /extensions/jwt/secrets?extension_id={extension_id}") + @Headers({ + "Authorization: Bearer {token}", + "Client-Id: {extension_id}" + }) + HystrixCommand getExtensionSecrets( + @Param("token") String jwtToken, + @Param("extension_id") String extensionId + ); + + /** + * Creates a JWT signing secret for a specific Extension. + *

+ * Also rotates any current secrets out of service, with enough time for instances of the Extension to gracefully switch over to the new secret. + * Use this function only when you are ready to install the new secret it returns. + * + * @param jwtToken Signed JWT with exp, user_id, and role (set to "external"). + * @param extensionId The Client ID associated with the extension. + * @param delay Optional: JWT signing activation delay for the newly created secret in seconds. Minimum: 300. Default: 300. + * @return ExtensionSecretsList + */ + @RequestLine("POST /extensions/jwt/secrets?extension_id={extension_id}&delay={delay}") + @Headers({ + "Authorization: Bearer {token}", + "Client-Id: {extension_id}" + }) + HystrixCommand createExtensionSecret( + @Param("token") String jwtToken, + @Param("extension_id") String extensionId, + @Param("delay") Integer delay + ); + + /** + * Returns one page of live channels that have installed or activated a specific Extension, + * identified by a client ID value assigned to the Extension when it is created. + *

+ * A channel that recently went live may take a few minutes to appear in this list, + * and a channel may continue to appear on this list for a few minutes after it stops broadcasting. + * + * @param authToken User OAuth Token or App Access Token + * @param extensionId ID of the Extension to search for. + * @param limit Maximum number of objects to return. Maximum: 100. Default: 20. + * @param after The cursor used to fetch the next page of data. + * @return ExtensionLiveChannelsList + */ + @RequestLine("GET /extensions/live?extension_id={extension_id}&first={first}&after={after}&cursor={after}") + @Headers("Authorization: Bearer {token}") + HystrixCommand getExtensionLiveChannels( + @Param("token") String authToken, + @Param("extension_id") String extensionId, + @Param("first") Integer limit, + @Param("after") String after + ); + + /** + * Twitch provides a publish-subscribe system for your EBS to communicate with both the broadcaster and viewers. + * Calling this endpoint forwards your message using the same mechanism as the send JavaScript helper function. + *

+ * A message can be sent to either a specified channel or globally (all channels on which your extension is active). + *

+ * Extension PubSub has a rate limit of 100 requests per minute for a combination of Extension client ID and broadcaster ID. + *

+ * A signed JWT must include the channel_id and pubsub_perms fields documented in JWT Schema. + * + * @param jwtToken Signed JWT with exp, user_id, role, channel_id, pubsub_perms.send + * @param extensionId Client ID associated with the Extension. + * @param input Details on the message to be sent and its targets. + * @return 204 No Content upon a successful request. + */ + @RequestLine("POST /extensions/pubsub") + @Headers({ + "Authorization: Bearer {token}", + "Client-Id: {extension_id}", + "Content-Type: application/json" + }) + HystrixCommand sendExtensionPubSubMessage( + @Param("token") String jwtToken, + @Param("extension_id") String extensionId, + SendPubSubMessageInput input + ); + + /** + * Gets information about a released Extension; either the current version or a specified version. + * + * @param authToken User OAuth Token or App Access Token + * @param extensionId ID of the Extension. + * @param extensionVersion The specific version of the Extension to return. If not provided, the current version is returned. + * @return ReleasedExtensionList + */ + @RequestLine("GET /extensions/released?extension_id={extension_id}&extension_version={extension_version}") + @Headers("Authorization: Bearer {token}") + HystrixCommand getReleasedExtensions( + @Param("token") String authToken, + @Param("extension_id") String extensionId, + @Param("extension_version") String extensionVersion + ); + + /** + * Enable activation of a specified Extension, after any required broadcaster configuration is correct. + *

+ * This is for Extensions that require broadcaster configuration before activation. + * Use this if, in Extension Capabilities, you select Custom/My Own Service. + *

+ * You enforce required broadcaster configuration with a required_configuration string in the Extension manifest. The contents of this string can be whatever you want. + * Once your EBS determines that the Extension is correctly configured on a channel, use this endpoint to provide that same configuration string, which enables activation on the channel. + * The endpoint URL includes the channel ID of the page where the Extension is iframe embedded. + *

+ * If a future version of the Extension requires a different configuration, change the required_configuration string in your manifest. + * When the new version is released, broadcasters will be required to re-configure that new version. + * + * @param jwtToken Signed JWT with exp, user_id, and role (set to "external"). + * @param extensionId ID for the Extension to activate. + * @param extensionVersion The version fo the Extension to release. + * @param configurationVersion The version of the configuration to use with the Extension. + * @param broadcasterId User ID of the broadcaster who has activated the specified Extension on their channel. + * @return 204 No Content upon a successful request. + */ + @RequestLine("PUT /extensions/required_configuration?broadcaster_id={broadcaster_id}") + @Headers({ + "Authorization: Bearer {token}", + "Client-Id: {extension_id}", + "Content-Type: application/json" + }) + @Body("%7B\"extension_id\":\"{extension_id}\",\"extension_version\":\"{extension_version}\",\"required_configuration\":\"{configuration_version}\",\"configuration_version\":\"{configuration_version}\"%7D") + HystrixCommand setExtensionRequiredConfiguration( + @Param("token") String jwtToken, + @Param("extension_id") String extensionId, + @Param("extension_version") String extensionVersion, + @Param("configuration_version") String configurationVersion, + @Param("broadcaster_id") String broadcasterId + ); + /** * Get Extension Transactions allows extension back end servers to fetch a list of transactions that have occurred for their extension across all of Twitch. * @@ -1924,6 +2198,8 @@ HystrixCommand updateUser( *

* Gets a list of all extensions (both active and inactive) for a specified user, identified by a Bearer token. The response has a JSON payload with a data field containing an array of user-information elements. * Required scope: user:read:broadcast + *

+ * Note: inactive extensions are only returned if the token has the user:edit:broadcast scope - https://github.com/twitchdev/issues/issues/477 * * @param authToken Auth Token * @return ExtensionList diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionBitsProduct.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionBitsProduct.java new file mode 100644 index 000000000..781e3b868 --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionBitsProduct.java @@ -0,0 +1,88 @@ +package com.github.twitch4j.helix.domain; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.With; +import lombok.experimental.Accessors; +import lombok.extern.jackson.Jacksonized; + +import java.time.Instant; + +@With +@Data +@Setter(AccessLevel.PRIVATE) +@Builder(toBuilder = true) +@Jacksonized +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ExtensionBitsProduct { + + /** + * SKU of the Bits product. + *

+ * This is unique across all products that belong to an Extension. + */ + private String sku; + + /** + * Object containing cost information. + */ + private Cost cost; + + /** + * Indicates if the product is in development and not yet released for public use. + */ + @Accessors(fluent = true) + @JsonProperty("in_development") + private Boolean isInDevelopment; + + /** + * Name of the product to be displayed in the Extension. + */ + private String displayName; + + /** + * Expiration time for the product in RFC3339 format. + */ + private Instant expiration; + + /** + * Indicates if Bits product purchase events are broadcast to all instances of an Extension on a channel via the “onTransactionComplete” helper callback. + */ + @Accessors(fluent = true) + @JsonProperty("is_broadcast") + private Boolean isBroadcast; + + @With + @Data + @Setter(AccessLevel.PRIVATE) + @Builder(toBuilder = true) + @Jacksonized + @NoArgsConstructor + @AllArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class Cost { + + /** + * Number of Bits for which the product will be exchanged. + */ + private Integer amount; + + /** + * Cost type. + *

+ * The one valid value is "bits". + */ + @Builder.Default + private String type = "bits"; + + } + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionBitsProductList.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionBitsProductList.java new file mode 100644 index 000000000..00a922d40 --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionBitsProductList.java @@ -0,0 +1,15 @@ +package com.github.twitch4j.helix.domain; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; + +import java.util.List; + +@Data +@Setter(AccessLevel.PRIVATE) +public class ExtensionBitsProductList { + + private List data; + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionConfigurationSegment.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionConfigurationSegment.java new file mode 100644 index 000000000..f5c15efee --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionConfigurationSegment.java @@ -0,0 +1,36 @@ +package com.github.twitch4j.helix.domain; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; +import org.jetbrains.annotations.Nullable; + +@Data +@Setter(AccessLevel.PRIVATE) +public class ExtensionConfigurationSegment { + + /** + * The type of segment. + */ + private ExtensionSegment segment; + + /** + * The contents of the segment. + *

+ * This string may be a plain string or a string-encoded JSON object. + */ + private String content; + + /** + * The version that identifies the segment’s definition. + */ + private String version; + + /** + * The ID of the broadcaster that owns the extension. + * The object includes this field only if the segment query parameter is set to developer or broadcaster. + */ + @Nullable + private String broadcasterId; + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionConfigurationSegmentInput.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionConfigurationSegmentInput.java new file mode 100644 index 000000000..f2316d939 --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionConfigurationSegmentInput.java @@ -0,0 +1,53 @@ +package com.github.twitch4j.helix.domain; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.With; +import lombok.extern.jackson.Jacksonized; +import org.jetbrains.annotations.Nullable; + +@With +@Data +@Setter(AccessLevel.PRIVATE) +@Builder(toBuilder = true) +@Jacksonized +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ExtensionConfigurationSegmentInput { + + /** + * Required: ID for the Extension which the configuration is for. + */ + private String extensionId; + + /** + * Required: Configuration type. + */ + private ExtensionSegment segment; + + /** + * User ID of the broadcaster. + * Required if the segment type is "developer" or "broadcaster". + */ + @Nullable + private String broadcasterId; + + /** + * Configuration in a string-encoded format. + */ + @Nullable + private String content; + + /** + * Configuration version with the segment type. + */ + @Nullable + private String version; + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionConfigurationSegmentList.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionConfigurationSegmentList.java new file mode 100644 index 000000000..775cab600 --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionConfigurationSegmentList.java @@ -0,0 +1,18 @@ +package com.github.twitch4j.helix.domain; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; + +import java.util.List; + +@Data +@Setter(AccessLevel.PRIVATE) +public class ExtensionConfigurationSegmentList { + + /** + * The segment objects. + */ + private List data; + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionLiveChannel.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionLiveChannel.java new file mode 100644 index 000000000..0224babdc --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionLiveChannel.java @@ -0,0 +1,36 @@ +package com.github.twitch4j.helix.domain; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; + +@Data +@Setter(AccessLevel.PRIVATE) +public class ExtensionLiveChannel { + + /** + * User ID of the broadcaster. + */ + private String broadcasterId; + + /** + * Broadcaster’s display name. + */ + private String broadcasterName; + + /** + * Name of the game being played. + */ + private String gameName; + + /** + * ID of the game being played. + */ + private String gameId; + + /** + * Title of the stream. + */ + private String title; + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionLiveChannelsList.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionLiveChannelsList.java new file mode 100644 index 000000000..8206b0e7c --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionLiveChannelsList.java @@ -0,0 +1,43 @@ +package com.github.twitch4j.helix.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Data +@Setter(AccessLevel.PRIVATE) +public class ExtensionLiveChannelsList { + + /** + * One page of live channels that have installed or activated a specific Extension. + */ + @JsonProperty("data") + private List channels; + + @Getter(AccessLevel.PRIVATE) + private Object pagination; + + /** + * @return the cursor to specify in the next call to obtain the next page of results, in an optional wrapper + * @implNote Pagination for this endpoint is currently broken + * in multiple respects + */ + @JsonIgnore + public Optional getCursor() { + // Currently, pagination is returned directly as a string, which departs from all of the other helix endpoints + // In the future, Twitch may fix the above inconsistency and return an object that contains a string + return Optional.ofNullable(pagination instanceof Map ? ((Map) pagination).get("cursor") : pagination) + .filter(c -> c instanceof String) + .map(c -> (String) c) + .filter(StringUtils::isNotBlank); + } + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionSecret.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionSecret.java new file mode 100644 index 000000000..50dc971bc --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionSecret.java @@ -0,0 +1,28 @@ +package com.github.twitch4j.helix.domain; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; + +import java.time.Instant; + +@Data +@Setter(AccessLevel.PRIVATE) +public class ExtensionSecret { + + /** + * Raw (base64-encoded) secret that should be used with JWT encoding. + */ + private String content; + + /** + * The earliest possible time this secret is valid to sign a JWT in RFC 3339 format. + */ + private Instant activeAt; + + /** + * The latest possible time this secret may be used to decode a JWT in RFC 3339 format. + */ + private Instant expiresAt; + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionSecrets.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionSecrets.java new file mode 100644 index 000000000..397f2b30d --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionSecrets.java @@ -0,0 +1,23 @@ +package com.github.twitch4j.helix.domain; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; + +import java.util.List; + +@Data +@Setter(AccessLevel.PRIVATE) +public class ExtensionSecrets { + + /** + * Indicates the version associated with the Extension secrets in the response. + */ + private Integer formatVersion; + + /** + * The secret objects. + */ + private List secrets; + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionSecretsList.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionSecretsList.java new file mode 100644 index 000000000..a86723750 --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionSecretsList.java @@ -0,0 +1,18 @@ +package com.github.twitch4j.helix.domain; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; + +import java.util.List; + +@Data +@Setter(AccessLevel.PRIVATE) +public class ExtensionSecretsList { + + /** + * The extension secrets; each secret object contains a base64-encoded secret, a UTC timestamp when the secret becomes active, and a timestamp when the secret expires. + */ + private List data; + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionSegment.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionSegment.java new file mode 100644 index 000000000..5e1b2f3be --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionSegment.java @@ -0,0 +1,21 @@ +package com.github.twitch4j.helix.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum ExtensionSegment { + + @JsonProperty("broadcaster") + BROADCASTER, + + @JsonProperty("developer") + DEVELOPER, + + @JsonProperty("global") + GLOBAL; + + @Override + public String toString() { + return this.name().toLowerCase(); + } + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionState.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionState.java new file mode 100644 index 000000000..0e9e4b621 --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ExtensionState.java @@ -0,0 +1,39 @@ +package com.github.twitch4j.helix.domain; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum ExtensionState { + + @JsonAlias({ "Testing", "HostedTest" }) // undocumented, but have observed "Testing" + @JsonProperty("InTest") + IN_TEST, + + @JsonAlias({ "Reviewing", "ReadyForReview" }) // undocumented, unobserved + @JsonProperty("InReview") + IN_REVIEW, + + @JsonProperty("Rejected") + REJECTED, + + @JsonProperty("Approved") + APPROVED, + + @JsonProperty("Released") + RELEASED, + + @JsonProperty("Deprecated") + DEPRECATED, + + @JsonAlias("Pending") // undocumented, unobserved + @JsonProperty("PendingAction") + PENDING_ACTION, + + @JsonAlias("Uploading") // undocumented, unobserved + @JsonProperty("AssetsUploaded") + ASSETS_UPLOADED, + + @JsonProperty("Deleted") + DELETED + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ReleasedExtension.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ReleasedExtension.java new file mode 100644 index 000000000..4c83bc37e --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ReleasedExtension.java @@ -0,0 +1,175 @@ +package com.github.twitch4j.helix.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.List; +import java.util.Map; + +@Data +@Setter(AccessLevel.PRIVATE) +public class ReleasedExtension { + + /** + * Name of the individual or organization that owns the Extension. + */ + private String authorName; + + /** + * Whether the Extension has features that use Bits. + */ + @Accessors(fluent = true) + @JsonProperty("bits_enabled") + private Boolean bitsEnabled; + + /** + * Indicates if a user can install the Extension on their channel. + * They may not be allowed if the Extension is currently in testing mode and the user is not on the allow list. + */ + @Accessors(fluent = true) + @JsonProperty("can_install") + private Boolean canInstall; + + /** + * Whether the Extension configuration is hosted by the EBS or the Extensions Configuration Service. + *

+ * Possible values include "hosted", "none" + */ + private String configurationLocation; + + /** + * The description of the Extension. + */ + private String description; + + /** + * URL to the Extension’s Terms of Service. + */ + private String eulaTosUrl; + + /** + * Indicates if the Extension can communicate with the installed channel’s chat. + */ + @JsonProperty("has_chat_support") + @Accessors(fluent = true) + private Boolean hasChatSupport; + + /** + * The default icon to be displayed in the Extensions directory. + */ + private String iconUrl; + + /** + * The default icon in a variety of sizes. + *

+ * Common sizes include "100x100", "24x24", and "300x200" + */ + private Map iconUrls; + + /** + * The autogenerated ID of the Extension. + */ + private String id; + + /** + * The name of the Extension. + */ + private String name; + + /** + * URL to the Extension’s privacy policy. + */ + private String privacyPolicyUrl; + + /** + * Indicates if the Extension wants to explicitly ask viewers to link their Twitch identity. + */ + @Accessors(fluent = true) + @JsonProperty("request_identity_link") + private Boolean requestIdentityLink; + + /** + * Screenshots to be shown in the Extensions marketplace. + */ + private List screenshotUrls; + + /** + * The current state of the Extension. + */ + private ExtensionState state; + + /** + * Indicates if the Extension can determine a user’s subscription level on the channel the Extension is installed on. + *

+ * Possible values include "optional", "none" + */ + private String subscriptionsSupportLevel; + + /** + * A brief description of the Extension. + */ + private String summary; + + /** + * The email users can use to receive Extension support. + */ + private String supportEmail; + + /** + * The version of the Extension. + */ + private String version; + + /** + * A brief description displayed on the channel to explain how the Extension works. + */ + private String viewerSummary; + + /** + * All configurations related to views such as: mobile, panel, video_overlay, and component. + */ + private Views views; + + /** + * Allow-listed configuration URLs for displaying the Extension. + */ + private List allowlistedConfigUrls; + + /** + * Allow-listed panel URLs for displaying the Extension. + */ + private List allowlistedPanelUrls; + + @Data + @Setter(AccessLevel.PRIVATE) + public static class Views { + private View mobile; + private View panel; + private View videoOverlay; + private View component; + } + + @Data + @Setter(AccessLevel.PRIVATE) + public static class View { + @Accessors(fluent = true) + @JsonProperty("can_link_external_content") + private Boolean canLinkExternalContent; + private String viewerUrl; + private Integer aspectWidth; + private Integer aspectHeight; + private Integer aspectRatioX; + private Integer aspectRatioY; + private Boolean autoscale; + private Integer height; + private Integer scalePixels; + private Integer targetHeight; + private Integer size; + private Boolean zoom; + private Integer zoomPixels; + } + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ReleasedExtensionList.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ReleasedExtensionList.java new file mode 100644 index 000000000..b9ac319e5 --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/ReleasedExtensionList.java @@ -0,0 +1,15 @@ +package com.github.twitch4j.helix.domain; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Setter; + +import java.util.List; + +@Data +@Setter(AccessLevel.PRIVATE) +public class ReleasedExtensionList { + + private List data; + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/domain/SendPubSubMessageInput.java b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/SendPubSubMessageInput.java new file mode 100644 index 000000000..155d2a14e --- /dev/null +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/domain/SendPubSubMessageInput.java @@ -0,0 +1,114 @@ +package com.github.twitch4j.helix.domain; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +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.extern.jackson.Jacksonized; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +@With +@Data +@Setter(AccessLevel.PRIVATE) +@Builder(toBuilder = true) +@Jacksonized +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SendPubSubMessageInput { + + /** + * Target a specific public channel. + */ + public static final String BROADCAST_TARGET = "broadcast"; + + /** + * Target all channels that have the extension active. + */ + public static final String GLOBAL_TARGET = "global"; + + /** + * Target a user via private message. + */ + public static final String WHISPER_TARGET_PREFIX = "whisper-"; + + /** + * Strings for valid PubSub targets. + * Valid values: "broadcast", "global", "whisper-" + */ + @Singular + @JsonProperty("target") + private List targets; + + /** + * ID of the broadcaster receiving the payload. + * This is not required if is_global_broadcast is set to true. + */ + @Nullable + private String broadcasterId; + + /** + * Indicates if the message should be sent to all channels where your Extension is active. + *

+ * Default: false. + */ + @Builder.Default + @JsonProperty("is_global_broadcast") + private boolean globalBroadcast = false; + + /** + * String-encoded JSON message to be sent. + *

+ * Must not exceed 5 KB in size. + */ + private String message; + + /** + * @return whether the constructed input complies with twitch requirements. + */ + public boolean validate() { + // Message must be present + if (message == null) + return false; + + // Must be either global or targeting a broadcaster + if (globalBroadcast == (broadcasterId != null && !broadcasterId.isEmpty())) + return false; + + // Must have targets + if (targets == null || targets.isEmpty()) + return false; + + // Targets must be valid + boolean hasGlobal = false; + for (String target : targets) { + if (target == null || target.isEmpty()) + return false; + + if (BROADCAST_TARGET.equals(target) || target.startsWith(WHISPER_TARGET_PREFIX)) + continue; + + if (GLOBAL_TARGET.equals(target)) { + hasGlobal = true; + continue; + } + + return false; + } + + // Global target must have boolean flag enabled + if (globalBroadcast == !hasGlobal) + return false; + + return true; + } + +} diff --git a/rest-helix/src/main/java/com/github/twitch4j/helix/interceptor/TwitchHelixClientIdInterceptor.java b/rest-helix/src/main/java/com/github/twitch4j/helix/interceptor/TwitchHelixClientIdInterceptor.java index 8f187ce85..767a8cc68 100644 --- a/rest-helix/src/main/java/com/github/twitch4j/helix/interceptor/TwitchHelixClientIdInterceptor.java +++ b/rest-helix/src/main/java/com/github/twitch4j/helix/interceptor/TwitchHelixClientIdInterceptor.java @@ -114,7 +114,7 @@ public void apply(RequestTemplate template) { template.removeHeader(AUTH_HEADER); template.header(AUTH_HEADER, BEARER_PREFIX + oauthToken); - } else { + } else if (!StringUtils.contains(oauthToken, '.')) { OAuth2Credential verifiedCredential = accessTokenCache.getIfPresent(oauthToken); if (verifiedCredential == null) { log.debug("Getting matching client-id for authorization token {}", oauthToken.substring(0, 5)); @@ -135,7 +135,8 @@ public void apply(RequestTemplate template) { } // set headers - template.header("Client-Id", clientId); + if (!template.headers().containsKey("Client-Id")) + template.header("Client-Id", clientId); template.header("User-Agent", twitchAPIBuilder.getUserAgent()); }