Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ public class LiveClientSettings {
*/
private String roomId;

/**
* Optional: API Key for increased limit to signing server
*/
private String apiKey;

public static LiveClientSettings createDefault()
{
var httpSettings = new HttpClientSettings();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
@Setter
public class ProxyClientSettings implements Iterator<ProxyData>
{
private boolean enabled, lastSuccess, autoDiscard = true, fallback = true;
private boolean enabled, autoDiscard = true, fallback = true;
private Rotation rotation = Rotation.CONSECUTIVE;
private final List<ProxyData> proxyList = new ArrayList<>();
private int index = -1;
Expand Down Expand Up @@ -63,10 +63,6 @@ public boolean hasNext() {

@Override
public ProxyData next() {
return lastSuccess ? proxyList.get(index) : rotate();
}

public ProxyData rotate() {
var nextProxy = switch (rotation)
{
case CONSECUTIVE -> {
Expand All @@ -84,12 +80,11 @@ public ProxyData rotate() {
};
onProxyUpdated.accept(nextProxy);
return nextProxy;
}
}

@Override
public void remove() {
proxyList.remove(index);
lastSuccess = false; // index is no longer valid and lastSuccess needs falsified
}

public void setIndex(int index) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,38 +27,41 @@
import io.github.jwdeveloper.tiktok.data.requests.LiveData;
import io.github.jwdeveloper.tiktok.data.requests.LiveUserData;

public interface LiveHttpClient {


public interface LiveHttpClient
{
/**
* @return list of gifts that are available in your country
*/
GiftsData.Response fetchGiftsData();

/**
* Returns information about user that is having a livestream
*
* @param userName
* @return
* @param userName name of user
* @return {@link LiveUserData.Response}
*/
LiveUserData.Response fetchLiveUserData(String userName);
default LiveUserData.Response fetchLiveUserData(String userName) {
return fetchLiveUserData(new LiveUserData.Request(userName));
}

LiveUserData.Response fetchLiveUserData(LiveUserData.Request request);

/**
* @param roomId can be obtained from browsers cookies or by invoked fetchLiveUserData
* @return
* @return {@link LiveData.Response}
*/
LiveData.Response fetchLiveData(String roomId);
default LiveData.Response fetchLiveData(String roomId) {
return fetchLiveData(new LiveData.Request(roomId));
}

LiveData.Response fetchLiveData(LiveData.Request request);


/**
* @param roomId can be obtained from browsers cookies or by invoked fetchLiveUserData
* @return
* @return {@link LiveConnectionData.Response}
*/
LiveConnectionData.Response fetchLiveConnectionData(String roomId);
default LiveConnectionData.Response fetchLiveConnectionData(String roomId) {
return fetchLiveConnectionData(new LiveConnectionData.Request(roomId));
}

LiveConnectionData.Response fetchLiveConnectionData(LiveConnectionData.Request request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
import io.github.jwdeveloper.tiktok.data.requests.LiveConnectionData;
import io.github.jwdeveloper.tiktok.data.requests.LiveData;
import io.github.jwdeveloper.tiktok.data.requests.LiveUserData;
import io.github.jwdeveloper.tiktok.exceptions.TikTokLiveException;
import io.github.jwdeveloper.tiktok.exceptions.TikTokLiveOfflineHostException;
import io.github.jwdeveloper.tiktok.exceptions.*;
import io.github.jwdeveloper.tiktok.gifts.TikTokGiftManager;
import io.github.jwdeveloper.tiktok.listener.ListenersManager;
import io.github.jwdeveloper.tiktok.listener.TikTokListenersManager;
Expand Down Expand Up @@ -127,22 +126,26 @@ public void tryConnect() {
var userData = httpClient.fetchLiveUserData(userDataRequest);
liveRoomInfo.setStartTime(userData.getStartedAtTimeStamp());
liveRoomInfo.setRoomId(userData.getRoomId());
if (userData.getUserStatus() == LiveUserData.UserStatus.Offline) {
throw new TikTokLiveOfflineHostException("User is offline: "+liveRoomInfo.getHostUser());
}
if (userData.getUserStatus() == LiveUserData.UserStatus.NotFound) {
throw new TikTokLiveOfflineHostException("User not found: "+liveRoomInfo.getHostUser());
}

if (userData.getUserStatus() == LiveUserData.UserStatus.Offline)
throw new TikTokLiveOfflineHostException("User is offline: "+liveRoomInfo.getHostName());

if (userData.getUserStatus() == LiveUserData.UserStatus.NotFound)
throw new TikTokLiveOfflineHostException("User not found: "+liveRoomInfo.getHostName());

var liveDataRequest = new LiveData.Request(userData.getRoomId());
var liveData = httpClient.fetchLiveData(liveDataRequest);

if (liveData.isAgeRestricted())
throw new TikTokLiveException("Livestream for "+liveRoomInfo.getHostName()+" is 18+ or age restricted!");

if (liveData.getLiveStatus() == LiveData.LiveStatus.HostNotFound)
throw new TikTokLiveOfflineHostException("LiveStream for "+liveRoomInfo.getHostName()+" could not be found.");

if (liveData.getLiveStatus() == LiveData.LiveStatus.HostOffline)
throw new TikTokLiveOfflineHostException("LiveStream for "+liveRoomInfo.getHostName()+" not found, is the Host offline?");

tikTokEventHandler.publish(this, new TikTokRoomDataResponseEvent(liveData));
if (liveData.getLiveStatus() == LiveData.LiveStatus.HostNotFound) {
throw new TikTokLiveOfflineHostException("LiveStream for Host name could not be found.");
}
if (liveData.getLiveStatus() == LiveData.LiveStatus.HostOffline) {
throw new TikTokLiveOfflineHostException("LiveStream for not be found, is the Host offline?");
}

liveRoomInfo.setTitle(liveData.getTitle());
liveRoomInfo.setViewersCount(liveData.getViewers());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ public TikTokLiveClientBuilder configure(Consumer<LiveClientSettings> onConfigur
}

public TikTokLiveClientBuilder addListener(TikTokEventListener listener) {
listeners.add(listener);
if (listener != null)
listeners.add(listener);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,24 @@
public class TikTokLiveHttpClient implements LiveHttpClient {

/**
* Signing API by Isaac Kogan
* https://github-wiki-see.page/m/isaackogan/TikTokLive/wiki/All-About-Signatures
*/
private static final String TIKTOK_SIGN_API = "https://tiktok.eulerstream.com/webcast/sign_url";
* <a href="https://github-wiki-see.page/m/isaackogan/TikTokLive/wiki/All-About-Signatures">Signing API by Isaac Kogan</a>
*/
private static final String TIKTOK_SIGN_API = "https://tiktok.eulerstream.com/webcast/fetch";
private static final String TIKTOK_URL_WEB = "https://www.tiktok.com/";
private static final String TIKTOK_URL_WEBCAST = "https://webcast.tiktok.com/webcast/";
public static final int TIKTOK_AGE_RESTRICTED_CODE = 4003110;

private final HttpClientFactory httpFactory;
private final LiveClientSettings clientSettings;
private final LiveUserDataMapper liveUserDataMapper;
private final LiveDataMapper liveDataMapper;
private final SignServerResponseMapper signServerResponseMapper;
private final GiftsDataMapper giftsDataMapper;

public TikTokLiveHttpClient(HttpClientFactory factory, LiveClientSettings settings) {
this.httpFactory = factory;
clientSettings = settings;
liveUserDataMapper = new LiveUserDataMapper();
liveDataMapper = new LiveDataMapper();
signServerResponseMapper = new SignServerResponseMapper();
giftsDataMapper = new GiftsDataMapper();
}

Expand Down Expand Up @@ -94,12 +92,6 @@ public GiftsData.Response fetchGiftsData() {
return giftsDataMapper.map(json);
}


@Override
public LiveUserData.Response fetchLiveUserData(String userName) {
return fetchLiveUserData(new LiveUserData.Request(userName));
}

@Override
public LiveUserData.Response fetchLiveUserData(LiveUserData.Request request) {
var url = TIKTOK_URL_WEB + "api-live/user/room";
Expand Down Expand Up @@ -136,11 +128,6 @@ public LiveUserData.Response fetchLiveUserData(LiveUserData.Request request) {
return liveUserDataMapper.map(json);
}

@Override
public LiveData.Response fetchLiveData(String roomId) {
return fetchLiveData(new LiveData.Request(roomId));
}

@Override
public LiveData.Response fetchLiveData(LiveData.Request request) {
var url = TIKTOK_URL_WEBCAST + "room/info";
Expand Down Expand Up @@ -175,20 +162,12 @@ public LiveData.Response fetchLiveData(LiveData.Request request) {
return liveDataMapper.map(json);
}

@Override
public LiveConnectionData.Response fetchLiveConnectionData(String roomId) {
return fetchLiveConnectionData(new LiveConnectionData.Request(roomId));
}

@Override
public LiveConnectionData.Response fetchLiveConnectionData(LiveConnectionData.Request request) {
HttpResponse<byte[]> credentialsResponse = getOptionalProxyResponse(request).orElseGet(()-> {
SignServerResponse signServerResponse = getSignedUrl(request.getRoomId());
return getWebsocketCredentialsResponse(signServerResponse.getSignedUrl());
});
HttpResponse<byte[]> credentialsResponse = getOptionalProxyResponse(request).orElseGet(()-> getStarterPayload(request.getRoomId()));

try {
var optionalHeader = credentialsResponse.headers().firstValue("set-cookie");
var optionalHeader = credentialsResponse.headers().firstValue("x-set-tt-cookie");
if (optionalHeader.isEmpty()) {
throw new TikTokSignServerException("Sign server did not return the set-cookie header");
}
Expand All @@ -210,49 +189,29 @@ public LiveConnectionData.Response fetchLiveConnectionData(LiveConnectionData.Re
}
}

SignServerResponse getSignedUrl(String roomId) {
var urlToSign = httpFactory
.client(TikTokLiveHttpClient.TIKTOK_URL_WEBCAST + "im/fetch")
.withParam("room_id", roomId)
.build()
.toUrl();
HttpResponse<byte[]> getStarterPayload(String room_id) {
HttpClientBuilder builder = httpFactory.client(TIKTOK_SIGN_API)
.withParam("client", "ttlive-java")
.withParam("uuc", "1")
.withParam("room_id", room_id);

if (clientSettings.getApiKey() != null)
builder.withParam("apiKey", clientSettings.getApiKey());

var optional = httpFactory
.client(TikTokLiveHttpClient.TIKTOK_SIGN_API)
.withParam("client", "ttlive-java")
.withParam("uuc", "1")
.withParam("url", urlToSign.toString())
.build()
.toJsonResponse();
var optional = builder.build().toResponse();

if (optional.isEmpty()) {
throw new TikTokSignServerException("Unable to sign url: " + urlToSign);
}

var json = optional.get();
return signServerResponseMapper.map(json);
}

HttpResponse<byte[]> getWebsocketCredentialsResponse(String signedUrl) {
var optionalResponse = httpFactory
.clientEmpty(signedUrl)
.build()
.toResponse();
if (optionalResponse.isEmpty()) {
throw new TikTokSignServerException("Unable to get websocket connection credentials");
}
return optionalResponse.get();
return optional.get();
}

Optional<HttpResponse<byte[]>> getOptionalProxyResponse(LiveConnectionData.Request request) {
var proxyClientSettings = clientSettings.getHttpSettings().getProxyClientSettings();
if (proxyClientSettings.isEnabled()) {
while (proxyClientSettings.hasNext()) {
try {
SignServerResponse signServerResponse = getSignedUrl(request.getRoomId());
HttpResponse<byte[]> credentialsResponse = getWebsocketCredentialsResponse(signServerResponse.getSignedUrl());
clientSettings.getHttpSettings().getProxyClientSettings().rotate();
HttpResponse<byte[]> credentialsResponse = getStarterPayload(request.getRoomId());
return Optional.of(credentialsResponse);
} catch (TikTokProxyRequestException | TikTokSignServerException ignored) {}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,12 @@ public Optional<HttpResponse<byte[]>> handleHttpProxyRequest() {
var request = prepareGetRequest();

var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() != 200) {
proxySettings.setLastSuccess(false);
if (response.statusCode() != 200)
continue;
}
proxySettings.setLastSuccess(true);
return Optional.of(response);
} catch (HttpConnectTimeoutException | ConnectException e) {
if (proxySettings.isAutoDiscard())
proxySettings.remove();
proxySettings.setLastSuccess(false);
throw new TikTokProxyRequestException(e);
} catch (IOException e) {
if (e.getMessage().contains("503") && proxySettings.isFallback()) // Indicates proxy protocol is not supported
Expand Down Expand Up @@ -121,14 +117,12 @@ public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {}

var response = createHttpResponse(body, toUrl(), responseInfo);

proxySettings.setLastSuccess(true);
return Optional.of(response);
} catch (IOException e) {
if (e.getMessage().contains("503") && proxySettings.isFallback()) // Indicates proxy protocol is not supported
return super.toResponse();
if (proxySettings.isAutoDiscard())
proxySettings.remove();
proxySettings.setLastSuccess(false);
throw new TikTokProxyRequestException(e);
} catch (Exception e) {
throw new TikTokLiveRequestException(e);
Expand All @@ -137,7 +131,7 @@ public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {}
throw new TikTokLiveRequestException("No more proxies available!");
} catch (NoSuchAlgorithmException | MalformedURLException | KeyManagementException e) {
// Should never be reached!
System.out.println("handleSocksProxyRequest()! If you see this message, reach us on discord!");
System.out.println("handleSocksProxyRequest: If you see this, message us on discord!");
e.printStackTrace();
return Optional.empty();
} catch (TikTokLiveRequestException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.github.jwdeveloper.tiktok.TikTokLiveHttpClient;
import io.github.jwdeveloper.tiktok.data.models.Picture;
import io.github.jwdeveloper.tiktok.data.models.users.User;
import io.github.jwdeveloper.tiktok.data.models.users.UserAttribute;
Expand Down Expand Up @@ -64,6 +65,9 @@ public LiveData.Response map(String json) {
default -> LiveData.LiveStatus.HostNotFound;
};
response.setLiveStatus(statusValue);
} else if (data.has("prompts") && jsonObject.has("status_code") &&
data.get("prompts").getAsString().isEmpty() && jsonObject.get("status_code").isJsonPrimitive()) {
response.setAgeRestricted(jsonObject.get("status_code").getAsInt() == TikTokLiveHttpClient.TIKTOK_AGE_RESTRICTED_CODE);
} else {
response.setLiveStatus(LiveData.LiveStatus.HostNotFound);
}
Expand Down
Loading