Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: forward extension module calls to helix #542

Merged
merged 3 commits into from May 4, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions rest-extensions/build.gradle.kts
Expand Up @@ -13,6 +13,7 @@ dependencies {

// Twitch4J Modules
api(project(":twitch4j-common"))
api(project(":twitch4j-helix"))
}

tasks.javadoc {
Expand Down
Expand Up @@ -73,12 +73,14 @@ HystrixCommand<ExtensionSecretList> getExtensionSecret(
* @param clientId The client ID identifying the extension when it is created.
* @param jsonWebToken Signed JWT created by the EBS, following the requirements documented in “Signing the JWT” (in Building Extensions)
* @return 204 No Content on a successful call
* @deprecated No migration path in the new Helix API.
*/
@RequestLine("DELETE /{client_id}/auth/secret")
@Headers({
"Client-Id: {client_id}",
"Authorization: Bearer {token}"
})
@Deprecated
HystrixCommand<Void> revokeExtensionSecrets(
@Param("client_id") String clientId,
@Param("token") String jsonWebToken
Expand All @@ -90,14 +92,28 @@ HystrixCommand<Void> revokeExtensionSecrets(
* A channel that just 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 clientId The client ID value assigned to the extension when it is created.
* @param cursor Cursor for forward pagination.
* @return ChannelList
*/
@RequestLine("GET /{client_id}/live_activated_channels")
@RequestLine("GET /{client_id}/live_activated_channels?cursor={cursor}")
@Headers("Client-Id: {client_id}")
HystrixCommand<ChannelList> getLiveChannelsWithExtensionActivated(
@Param("client_id") String clientId
@Param("client_id") String clientId,
@Param("cursor") String cursor
);

/**
* Returns one page of live channels that have installed and activated a specified extension.
*
* @param clientId The client ID value assigned to the extension when it is created.
* @return ChannelList
* @deprecated use {@link #getLiveChannelsWithExtensionActivated(String, String)} instead (can pass null for cursor)
*/
@Deprecated
default HystrixCommand<ChannelList> getLiveChannelsWithExtensionActivated(String clientId) {
return getLiveChannelsWithExtensionActivated(clientId, null);
}

/**
* Enable activation of a specified extension, after any required broadcaster configuration is correct.
* <p>
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -77,6 +78,12 @@ 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 compatibilityLayer = true;
iProdigy marked this conversation as resolved.
Show resolved Hide resolved

/**
* Twitch API Client (Extensions)
*
Expand Down Expand Up @@ -104,6 +111,19 @@ public TwitchExtensions build() {
if (proxyConfig != null)
proxyConfig.apply(clientBuilder);

// Helix Compatibility Layer
if (compatibilityLayer) {
return TwitchExtensionsCompatibilityLayer.builder()
.clientId(clientId)
.clientSecret(clientSecret)
.userAgent(userAgent)
.timeout(timeout)
.requestQueueSize(requestQueueSize)
.logLevel(logLevel)
.proxyConfig(proxyConfig)
.build();
}

// Feign
return HystrixFeign.builder()
.client(new OkHttpClient(clientBuilder.build()))
Expand Down
@@ -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<ConfigurationSegmentType, ExtensionSegment> SEGMENT_CONVERTER = configurationSegmentType -> {
switch (configurationSegmentType) {
case GLOBAL:
return ExtensionSegment.GLOBAL;
case DEVELOPER:
return ExtensionSegment.DEVELOPER;
case BROADCASTER:
return ExtensionSegment.BROADCASTER;
default:
return null;
}
};

Function<ExtensionSegment, ConfigurationSegmentType> 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<ExtensionState, String> STATE_CONVERTER = state -> {
switch (state) {
case IN_TEST:
return "testing";
case ASSETS_UPLOADED:
return "uploading";
default:
return state.name().toLowerCase();
}
};

Function<ReleasedExtension.Views, Map<String, Object>> VIEWS_CONVERTER = views -> {
if (views == null) return Collections.emptyMap();

final BiConsumer<ReleasedExtension.View, Map<String, Object>> addView = (v, m) -> {
final BiConsumer<String, Object> 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<String, Object> map = new HashMap<>();

final BiConsumer<String, ReleasedExtension.View> addViewMap = (s, v) -> {
if (v != null) {
final Map<String, Object> 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<ReleasedExtension, ExtensionInformation> EXTENSION_CONVERTER = ext -> {
Map<String, String> 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<ReleasedExtensionList, ExtensionInformation> EXTENSION_LIST_CONVERTER = list -> {
if (list == null || list.getData() == null || list.getData().isEmpty()) return null;
return EXTENSION_CONVERTER.apply(list.getData().get(0));
};

Function<ExtensionLiveChannel, Channel> LIVE_CHANNEL_CONVERTER = c -> new Channel(c.getBroadcasterId(), c.getBroadcasterName(), c.getGameId(), c.getTitle(), null);

Function<ExtensionLiveChannelsList, ChannelList> 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<com.github.twitch4j.helix.domain.ExtensionSecret, ExtensionSecret> SECRET_CONVERTER = secret -> new ExtensionSecret(secret.getActiveAt(), secret.getContent(), secret.getExpiresAt());

Function<ExtensionSecretsList, ExtensionSecretList> 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<ExtensionConfigurationSegmentList, Map<String, ConfigurationSegment>> CONFIG_SEGMENT_LIST_CONVERTER = list -> {
if (list == null || list.getData() == null) return null;

final Map<String, ConfigurationSegment> 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);
};

}
@@ -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<T, U> extends HystrixCommand<U> {

private final HystrixCommand<T> command;
private final Function<T, U> converter;

public HystrixCommandConverter(@NonNull HystrixCommand<T> hystrixCommand, @NonNull Function<T, U> converter) {
super(hystrixCommand.getCommandGroup());
this.command = hystrixCommand;
this.converter = converter;
}

@Override
protected U run() {
return converter.apply(command.execute());
}

}