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
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,22 @@
@Getter
@EventMeta(eventType = EventType.Control)
public class TikTokDisconnectedEvent extends TikTokLiveClientEvent {
public static int UNKNOWN_CLOSE_CODE = -1;

/** Valid CloseFrame code or -1 for unknown */
private final int code;
private final String reason;

public TikTokDisconnectedEvent(String reason) {
public TikTokDisconnectedEvent(int code, String reason) {
this.code = code;
this.reason = reason.isBlank() ? "None" : reason;
}

public static TikTokDisconnectedEvent of(String reason)
{
return new TikTokDisconnectedEvent(reason);
public TikTokDisconnectedEvent(String reason) {
this(UNKNOWN_CLOSE_CODE, reason);
}

public boolean isUnknownCloseCode() {
return this.code == UNKNOWN_CLOSE_CODE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/
package io.github.jwdeveloper.tiktok.data.requests;

import io.github.jwdeveloper.tiktok.data.models.users.User;
import io.github.jwdeveloper.tiktok.live.LiveRoomInfo;
import lombok.*;

public class LiveUserData {
Expand All @@ -43,9 +43,7 @@ public Request(String userName) {
public static class Response {
private final String json;
private final UserStatus userStatus;
private final String roomId;
private final long startTime;
private final User user;
private final LiveRoomInfo roomInfo;

public boolean isLiveOnline() {
return userStatus == LiveUserData.UserStatus.Live || userStatus == LiveUserData.UserStatus.LivePaused;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ public class LiveClientSettings {
/** Throw an exception on 18+ Age Restriction */
private boolean throwOnAgeRestriction;

/** Use Eulerstream.com websocket for events
* @apiNote Requires API Key
*/
private boolean useEulerstreamWebsocket;

/** Use Eulerstream.com enterprise endpoints
* @apiNote Requires API Key with
*/
private boolean useEulerstreamEnterprise;

/**
* Optional: Sometimes not every messages from chat are send to TikTokLiveJava to fix this issue you can set sessionId.
* <p>This requires {@link #ttTargetIdc} also being set correctly for sessionid to be effective.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import io.github.jwdeveloper.tiktok.data.events.common.TikTokEvent;
import io.github.jwdeveloper.tiktok.listener.ListenersManager;
import io.github.jwdeveloper.tiktok.websocket.LiveClientStopType;

import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
Expand All @@ -49,16 +50,16 @@ public interface LiveClient {

/**
* Disconnects the connection.
* @param type
* <p>0 - Normal - Initiates disconnection and returns
* <p>1 - Disconnects blocking and returns after closure
* <p>2 - Disconnects and kills connection to websocket
* <p>Default {@link #disconnect()} is 0
* @param type {@code LiveClientStopType}
* @see LiveClientStopType
*/
void disconnect(int type);
void disconnect(LiveClientStopType type);

/**
* Disconnects with {@link LiveClientStopType#NORMAL}
*/
default void disconnect() {
disconnect(0);
disconnect(LiveClientStopType.NORMAL);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,7 @@ public interface LiveRoomInfo
String getTitle();
User getHost();
List<RankingUser> getUsersRanking();
String getLanguage();
ConnectionState getConnectionState();
void copy(LiveRoomInfo roomInfo);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2023-2024 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok.websocket;

public enum LiveClientStopType
{
/**
* Initiates the websocket close handshake. This method does not block<br> In oder to make sure
* the connection is closed use {@link LiveClientStopType#CLOSE_BLOCKING}
*/
NORMAL,
/**
* Same as {@link LiveClientStopType#NORMAL} but blocks until the websocket closed or failed to do so.<br>
*
* @apiNote Can throw {@link InterruptedException} when/if the threads get interrupted
*/
CLOSE_BLOCKING,
/**
* This will close the connection immediately without a proper close handshake.
* The code and the message therefore won't be transferred over the wire also they will be forwarded to onClose/onWebsocketClose. */
DISCONNECT
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@

public interface LiveSocketClient {
void start(LiveConnectionData.Response webcastResponse, LiveClient tikTokLiveClient);
void stop(int type);
void stop(LiveClientStopType type);
boolean isConnected();
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import io.github.jwdeveloper.tiktok.live.*;
import io.github.jwdeveloper.tiktok.messages.webcast.ProtoMessageFetchResult;
import io.github.jwdeveloper.tiktok.models.ConnectionState;
import io.github.jwdeveloper.tiktok.websocket.LiveSocketClient;
import io.github.jwdeveloper.tiktok.websocket.*;
import lombok.Getter;

import java.util.Base64;
Expand Down Expand Up @@ -79,11 +79,14 @@ public TikTokLiveClient(

public void connect() {
try {
tryConnect();
if (clientSettings.isUseEulerstreamWebsocket())
tryEulerConnect();
else
tryConnect();
} catch (TikTokLiveException e) {
setState(ConnectionState.DISCONNECTED);
tikTokEventHandler.publish(this, new TikTokErrorEvent(e));
tikTokEventHandler.publish(this, new TikTokDisconnectedEvent("Exception: "+e.getMessage()));
tikTokEventHandler.publish(this, new TikTokDisconnectedEvent("Exception: " + e.getMessage()));

if (e instanceof TikTokLiveOfflineHostException && clientSettings.isRetryOnConnectionFailure()) {
try {
Expand All @@ -101,6 +104,17 @@ public void connect() {
}
}

private void tryEulerConnect() {
if (!roomInfo.hasConnectionState(ConnectionState.DISCONNECTED)) {
throw new TikTokLiveException("Already connected");
}

setState(ConnectionState.CONNECTING);
tikTokEventHandler.publish(this, new TikTokConnectingEvent());
webSocketClient.start(null, this);
setState(ConnectionState.CONNECTED);
}

public void tryConnect() {
if (!roomInfo.hasConnectionState(ConnectionState.DISCONNECTED)) {
throw new TikTokLiveException("Already connected");
Expand All @@ -110,16 +124,15 @@ public void tryConnect() {
tikTokEventHandler.publish(this, new TikTokConnectingEvent());
var userDataRequest = new LiveUserData.Request(roomInfo.getHostName());
var userData = httpClient.fetchLiveUserData(userDataRequest);
roomInfo.setStartTime(userData.getStartTime());
roomInfo.setRoomId(userData.getRoomId());
roomInfo.copy(userData.getRoomInfo());

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

if (userData.getUserStatus() == LiveUserData.UserStatus.NotFound)
throw new TikTokLiveUnknownHostException("User not found: " + roomInfo.getHostName(), userData, null);

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

if (liveData.isAgeRestricted() && clientSettings.isThrowOnAgeRestriction())
Expand All @@ -143,17 +156,17 @@ public void tryConnect() {
throw new TikTokLivePreConnectionException(preconnectEvent);

if (clientSettings.isFetchGifts())
giftManager.attachGiftsList(httpClient.fetchRoomGiftsData(userData.getRoomId()).getGifts());
giftManager.attachGiftsList(httpClient.fetchRoomGiftsData(userData.getRoomInfo().getRoomId()).getGifts());

var liveConnectionRequest = new LiveConnectionData.Request(userData.getRoomId());
var liveConnectionRequest = new LiveConnectionData.Request(userData.getRoomInfo().getRoomId());
var liveConnectionData = httpClient.fetchLiveConnectionData(liveConnectionRequest);
webSocketClient.start(liveConnectionData, this);

setState(ConnectionState.CONNECTED);
tikTokEventHandler.publish(this, new TikTokRoomInfoEvent(roomInfo));
}

public void disconnect(int type) {
public void disconnect(LiveClientStopType type) {
if (webSocketClient.isConnected())
webSocketClient.stop(type);
if (!roomInfo.hasConnectionState(ConnectionState.DISCONNECTED))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import io.github.jwdeveloper.tiktok.mappers.handlers.TikTokRoomInfoEventHandler;
import io.github.jwdeveloper.tiktok.mappers.handlers.TikTokSocialMediaEventHandler;
import io.github.jwdeveloper.tiktok.websocket.*;
import io.github.jwdeveloper.tiktok.websocket.euler.TikTokWebSocketEulerClient;

import java.util.*;
import java.util.concurrent.CompletableFuture;
Expand Down Expand Up @@ -137,7 +138,7 @@ public LiveClient build() {
dependance.registerSingleton(LiveSocketClient.class, TikTokWebSocketOfflineClient.class);
dependance.registerSingleton(LiveHttpClient.class, TikTokLiveHttpOfflineClient.class);
} else {
dependance.registerSingleton(LiveSocketClient.class, TikTokWebSocketClient.class);
dependance.registerSingleton(LiveSocketClient.class, clientSettings.isUseEulerstreamWebsocket() ? TikTokWebSocketEulerClient.class : TikTokWebSocketClient.class);
dependance.registerSingleton(LiveHttpClient.class, TikTokLiveHttpClient.class);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public class TikTokLiveHttpClient implements LiveHttpClient
*/
private static final String TIKTOK_SIGN_API = "https://tiktok.eulerstream.com/webcast/fetch";
private static final String TIKTOK_CHAT_URL = "https://tiktok.eulerstream.com/webcast/chat";
private static final String TIKTOK_SIGN_ENTERPRISE_API = "https://tiktok.enterprise.eulerstream.com/webcast/fetch";
private static final String TIKTOK_CHAT_ENTERPRISE_URL = "https://tiktok.enterprise.eulerstream.com/webcast/chat";
private static final String TIKTOK_URL_WEB = "https://www.tiktok.com/";
private static final String TIKTOK_URL_WEBCAST = "https://webcast.tiktok.com/webcast/";
private static final String TIKTOK_ROOM_GIFTS_URL = TIKTOK_URL_WEBCAST+"gift/list/";
Expand All @@ -53,19 +55,13 @@ public class TikTokLiveHttpClient implements LiveHttpClient

private final HttpClientFactory httpFactory;
private final LiveClientSettings clientSettings;
private final LiveUserDataMapper liveUserDataMapper;
private final LiveDataMapper liveDataMapper;
private final GiftsDataMapper giftsDataMapper;
private final Logger logger;

@Inject
public TikTokLiveHttpClient(HttpClientFactory factory) {
this.httpFactory = factory;
this.clientSettings = factory.getLiveClientSettings();
this.logger = LoggerFactory.create("HttpClient-"+hashCode(), clientSettings);
liveUserDataMapper = new LiveUserDataMapper();
liveDataMapper = new LiveDataMapper();
giftsDataMapper = new GiftsDataMapper();
}

public TikTokLiveHttpClient(Consumer<LiveClientSettings> consumer) {
Expand Down Expand Up @@ -95,7 +91,7 @@ public GiftsData.Response getRoomGiftsData(String room_id) {
throw new TikTokLiveRequestException("Unable to fetch gifts information's - "+result);

var json = result.getContent();
return giftsDataMapper.mapRoom(json);
return GiftsDataMapper.mapRoom(json);
}

@Override
Expand Down Expand Up @@ -125,7 +121,7 @@ public LiveUserData.Response getLiveUserData(LiveUserData.Request request) {
throw new TikTokLiveRequestException("Unable to get information's about user - "+result);

var json = result.getContent();
return liveUserDataMapper.map(json, logger);
return LiveUserDataMapper.map(json, logger);
}

@Override
Expand Down Expand Up @@ -153,7 +149,7 @@ public LiveData.Response getLiveData(LiveData.Request request) {
throw new TikTokLiveRequestException("Unable to get info about live room - "+result);

var json = result.getContent();
return liveDataMapper.map(json);
return LiveDataMapper.map(json);
}

@Override
Expand Down Expand Up @@ -204,10 +200,10 @@ public boolean requestSendChat(LiveRoomInfo roomInfo, String content) {
body.addProperty("sessionId", clientSettings.getSessionId());
body.addProperty("ttTargetIdc", clientSettings.getTtTargetIdc());
body.addProperty("roomId", roomInfo.getRoomId());
HttpClientBuilder builder = httpFactory.client(TIKTOK_CHAT_URL)
HttpClientBuilder builder = httpFactory.client(clientSettings.isUseEulerstreamEnterprise() ? TIKTOK_CHAT_ENTERPRISE_URL : TIKTOK_CHAT_URL)
.withHeader("Content-Type", "application/json");
if (clientSettings.getApiKey() != null)
builder.withHeader("apiKey", clientSettings.getApiKey());
builder.withHeader("x-api-key", clientSettings.getApiKey());
var result = builder.withBody(HttpRequest.BodyPublishers.ofString(body.toString())).build().toJsonResponse();
return result.isSuccess();
}
Expand All @@ -225,14 +221,14 @@ protected ActionResult<HttpResponse<byte[]>> getStartingPayload(LiveConnectionDa
}

protected ActionResult<HttpResponse<byte[]>> getByteResponse(String room_id) {
HttpClientBuilder builder = httpFactory.client(TIKTOK_SIGN_API)
HttpClientBuilder builder = httpFactory.client(clientSettings.isUseEulerstreamEnterprise() ? TIKTOK_SIGN_ENTERPRISE_API : TIKTOK_SIGN_API)
.withParam("client", "ttlive-java")
.withParam("room_id", room_id);

if (clientSettings.getSessionId() != null) // Allows receiving of all comments and Subscribe Events
builder.withParam("session_id", clientSettings.getSessionId());
if (clientSettings.getApiKey() != null)
builder.withParam("apiKey", clientSettings.getApiKey());
builder.withHeader("x-api-key", clientSettings.getApiKey());

var result = builder.build().toHttpResponse(HttpResponse.BodyHandlers.ofByteArray());

Expand All @@ -241,4 +237,4 @@ protected ActionResult<HttpResponse<byte[]>> getByteResponse(String room_id) {

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public GiftsData.Response fetchRoomGiftsData(String room_id) {

@Override
public LiveUserData.Response fetchLiveUserData(LiveUserData.Request request) {
return new LiveUserData.Response("", LiveUserData.UserStatus.Live, "offline_room_id", 0, null);
return new LiveUserData.Response("", LiveUserData.UserStatus.Live, null);
}

@Override
Expand Down
Loading