getLiveChannelsWithExtensionActivated(String clientId) {
+ return getLiveChannelsWithExtensionActivated(clientId, null);
+ }
+
/**
* Enable activation of a specified extension, after any required broadcaster configuration is correct.
*
diff --git a/rest-extensions/src/main/java/com/github/twitch4j/extensions/TwitchExtensionsBuilder.java b/rest-extensions/src/main/java/com/github/twitch4j/extensions/TwitchExtensionsBuilder.java
index 40cb2fd9b..27429e0cb 100644
--- a/rest-extensions/src/main/java/com/github/twitch4j/extensions/TwitchExtensionsBuilder.java
+++ b/rest-extensions/src/main/java/com/github/twitch4j/extensions/TwitchExtensionsBuilder.java
@@ -4,6 +4,7 @@
import com.github.twitch4j.common.config.ProxyConfig;
import com.github.twitch4j.common.config.Twitch4JGlobal;
import com.github.twitch4j.common.util.TypeConvert;
+import com.github.twitch4j.extensions.compat.TwitchExtensionsCompatibilityLayer;
import com.github.twitch4j.extensions.util.TwitchExtensionsClientIdInterceptor;
import com.github.twitch4j.extensions.util.TwitchExtensionsErrorDecoder;
import com.netflix.config.ConfigurationManager;
@@ -22,11 +23,15 @@
/**
* Twitch API - Extensions
+ *
+ * @see Twitch Shutdown Announcement
+ * @deprecated the Extensions API traditionally uses the decommissioned Kraken API. While the module now forwards calls to Helix, please migrate to using Helix directly as this module will be removed in the future.
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
+@Deprecated
public class TwitchExtensionsBuilder {
/**
@@ -77,14 +82,36 @@ public class TwitchExtensionsBuilder {
@With
private ProxyConfig proxyConfig = null;
+ /**
+ * Whether the compatibility layer should be used to forward requests to the new Helix API
+ */
+ @With
+ private boolean helixForwarding = true;
+
/**
* Twitch API Client (Extensions)
*
* @return TwitchExtensions
+ * @see Twitch Shutdown Announcement
+ * @deprecated the Extensions API traditionally uses the decommissioned Kraken API. While the module now forwards calls to Helix, please migrate to using Helix directly as this module will be removed in the future.
*/
+ @Deprecated
public TwitchExtensions build() {
log.debug("Extensions: Initializing Module ...");
+ // Helix Compatibility Layer
+ if (helixForwarding) {
+ return TwitchExtensionsCompatibilityLayer.builder()
+ .clientId(clientId)
+ .clientSecret(clientSecret)
+ .userAgent(userAgent)
+ .timeout(timeout)
+ .requestQueueSize(requestQueueSize)
+ .logLevel(logLevel)
+ .proxyConfig(proxyConfig)
+ .build();
+ }
+
// Hystrix
ConfigurationManager.getConfigInstance().setProperty("hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds", timeout);
ConfigurationManager.getConfigInstance().setProperty("hystrix.command.default.requestCache.enabled", false);
@@ -122,7 +149,10 @@ public TwitchExtensions build() {
* Initialize the builder
*
* @return Twitch Extensions Builder
+ * @see Twitch Shutdown Announcement
+ * @deprecated the Extensions API traditionally uses the decommissioned Kraken API. While the module now forwards calls to Helix, please migrate to using Helix directly as this module will be removed in the future.
*/
+ @Deprecated
public static TwitchExtensionsBuilder builder() {
return new TwitchExtensionsBuilder();
}
diff --git a/rest-extensions/src/main/java/com/github/twitch4j/extensions/compat/ExtensionsTypeConverters.java b/rest-extensions/src/main/java/com/github/twitch4j/extensions/compat/ExtensionsTypeConverters.java
new file mode 100644
index 000000000..9aa9a0741
--- /dev/null
+++ b/rest-extensions/src/main/java/com/github/twitch4j/extensions/compat/ExtensionsTypeConverters.java
@@ -0,0 +1,187 @@
+package com.github.twitch4j.extensions.compat;
+
+import com.github.twitch4j.extensions.domain.Channel;
+import com.github.twitch4j.extensions.domain.ChannelList;
+import com.github.twitch4j.extensions.domain.ConfigurationSegment;
+import com.github.twitch4j.extensions.domain.ConfigurationSegmentType;
+import com.github.twitch4j.extensions.domain.ExtensionInformation;
+import com.github.twitch4j.extensions.domain.ExtensionSecret;
+import com.github.twitch4j.extensions.domain.ExtensionSecretList;
+import com.github.twitch4j.helix.domain.ExtensionConfigurationSegmentList;
+import com.github.twitch4j.helix.domain.ExtensionLiveChannel;
+import com.github.twitch4j.helix.domain.ExtensionLiveChannelsList;
+import com.github.twitch4j.helix.domain.ExtensionSecrets;
+import com.github.twitch4j.helix.domain.ExtensionSecretsList;
+import com.github.twitch4j.helix.domain.ExtensionSegment;
+import com.github.twitch4j.helix.domain.ExtensionState;
+import com.github.twitch4j.helix.domain.ReleasedExtension;
+import com.github.twitch4j.helix.domain.ReleasedExtensionList;
+import lombok.experimental.UtilityClass;
+import org.apache.commons.lang3.tuple.Pair;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@UtilityClass
+class ExtensionsTypeConverters {
+
+ Function SEGMENT_CONVERTER = configurationSegmentType -> {
+ switch (configurationSegmentType) {
+ case GLOBAL:
+ return ExtensionSegment.GLOBAL;
+ case DEVELOPER:
+ return ExtensionSegment.DEVELOPER;
+ case BROADCASTER:
+ return ExtensionSegment.BROADCASTER;
+ default:
+ return null;
+ }
+ };
+
+ Function HELIX_SEGMENT_CONVERTER = extensionSegment -> {
+ switch (extensionSegment) {
+ case BROADCASTER:
+ return ConfigurationSegmentType.BROADCASTER;
+ case DEVELOPER:
+ return ConfigurationSegmentType.DEVELOPER;
+ case GLOBAL:
+ return ConfigurationSegmentType.GLOBAL;
+ default:
+ return null;
+ }
+ };
+
+ Function STATE_CONVERTER = state -> {
+ switch (state) {
+ case IN_TEST:
+ return "testing";
+ case ASSETS_UPLOADED:
+ return "uploading";
+ default:
+ return state.name().toLowerCase();
+ }
+ };
+
+ Function> VIEWS_CONVERTER = views -> {
+ if (views == null) return Collections.emptyMap();
+
+ final BiConsumer> addView = (v, m) -> {
+ final BiConsumer addProperty = (s, o) -> {
+ if (o != null)
+ m.put(s, o);
+ };
+
+ addProperty.accept("can_link_external_content", v.canLinkExternalContent());
+ addProperty.accept("viewer_url", v.getViewerUrl());
+ addProperty.accept("aspect_width", v.getAspectWidth());
+ addProperty.accept("aspect_height", v.getAspectHeight());
+ addProperty.accept("aspect_ratio_x", v.getAspectRatioX());
+ addProperty.accept("aspect_ratio_y", v.getAspectRatioY());
+ addProperty.accept("autoscale", v.getAutoscale());
+ addProperty.accept("height", v.getHeight());
+ addProperty.accept("scale_pixels", v.getScalePixels());
+ addProperty.accept("target_height", v.getTargetHeight());
+ addProperty.accept("size", v.getSize());
+ addProperty.accept("zoom", v.getZoom());
+ addProperty.accept("zoom_pixels", v.getZoomPixels());
+ };
+
+ final Map map = new HashMap<>();
+
+ final BiConsumer addViewMap = (s, v) -> {
+ if (v != null) {
+ final Map m = new HashMap<>();
+ addView.accept(v, m);
+ map.put(s, m);
+ }
+ };
+
+ addViewMap.accept("mobile", views.getMobile());
+ addViewMap.accept("panel", views.getPanel());
+ addViewMap.accept("video_overlay", views.getVideoOverlay());
+ addViewMap.accept("component", views.getComponent());
+ return Collections.unmodifiableMap(map);
+ };
+
+ Function EXTENSION_CONVERTER = ext -> {
+ Map viewerUrls = ext.getViews() == null ? null :
+ Stream.of(Pair.of("component", ext.getViews().getComponent()), Pair.of("mobile", ext.getViews().getMobile()), Pair.of("panel", ext.getViews().getPanel()), Pair.of("video_overlay", ext.getViews().getVideoOverlay()))
+ .filter(pair -> pair.getRight() != null)
+ .map(pair -> Pair.of(pair.getLeft(), pair.getRight().getViewerUrl()))
+ .filter(pair -> pair.getRight() != null)
+ .collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
+
+ return ExtensionInformation.builder()
+ .authorName(ext.getAuthorName())
+ .bitsEnabled(ext.bitsEnabled())
+ .canInstall(ext.canInstall())
+ .configurationLocation(ext.getConfigurationLocation())
+ .description(ext.getDescription())
+ .eulaTosUrl(ext.getEulaTosUrl())
+ .hasChatSupport(ext.hasChatSupport())
+ .iconUrl(ext.getIconUrl())
+ .iconUrls(ext.getIconUrls())
+ .id(ext.getId())
+ .name(ext.getName())
+ .panelHeight(ext.getViews() != null && ext.getViews().getPanel() != null ? ext.getViews().getPanel().getHeight() : null)
+ .privacyPolicyUrl(ext.getPrivacyPolicyUrl())
+ .requestIdentityLink(ext.requestIdentityLink())
+ .screenshotUrls(ext.getScreenshotUrls())
+ .state(STATE_CONVERTER.apply(ext.getState()))
+ .subscriptionsSupportLevel(ext.getSubscriptionsSupportLevel())
+ .summary(ext.getSummary())
+ .supportEmail(ext.getSupportEmail())
+ .version(ext.getVersion())
+ .viewerSummary(ext.getViewerSummary())
+ .viewerUrl(viewerUrls == null || viewerUrls.isEmpty() ? null : viewerUrls.values().toArray(new String[0])[0])
+ .viewerUrls(viewerUrls)
+ .views(VIEWS_CONVERTER.apply(ext.getViews()))
+ .whitelistedConfigUrls(ext.getAllowlistedConfigUrls())
+ .whitelistedPanelUrls(ext.getAllowlistedPanelUrls())
+ .build();
+ };
+
+ Function EXTENSION_LIST_CONVERTER = list -> {
+ if (list == null || list.getData() == null || list.getData().isEmpty()) return null;
+ return EXTENSION_CONVERTER.apply(list.getData().get(0));
+ };
+
+ Function LIVE_CHANNEL_CONVERTER = c -> new Channel(c.getBroadcasterId(), c.getBroadcasterName(), c.getGameId(), c.getTitle(), null);
+
+ Function LIVE_CHANNELS_CONVERTER = helixList -> {
+ if (helixList == null || helixList.getChannels() == null) return null;
+ return new ChannelList(helixList.getChannels().stream().map(LIVE_CHANNEL_CONVERTER).collect(Collectors.toList()), helixList.getCursor().orElse(null));
+ };
+
+ Function SECRET_CONVERTER = secret -> new ExtensionSecret(secret.getActiveAt(), secret.getContent(), secret.getExpiresAt());
+
+ Function SECRETS_CONVERTER = helixList -> {
+ if (helixList == null || helixList.getData() == null || helixList.getData().isEmpty()) return null;
+ final ExtensionSecrets secrets = helixList.getData().get(0);
+ return new ExtensionSecretList(secrets.getFormatVersion(), secrets.getSecrets().stream().map(SECRET_CONVERTER).collect(Collectors.toList()));
+ };
+
+ Function> CONFIG_SEGMENT_LIST_CONVERTER = list -> {
+ if (list == null || list.getData() == null) return null;
+
+ final Map map = new HashMap<>();
+
+ list.getData().forEach(segment -> {
+ String name = segment.getSegment().toString();
+ String broadcasterId = segment.getBroadcasterId() == null ? "" : segment.getBroadcasterId();
+ String key = name + ':' + broadcasterId;
+
+ ConfigurationSegment.Segment s = new ConfigurationSegment.Segment(HELIX_SEGMENT_CONVERTER.apply(segment.getSegment()), segment.getBroadcasterId());
+ ConfigurationSegment.Record r = new ConfigurationSegment.Record(segment.getContent(), segment.getVersion());
+ map.put(key, new ConfigurationSegment(s, r));
+ });
+
+ return Collections.unmodifiableMap(map);
+ };
+
+}
diff --git a/rest-extensions/src/main/java/com/github/twitch4j/extensions/compat/HystrixCommandConverter.java b/rest-extensions/src/main/java/com/github/twitch4j/extensions/compat/HystrixCommandConverter.java
new file mode 100644
index 000000000..868763300
--- /dev/null
+++ b/rest-extensions/src/main/java/com/github/twitch4j/extensions/compat/HystrixCommandConverter.java
@@ -0,0 +1,24 @@
+package com.github.twitch4j.extensions.compat;
+
+import com.netflix.hystrix.HystrixCommand;
+import lombok.NonNull;
+
+import java.util.function.Function;
+
+class HystrixCommandConverter extends HystrixCommand {
+
+ private final HystrixCommand command;
+ private final Function converter;
+
+ public HystrixCommandConverter(@NonNull HystrixCommand hystrixCommand, @NonNull Function converter) {
+ super(hystrixCommand.getCommandGroup());
+ this.command = hystrixCommand;
+ this.converter = converter;
+ }
+
+ @Override
+ protected U run() {
+ return converter.apply(command.execute());
+ }
+
+}
diff --git a/rest-extensions/src/main/java/com/github/twitch4j/extensions/compat/TwitchExtensionsCompatibilityLayer.java b/rest-extensions/src/main/java/com/github/twitch4j/extensions/compat/TwitchExtensionsCompatibilityLayer.java
new file mode 100644
index 000000000..5ec890765
--- /dev/null
+++ b/rest-extensions/src/main/java/com/github/twitch4j/extensions/compat/TwitchExtensionsCompatibilityLayer.java
@@ -0,0 +1,160 @@
+package com.github.twitch4j.extensions.compat;
+
+import com.github.twitch4j.common.config.ProxyConfig;
+import com.github.twitch4j.common.config.Twitch4JGlobal;
+import com.github.twitch4j.extensions.TwitchExtensions;
+import com.github.twitch4j.extensions.domain.ChannelList;
+import com.github.twitch4j.extensions.domain.ConfigurationSegment;
+import com.github.twitch4j.extensions.domain.ConfigurationSegmentType;
+import com.github.twitch4j.extensions.domain.ExtensionConfigurationSegment;
+import com.github.twitch4j.extensions.domain.ExtensionInformation;
+import com.github.twitch4j.extensions.domain.ExtensionSecretList;
+import com.github.twitch4j.helix.TwitchHelix;
+import com.github.twitch4j.helix.TwitchHelixBuilder;
+import com.github.twitch4j.helix.domain.ExtensionConfigurationSegmentInput;
+import com.github.twitch4j.helix.domain.ExtensionSegment;
+import com.github.twitch4j.helix.domain.SendPubSubMessageInput;
+import com.netflix.hystrix.HystrixCommand;
+import feign.Logger;
+import lombok.Builder;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+import static com.github.twitch4j.extensions.compat.ExtensionsTypeConverters.*; // Must be wildcard import for static: https://github.com/projectlombok/lombok/issues/2044
+
+/**
+ * Forwards twitch extensions api calls to the new helix api
+ */
+@SuppressWarnings("deprecation")
+public final class TwitchExtensionsCompatibilityLayer implements TwitchExtensions {
+
+ private final String clientId;
+ private final TwitchHelix helix;
+
+ @Builder
+ public TwitchExtensionsCompatibilityLayer(String clientId, String clientSecret, String userAgent, Long timeout, Integer requestQueueSize, Logger.Level logLevel, ProxyConfig proxyConfig) {
+ this.clientId = clientId;
+ this.helix = TwitchHelixBuilder.builder()
+ .withClientId(clientId)
+ .withClientSecret(clientSecret)
+ .withUserAgent(userAgent != null ? userAgent : Twitch4JGlobal.userAgent)
+ .withTimeout(timeout != null ? timeout.intValue() : 5000)
+ .withRequestQueueSize(requestQueueSize != null ? requestQueueSize : -1)
+ .withLogLevel(logLevel != null ? logLevel : Logger.Level.NONE)
+ .withProxyConfig(proxyConfig)
+ .build();
+ }
+
+ @Override
+ public HystrixCommand createExtensionSecret(String clientId, String jsonWebToken, int activationDelaySeconds) {
+ return new HystrixCommandConverter<>(
+ helix.createExtensionSecret(jsonWebToken, getClientId(clientId), activationDelaySeconds),
+ SECRETS_CONVERTER
+ );
+ }
+
+ @Override
+ public HystrixCommand getExtensionSecret(String clientId, String jsonWebToken) {
+ return new HystrixCommandConverter<>(
+ helix.getExtensionSecrets(jsonWebToken, getClientId(clientId)),
+ SECRETS_CONVERTER
+ );
+ }
+
+ @Override
+ public HystrixCommand revokeExtensionSecrets(String clientId, String jsonWebToken) {
+ throw new UnsupportedOperationException("There is no direct Helix replacement for this endpoint.");
+ }
+
+ @Override
+ public HystrixCommand getLiveChannelsWithExtensionActivated(String clientId, String cursor) {
+ return new HystrixCommandConverter<>(
+ helix.getExtensionLiveChannels(null, getClientId(clientId), 100, cursor),
+ LIVE_CHANNELS_CONVERTER
+ );
+ }
+
+ @Override
+ public HystrixCommand setExtensionRequiredConfiguration(String clientId, String jsonWebToken, String extensionVersion, String channelId, String requiredConfiguration) {
+ return helix.setExtensionRequiredConfiguration(
+ jsonWebToken,
+ getClientId(clientId),
+ extensionVersion,
+ requiredConfiguration,
+ channelId
+ );
+ }
+
+ @Override
+ public HystrixCommand setExtensionConfigurationSegment(String clientId, String jsonWebToken, ExtensionConfigurationSegment configurationSegment) {
+ final String extensionId = getClientId(clientId);
+ return helix.setExtensionConfigurationSegment(
+ jsonWebToken,
+ extensionId,
+ ExtensionConfigurationSegmentInput.builder()
+ .extensionId(extensionId)
+ .segment(SEGMENT_CONVERTER.apply(configurationSegment.getSegment()))
+ .broadcasterId(configurationSegment.getChannelId())
+ .content(configurationSegment.getContent())
+ .version(configurationSegment.getVersion())
+ .build()
+ );
+ }
+
+ @Override
+ public HystrixCommand