Compare commits

...

12 Commits

Author SHA1 Message Date
David Kohler
57ff1f1385 Only copy roomInfo if its valid and null check safety! (#146) 2025-09-17 18:55:10 -04:00
GitHub Action
aa1ef1f170 Update version in pom.xml 2025-09-17 01:49:13 +00:00
David Kohler
834dfa0939 MINOR 2025-09-16 21:47:23 -04:00
David Kohler
ab97affc73 Add integration for Eulerstream websocket connections and other QOL changes! (#144)
* Quick string alteration for proper state!

* Remove -1 close code and convert to standard public static value with reference!

* Convert to and use LiveClientStopType enum for disconnecting websocket from magic numbers!

* Add capability to use Eulerstream Enterprise server and websocket!
2025-09-16 21:46:56 -04:00
GitHub Action
c8120d89b2 Update version in pom.xml 2025-09-06 18:11:34 +00:00
Mathieu Bayou
d325dffdac Fix: API Key added in the body instead of param (#142)
* Fix: Add the missing API Key on send chat calls.

* Fix: Wrongly added the apiKey in the body instead of the param

* Update TikTokLiveHttpClient.java

We do not need to null check as Eulerstream verifies anyway so we can just pass whatever the value is.

* Update TikTokLiveHttpClient.java

Revert to your method, but using Header instead of Param, since HttpClient does not allow null values!

Co-authored-by: mbayou <mathieu@novasquare.io>
Co-authored-by: kohlerpop1 <70915561+kohlerpop1@users.noreply.github.com>
2025-09-06 14:09:15 -04:00
GitHub Action
dac688e9d6 Update version in pom.xml 2025-09-06 03:40:42 +00:00
kohlerpop1
92c9724108 Quick string alteration for proper state! 2025-09-05 23:37:50 -04:00
Mathieu Bayou
f7bef6bb31 Fix: Add the missing API Key on send chat calls. (#141) 2025-09-05 23:33:54 -04:00
GitHub Action
d8661fa2e3 Update version in pom.xml 2025-09-05 03:44:01 +00:00
David Kohler
fc02239d48 Develop 1.10.8 (#140)
* Add support for TikTokLinkMicBattleItemCard for battle/match power-ups
* Switch to an efficient pool of daemon threads instead of thread per websocket and sleeping!
* Implement Eulerstream send chat API endpoint!
* Add static to fields for single instance to manage all heartbeat threads. Far more efficient than 1 thread each sleeping!
* Add global comment to known its a true global singleton!
2025-09-04 23:41:18 -04:00
GitHub Action
77eeedc15c Update version in pom.xml 2025-07-20 01:00:06 +00:00
40 changed files with 693 additions and 188 deletions

View File

@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>TikTokLiveJava</artifactId> <artifactId>TikTokLiveJava</artifactId>
<groupId>io.github.jwdeveloper.tiktok</groupId> <groupId>io.github.jwdeveloper.tiktok</groupId>
<version>1.10.6-Release</version> <version>1.11.0-Release</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>API</artifactId> <artifactId>API</artifactId>

View File

@@ -30,14 +30,22 @@ import lombok.Getter;
@Getter @Getter
@EventMeta(eventType = EventType.Control) @EventMeta(eventType = EventType.Control)
public class TikTokDisconnectedEvent extends TikTokLiveClientEvent { 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; private final String reason;
public TikTokDisconnectedEvent(String reason) { public TikTokDisconnectedEvent(int code, String reason) {
this.code = code;
this.reason = reason.isBlank() ? "None" : reason; this.reason = reason.isBlank() ? "None" : reason;
} }
public static TikTokDisconnectedEvent of(String reason) public TikTokDisconnectedEvent(String reason) {
{ this(UNKNOWN_CLOSE_CODE, reason);
return new TikTokDisconnectedEvent(reason); }
public boolean isUnknownCloseCode() {
return this.code == UNKNOWN_CLOSE_CODE;
} }
} }

View File

@@ -0,0 +1,15 @@
package io.github.jwdeveloper.tiktok.data.events;
import io.github.jwdeveloper.tiktok.annotations.*;
import io.github.jwdeveloper.tiktok.data.events.common.TikTokHeaderEvent;
import io.github.jwdeveloper.tiktok.messages.webcast.WebcastLinkMicBattleItemCard;
import lombok.Getter;
@Getter
@EventMeta(eventType = EventType.Message)
public class TikTokLinkMicBattleItemCard extends TikTokHeaderEvent {
public TikTokLinkMicBattleItemCard(WebcastLinkMicBattleItemCard msg) {
super(msg.getCommon());
}
}

View File

@@ -23,6 +23,7 @@
package io.github.jwdeveloper.tiktok.data.models.battles; package io.github.jwdeveloper.tiktok.data.models.battles;
import io.github.jwdeveloper.tiktok.data.models.users.User; import io.github.jwdeveloper.tiktok.data.models.users.User;
import io.github.jwdeveloper.tiktok.messages.data.BattleUserInfo;
import io.github.jwdeveloper.tiktok.messages.enums.BattleType; import io.github.jwdeveloper.tiktok.messages.enums.BattleType;
import io.github.jwdeveloper.tiktok.messages.webcast.WebcastLinkMicBattle; import io.github.jwdeveloper.tiktok.messages.webcast.WebcastLinkMicBattle;
import lombok.Data; import lombok.Data;
@@ -72,12 +73,12 @@ public class Team {
this.hosts = List.copyOf(hosts); this.hosts = List.copyOf(hosts);
} }
public Team(WebcastLinkMicBattle.BattleUserInfo anchorInfo) { public Team(BattleUserInfo anchorInfo) {
this.hosts = List.of(new User(anchorInfo.getUser())); this.hosts = List.of(new User(anchorInfo.getUser()));
this.teamId = hosts.get(0).getId(); this.teamId = hosts.get(0).getId();
} }
public Team(WebcastLinkMicBattle.BattleUserInfo anchorInfo, WebcastLinkMicBattle.BattleComboInfo battleCombo) { public Team(BattleUserInfo anchorInfo, WebcastLinkMicBattle.BattleComboInfo battleCombo) {
this(anchorInfo); this(anchorInfo);
this.winStreak = (int) battleCombo.getComboCount(); this.winStreak = (int) battleCombo.getComboCount();
} }

View File

@@ -24,7 +24,7 @@ package io.github.jwdeveloper.tiktok.data.models.users;
import io.github.jwdeveloper.tiktok.data.models.Picture; import io.github.jwdeveloper.tiktok.data.models.Picture;
import io.github.jwdeveloper.tiktok.data.models.badges.Badge; import io.github.jwdeveloper.tiktok.data.models.badges.Badge;
import io.github.jwdeveloper.tiktok.messages.data.BattleUserArmy; import io.github.jwdeveloper.tiktok.messages.data.*;
import io.github.jwdeveloper.tiktok.messages.webcast.*; import io.github.jwdeveloper.tiktok.messages.webcast.*;
import lombok.*; import lombok.*;
@@ -140,7 +140,7 @@ public class User {
this(id, name, profileId, null, picture, 0, 0, List.of(Badge.empty())); this(id, name, profileId, null, picture, 0, 0, List.of(Badge.empty()));
} }
public User(WebcastLinkMicBattle.BattleUserInfo.BattleBaseUserInfo host) { public User(BattleUserInfo.BattleBaseUserInfo host) {
this(host.getUserId(), host.getDisplayId(), host.getNickName(), Picture.map(host.getAvatarThumb())); this(host.getUserId(), host.getDisplayId(), host.getNickName(), Picture.map(host.getAvatarThumb()));
} }

View File

@@ -22,7 +22,7 @@
*/ */
package io.github.jwdeveloper.tiktok.data.requests; 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.*; import lombok.*;
public class LiveUserData { public class LiveUserData {
@@ -43,9 +43,7 @@ public class LiveUserData {
public static class Response { public static class Response {
private final String json; private final String json;
private final UserStatus userStatus; private final UserStatus userStatus;
private final String roomId; private final LiveRoomInfo roomInfo;
private final long startTime;
private final User user;
public boolean isLiveOnline() { public boolean isLiveOnline() {
return userStatus == LiveUserData.UserStatus.Live || userStatus == LiveUserData.UserStatus.LivePaused; return userStatus == LiveUserData.UserStatus.Live || userStatus == LiveUserData.UserStatus.LivePaused;

View File

@@ -89,6 +89,16 @@ public class LiveClientSettings {
/** Throw an exception on 18+ Age Restriction */ /** Throw an exception on 18+ Age Restriction */
private boolean throwOnAgeRestriction; 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. * 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. * <p>This requires {@link #ttTargetIdc} also being set correctly for sessionid to be effective.

View File

@@ -26,6 +26,7 @@ import io.github.jwdeveloper.tiktok.data.requests.GiftsData;
import io.github.jwdeveloper.tiktok.data.requests.LiveConnectionData; import io.github.jwdeveloper.tiktok.data.requests.LiveConnectionData;
import io.github.jwdeveloper.tiktok.data.requests.LiveData; import io.github.jwdeveloper.tiktok.data.requests.LiveData;
import io.github.jwdeveloper.tiktok.data.requests.LiveUserData; import io.github.jwdeveloper.tiktok.data.requests.LiveUserData;
import io.github.jwdeveloper.tiktok.live.LiveRoomInfo;
public interface LiveHttpClient public interface LiveHttpClient
{ {
@@ -64,4 +65,6 @@ public interface LiveHttpClient
} }
LiveConnectionData.Response fetchLiveConnectionData(LiveConnectionData.Request request); LiveConnectionData.Response fetchLiveConnectionData(LiveConnectionData.Request request);
boolean sendChat(LiveRoomInfo roomInfo, String content);
} }

View File

@@ -24,6 +24,7 @@ package io.github.jwdeveloper.tiktok.live;
import io.github.jwdeveloper.tiktok.data.events.common.TikTokEvent; import io.github.jwdeveloper.tiktok.data.events.common.TikTokEvent;
import io.github.jwdeveloper.tiktok.listener.ListenersManager; import io.github.jwdeveloper.tiktok.listener.ListenersManager;
import io.github.jwdeveloper.tiktok.websocket.LiveClientStopType;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -36,7 +37,6 @@ public interface LiveClient {
*/ */
void connect(); void connect();
/** /**
* Connects in asynchronous way * Connects in asynchronous way
* When connected Consumer returns instance of LiveClient * When connected Consumer returns instance of LiveClient
@@ -48,19 +48,18 @@ public interface LiveClient {
*/ */
CompletableFuture<LiveClient> connectAsync(); CompletableFuture<LiveClient> connectAsync();
/** /**
* Disconnects the connection. * Disconnects the connection.
* @param type * @param type {@code LiveClientStopType}
* <p>0 - Normal - Initiates disconnection and returns * @see LiveClientStopType
* <p>1 - Disconnects blocking and returns after closure
* <p>2 - Disconnects and kills connection to websocket
* <p>Default {@link #disconnect()} is 0
*/ */
void disconnect(int type); void disconnect(LiveClientStopType type);
/**
* Disconnects with {@link LiveClientStopType#NORMAL}
*/
default void disconnect() { default void disconnect() {
disconnect(0); disconnect(LiveClientStopType.NORMAL);
} }
/** /**
@@ -68,7 +67,6 @@ public interface LiveClient {
*/ */
void publishEvent(TikTokEvent event); void publishEvent(TikTokEvent event);
/** /**
* @param webcastMessageName name of TikTok protocol-buffer message * @param webcastMessageName name of TikTok protocol-buffer message
* @param payloadBase64 protocol-buffer message bytes payload * @param payloadBase64 protocol-buffer message bytes payload
@@ -96,4 +94,12 @@ public interface LiveClient {
* Logger * Logger
*/ */
Logger getLogger(); Logger getLogger();
/**
* Send a chat message to the connected room
* @return true if successful, otherwise false
* @apiNote This is known to return true on some sessionIds despite failing!
* <p>We cannot fix this as it is a TikTok issue, not a library issue.
*/
boolean sendChat(String content);
} }

View File

@@ -49,5 +49,7 @@ public interface LiveRoomInfo
String getTitle(); String getTitle();
User getHost(); User getHost();
List<RankingUser> getUsersRanking(); List<RankingUser> getUsersRanking();
String getLanguage();
ConnectionState getConnectionState(); ConnectionState getConnectionState();
void copy(LiveRoomInfo roomInfo);
} }

View File

@@ -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
}

View File

@@ -27,6 +27,6 @@ import io.github.jwdeveloper.tiktok.live.LiveClient;
public interface LiveSocketClient { public interface LiveSocketClient {
void start(LiveConnectionData.Response webcastResponse, LiveClient tikTokLiveClient); void start(LiveConnectionData.Response webcastResponse, LiveClient tikTokLiveClient);
void stop(int type); void stop(LiveClientStopType type);
boolean isConnected(); boolean isConnected();
} }

View File

@@ -2121,6 +2121,24 @@ message PublicAreaMessageCommon {
} }
} }
message BattleUserInfo {
BattleBaseUserInfo user = 1;
repeated BattleRivalTag tags = 2;
message BattleBaseUserInfo {
int64 user_id = 1;
string nick_name = 2;
Image avatar_thumb = 3;
string display_id = 4;
}
message BattleRivalTag {
Image bg_image = 1;
Image icon_image = 2;
string content = 3;
}
}
message GiftModeMeta { message GiftModeMeta {
int64 gift_id = 1; int64 gift_id = 1;
string gift_name_key = 2; string gift_name_key = 2;

View File

@@ -820,4 +820,19 @@ enum BattleType {
enum BattleInviteType { enum BattleInviteType {
BATTLE_INVITE_TYPE_NORMAL = 0; BATTLE_INVITE_TYPE_NORMAL = 0;
BATTLE_INVITE_TYPE_AGAIN = 1; BATTLE_INVITE_TYPE_AGAIN = 1;
}
enum BattleCardMsgType {
BATTLE_CARD_MSG_TYPE_UNKNOWN_CARD_ACTION = 0;
BATTLE_CARD_MSG_TYPE_CARD_OBTAIN_GUIDE = 1;
BATTLE_CARD_MSG_TYPE_USE_CRITICAL_STRIKE_CARD = 2;
BATTLE_CARD_MSG_TYPE_USE_SMOKE_CARD = 3;
BATTLE_CARD_MSG_TYPE_AWARD_CARD_NOTICE = 4;
BATTLE_CARD_MSG_TYPE_USE_EXTRA_TIME_CARD = 5;
BATTLE_CARD_MSG_TYPE_USE_SPECIAL_EFFECT_CARD = 6;
BATTLE_CARD_MSG_TYPE_USE_POTION_CARD = 7;
BATTLE_CARD_MSG_TYPE_USE_WAVE_CARD = 8;
BATTLE_CARD_MSG_TYPE_SPECIAL_EFFECT_NOTICE = 9;
BATTLE_CARD_MSG_TYPE_USE_TOP_2_CARD = 10;
BATTLE_CARD_MSG_TYPE_USE_TOP_3_CARD = 11;
} }

View File

@@ -1219,24 +1219,6 @@ message WebcastLinkMicBattle {
// BattleUserInfo user_info = 2; // BattleUserInfo user_info = 2;
// } // }
message BattleUserInfo {
BattleBaseUserInfo user = 1;
repeated BattleRivalTag tags = 2;
message BattleBaseUserInfo {
int64 user_id = 1;
string nick_name = 2;
Image avatar_thumb = 3;
string display_id = 4;
}
message BattleRivalTag {
Image bg_image = 1;
Image icon_image = 2;
string content = 3;
}
}
message BattleABTestSetting { message BattleABTestSetting {
int64 uid = 1; int64 uid = 1;
BattleABTestList ab_test_list = 2; BattleABTestList ab_test_list = 2;
@@ -1471,4 +1453,139 @@ message RoomVerifyMessage {
string content = 3; string content = 3;
int64 noticeType = 4; int64 noticeType = 4;
bool closeRoom = 5; bool closeRoom = 5;
}
message WebcastLinkMicBattleItemCard {
CommonMessageData common = 1;
int64 battle_id = 2;
BattleCardMsgType msg_type = 3;
CardObtainGuide card_obtain_guide = 4;
UseCriticalStrikeCard use_critical_strike_card = 5;
UseSmokeCard use_smoke_card = 6;
AwardCardNotice award_card_notice = 7;
UseExtraTimeCard use_extra_time_card = 8;
UseSpecialEffectCard use_special_effect_card = 9;
UsePotionCard use_potion_card = 10;
UseWaveCard use_wave_card = 11;
SpecialEffectNotice special_effect_notice = 12;
UseTop2Card use_top2_card = 13;
UseTop3Card use_top3_card = 14;
message CardObtainGuide {
int32 not_in_use = 1;
}
message UseCriticalStrikeCard {
CriticalStrikeCardInfo card_info = 1;
int64 anchor_id = 2;
Text display_content = 3;
message CriticalStrikeCardInfo {
string card_name_key = 1;
Image card_image = 2;
int64 send_time_sec = 3;
BattleUserInfo send_user = 4;
int64 effect_last_duration = 5;
int64 critical_strike_rate_low = 6;
int64 critical_strike_rate_high = 7;
int64 multiple = 8;
string gift_name_key = 9;
string rule_url = 10;
int64 effect_time_sec = 11;
int64 to_anchor_id = 12;
string to_anchor_id_str = 13;
}
}
message UseSmokeCard {
CommonCardInfo card_info = 1;
int64 anchor_id = 2;
Text display_content = 3;
}
message AwardCardNotice {
Text display_content = 1;
repeated BattleUserInfo awarded_users = 2;
}
message UseExtraTimeCard {
ExtraTimeCardInfo card_info = 1;
int64 anchor_id = 2;
Text display_content = 3;
message ExtraTimeCardInfo {
string card_name_key = 1;
Image card_image = 2;
int64 send_time_sec = 3;
BattleUserInfo send_user = 4;
int64 effect_last_duration = 5;
string rule_url = 6;
int64 effect_time_sec = 7;
int64 to_anchor_id = 8;
int64 extra_duration_sec = 9;
string to_anchor_id_str = 10;
}
}
message UseSpecialEffectCard {
CommonCardInfo card_info = 1;
int64 anchor_id = 2;
Text display_content = 3;
repeated AnchorPair affected_anchor_pairs = 4;
}
message AnchorPair {
int64 source_anchor_id = 1;
int64 target_anchor_id = 2;
}
message UsePotionCard {
CommonCardInfo card_info = 1;
int64 anchor_id = 2;
Text display_content = 3;
}
message UseWaveCard {
CommonCardInfo card_info = 1;
int64 anchor_id = 2;
Text display_content = 3;
}
message CommonCardInfo {
string card_name_key = 1;
Image card_image = 2;
int64 send_time_sec = 3;
BattleUserInfo send_user = 4;
int64 effect_last_duration = 5;
string rule_url = 6;
int64 effect_time_sec = 7;
int64 to_anchor_id = 8;
string to_anchor_id_str = 9;
}
message SpecialEffectNotice {
int64 score = 1;
int64 from_user_id = 2;
int64 to_anchor_id = 3;
repeated AnchorPair affected_anchor_pairs = 4;
}
message UseTop2Card {
Top2CardInfo card_info = 1;
int64 anchor_id = 2;
Text display_content = 3;
message Top2CardInfo {
CommonCardInfo common = 1;
}
}
message UseTop3Card {
Top3CardInfo card_info = 1;
int64 anchor_id = 2;
Text display_content = 3;
message Top3CardInfo {
CommonCardInfo common = 1;
}
}
} }

View File

@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>TikTokLiveJava</artifactId> <artifactId>TikTokLiveJava</artifactId>
<groupId>io.github.jwdeveloper.tiktok</groupId> <groupId>io.github.jwdeveloper.tiktok</groupId>
<version>1.10.6-Release</version> <version>1.11.0-Release</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@@ -35,7 +35,7 @@ import io.github.jwdeveloper.tiktok.listener.ListenersManager;
import io.github.jwdeveloper.tiktok.live.*; import io.github.jwdeveloper.tiktok.live.*;
import io.github.jwdeveloper.tiktok.messages.webcast.ProtoMessageFetchResult; import io.github.jwdeveloper.tiktok.messages.webcast.ProtoMessageFetchResult;
import io.github.jwdeveloper.tiktok.models.ConnectionState; import io.github.jwdeveloper.tiktok.models.ConnectionState;
import io.github.jwdeveloper.tiktok.websocket.LiveSocketClient; import io.github.jwdeveloper.tiktok.websocket.*;
import lombok.Getter; import lombok.Getter;
import java.util.Base64; import java.util.Base64;
@@ -79,11 +79,14 @@ public class TikTokLiveClient implements LiveClient
public void connect() { public void connect() {
try { try {
tryConnect(); if (clientSettings.isUseEulerstreamWebsocket())
tryEulerConnect();
else
tryConnect();
} catch (TikTokLiveException e) { } catch (TikTokLiveException e) {
setState(ConnectionState.DISCONNECTED); setState(ConnectionState.DISCONNECTED);
tikTokEventHandler.publish(this, new TikTokErrorEvent(e)); 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()) { if (e instanceof TikTokLiveOfflineHostException && clientSettings.isRetryOnConnectionFailure()) {
try { try {
@@ -101,6 +104,17 @@ public class TikTokLiveClient implements LiveClient
} }
} }
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() { public void tryConnect() {
if (!roomInfo.hasConnectionState(ConnectionState.DISCONNECTED)) { if (!roomInfo.hasConnectionState(ConnectionState.DISCONNECTED)) {
throw new TikTokLiveException("Already connected"); throw new TikTokLiveException("Already connected");
@@ -110,8 +124,6 @@ public class TikTokLiveClient implements LiveClient
tikTokEventHandler.publish(this, new TikTokConnectingEvent()); tikTokEventHandler.publish(this, new TikTokConnectingEvent());
var userDataRequest = new LiveUserData.Request(roomInfo.getHostName()); var userDataRequest = new LiveUserData.Request(roomInfo.getHostName());
var userData = httpClient.fetchLiveUserData(userDataRequest); var userData = httpClient.fetchLiveUserData(userDataRequest);
roomInfo.setStartTime(userData.getStartTime());
roomInfo.setRoomId(userData.getRoomId());
if (userData.getUserStatus() == LiveUserData.UserStatus.Offline) if (userData.getUserStatus() == LiveUserData.UserStatus.Offline)
throw new TikTokLiveOfflineHostException("User is offline: " + roomInfo.getHostName(), userData, null); throw new TikTokLiveOfflineHostException("User is offline: " + roomInfo.getHostName(), userData, null);
@@ -119,7 +131,9 @@ public class TikTokLiveClient implements LiveClient
if (userData.getUserStatus() == LiveUserData.UserStatus.NotFound) if (userData.getUserStatus() == LiveUserData.UserStatus.NotFound)
throw new TikTokLiveUnknownHostException("User not found: " + roomInfo.getHostName(), userData, null); throw new TikTokLiveUnknownHostException("User not found: " + roomInfo.getHostName(), userData, null);
var liveDataRequest = new LiveData.Request(userData.getRoomId()); roomInfo.copy(userData.getRoomInfo());
var liveDataRequest = new LiveData.Request(userData.getRoomInfo().getRoomId());
var liveData = httpClient.fetchLiveData(liveDataRequest); var liveData = httpClient.fetchLiveData(liveDataRequest);
if (liveData.isAgeRestricted() && clientSettings.isThrowOnAgeRestriction()) if (liveData.isAgeRestricted() && clientSettings.isThrowOnAgeRestriction())
@@ -143,9 +157,9 @@ public class TikTokLiveClient implements LiveClient
throw new TikTokLivePreConnectionException(preconnectEvent); throw new TikTokLivePreConnectionException(preconnectEvent);
if (clientSettings.isFetchGifts()) 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); var liveConnectionData = httpClient.fetchLiveConnectionData(liveConnectionRequest);
webSocketClient.start(liveConnectionData, this); webSocketClient.start(liveConnectionData, this);
@@ -153,7 +167,7 @@ public class TikTokLiveClient implements LiveClient
tikTokEventHandler.publish(this, new TikTokRoomInfoEvent(roomInfo)); tikTokEventHandler.publish(this, new TikTokRoomInfoEvent(roomInfo));
} }
public void disconnect(int type) { public void disconnect(LiveClientStopType type) {
if (webSocketClient.isConnected()) if (webSocketClient.isConnected())
webSocketClient.stop(type); webSocketClient.stop(type);
if (!roomInfo.hasConnectionState(ConnectionState.DISCONNECTED)) if (!roomInfo.hasConnectionState(ConnectionState.DISCONNECTED))
@@ -183,6 +197,11 @@ public class TikTokLiveClient implements LiveClient
messageHandler.handleSingleMessage(this, message); messageHandler.handleSingleMessage(this, message);
} }
@Override
public boolean sendChat(String content) {
return httpClient.sendChat(roomInfo, content);
}
public void connectAsync(Consumer<LiveClient> onConnection) { public void connectAsync(Consumer<LiveClient> onConnection) {
connectAsync().thenAccept(onConnection); connectAsync().thenAccept(onConnection);
} }

View File

@@ -42,6 +42,7 @@ import io.github.jwdeveloper.tiktok.mappers.handlers.TikTokGiftEventHandler;
import io.github.jwdeveloper.tiktok.mappers.handlers.TikTokRoomInfoEventHandler; import io.github.jwdeveloper.tiktok.mappers.handlers.TikTokRoomInfoEventHandler;
import io.github.jwdeveloper.tiktok.mappers.handlers.TikTokSocialMediaEventHandler; import io.github.jwdeveloper.tiktok.mappers.handlers.TikTokSocialMediaEventHandler;
import io.github.jwdeveloper.tiktok.websocket.*; import io.github.jwdeveloper.tiktok.websocket.*;
import io.github.jwdeveloper.tiktok.websocket.euler.TikTokWebSocketEulerClient;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -132,12 +133,12 @@ public class TikTokLiveClientBuilder implements LiveClientBuilder {
//networking //networking
dependance.registerSingleton(HttpClientFactory.class); dependance.registerSingleton(HttpClientFactory.class);
dependance.registerSingleton(WebSocketHeartbeatTask.class); dependance.registerSingleton(WebSocketHeartbeatTask.class); // True global singleton - Static objects are located to serve as global
if (clientSettings.isOffline()) { if (clientSettings.isOffline()) {
dependance.registerSingleton(LiveSocketClient.class, TikTokWebSocketOfflineClient.class); dependance.registerSingleton(LiveSocketClient.class, TikTokWebSocketOfflineClient.class);
dependance.registerSingleton(LiveHttpClient.class, TikTokLiveHttpOfflineClient.class); dependance.registerSingleton(LiveHttpClient.class, TikTokLiveHttpOfflineClient.class);
} else { } else {
dependance.registerSingleton(LiveSocketClient.class, TikTokWebSocketClient.class); dependance.registerSingleton(LiveSocketClient.class, clientSettings.isUseEulerstreamWebsocket() ? TikTokWebSocketEulerClient.class : TikTokWebSocketClient.class);
dependance.registerSingleton(LiveHttpClient.class, TikTokLiveHttpClient.class); dependance.registerSingleton(LiveHttpClient.class, TikTokLiveHttpClient.class);
} }

View File

@@ -22,6 +22,7 @@
*/ */
package io.github.jwdeveloper.tiktok; package io.github.jwdeveloper.tiktok;
import com.google.gson.JsonObject;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
import io.github.jwdeveloper.dependance.injector.api.annotations.Inject; import io.github.jwdeveloper.dependance.injector.api.annotations.Inject;
import io.github.jwdeveloper.tiktok.common.*; import io.github.jwdeveloper.tiktok.common.*;
@@ -30,9 +31,10 @@ import io.github.jwdeveloper.tiktok.data.settings.LiveClientSettings;
import io.github.jwdeveloper.tiktok.exceptions.*; import io.github.jwdeveloper.tiktok.exceptions.*;
import io.github.jwdeveloper.tiktok.http.*; import io.github.jwdeveloper.tiktok.http.*;
import io.github.jwdeveloper.tiktok.http.mappers.*; import io.github.jwdeveloper.tiktok.http.mappers.*;
import io.github.jwdeveloper.tiktok.live.LiveRoomInfo;
import io.github.jwdeveloper.tiktok.messages.webcast.ProtoMessageFetchResult; import io.github.jwdeveloper.tiktok.messages.webcast.ProtoMessageFetchResult;
import java.net.http.HttpResponse; import java.net.http.*;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -42,16 +44,17 @@ public class TikTokLiveHttpClient implements LiveHttpClient
* <a href="https://github-wiki-see.page/m/isaackogan/TikTokLive/wiki/All-About-Signatures">Signing API by Isaac Kogan</a> * <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_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_WEB = "https://www.tiktok.com/";
private static final String TIKTOK_URL_WEBCAST = "https://webcast.tiktok.com/webcast/"; private static final String TIKTOK_URL_WEBCAST = "https://webcast.tiktok.com/webcast/";
public static final String TIKTOK_ROOM_GIFTS_URL = TIKTOK_URL_WEBCAST+"gift/list/"; private static final String TIKTOK_ROOM_GIFTS_URL = TIKTOK_URL_WEBCAST+"gift/list/";
private static final String TIKTOK_ROOM_INFO_URL = TIKTOK_URL_WEBCAST + "room/info";
public static final int TIKTOK_AGE_RESTRICTED_CODE = 4003110; public static final int TIKTOK_AGE_RESTRICTED_CODE = 4003110;
private final HttpClientFactory httpFactory; private final HttpClientFactory httpFactory;
private final LiveClientSettings clientSettings; private final LiveClientSettings clientSettings;
private final LiveUserDataMapper liveUserDataMapper;
private final LiveDataMapper liveDataMapper;
private final GiftsDataMapper giftsDataMapper;
private final Logger logger; private final Logger logger;
@Inject @Inject
@@ -59,9 +62,6 @@ public class TikTokLiveHttpClient implements LiveHttpClient
this.httpFactory = factory; this.httpFactory = factory;
this.clientSettings = factory.getLiveClientSettings(); this.clientSettings = factory.getLiveClientSettings();
this.logger = LoggerFactory.create("HttpClient-"+hashCode(), clientSettings); this.logger = LoggerFactory.create("HttpClient-"+hashCode(), clientSettings);
liveUserDataMapper = new LiveUserDataMapper();
liveDataMapper = new LiveDataMapper();
giftsDataMapper = new GiftsDataMapper();
} }
public TikTokLiveHttpClient(Consumer<LiveClientSettings> consumer) { public TikTokLiveHttpClient(Consumer<LiveClientSettings> consumer) {
@@ -91,7 +91,7 @@ public class TikTokLiveHttpClient implements LiveHttpClient
throw new TikTokLiveRequestException("Unable to fetch gifts information's - "+result); throw new TikTokLiveRequestException("Unable to fetch gifts information's - "+result);
var json = result.getContent(); var json = result.getContent();
return giftsDataMapper.mapRoom(json); return GiftsDataMapper.mapRoom(json);
} }
@Override @Override
@@ -121,7 +121,7 @@ public class TikTokLiveHttpClient implements LiveHttpClient
throw new TikTokLiveRequestException("Unable to get information's about user - "+result); throw new TikTokLiveRequestException("Unable to get information's about user - "+result);
var json = result.getContent(); var json = result.getContent();
return liveUserDataMapper.map(json, logger); return LiveUserDataMapper.map(json, logger);
} }
@Override @Override
@@ -138,8 +138,7 @@ public class TikTokLiveHttpClient implements LiveHttpClient
} }
public LiveData.Response getLiveData(LiveData.Request request) { public LiveData.Response getLiveData(LiveData.Request request) {
var url = TIKTOK_URL_WEBCAST + "room/info"; var result = httpFactory.client(TIKTOK_ROOM_INFO_URL)
var result = httpFactory.client(url)
.withParam("room_id", request.getRoomId()) .withParam("room_id", request.getRoomId())
.withCookie("sessionid", clientSettings.getSessionId()) .withCookie("sessionid", clientSettings.getSessionId())
.withCookie("tt-target-idc", clientSettings.getTtTargetIdc()) .withCookie("tt-target-idc", clientSettings.getTtTargetIdc())
@@ -150,7 +149,7 @@ public class TikTokLiveHttpClient implements LiveHttpClient
throw new TikTokLiveRequestException("Unable to get info about live room - "+result); throw new TikTokLiveRequestException("Unable to get info about live room - "+result);
var json = result.getContent(); var json = result.getContent();
return liveDataMapper.map(json); return LiveDataMapper.map(json);
} }
@Override @Override
@@ -182,6 +181,33 @@ public class TikTokLiveHttpClient implements LiveHttpClient
} }
} }
@Override
public boolean sendChat(LiveRoomInfo roomInfo, String content) {
var proxyClientSettings = clientSettings.getHttpSettings().getProxyClientSettings();
if (proxyClientSettings.isEnabled()) {
while (proxyClientSettings.hasNext()) {
try {
return requestSendChat(roomInfo, content);
} catch (TikTokProxyRequestException ignored) {}
}
}
return requestSendChat(roomInfo, content);
}
public boolean requestSendChat(LiveRoomInfo roomInfo, String content) {
JsonObject body = new JsonObject();
body.addProperty("content", content);
body.addProperty("sessionId", clientSettings.getSessionId());
body.addProperty("ttTargetIdc", clientSettings.getTtTargetIdc());
body.addProperty("roomId", roomInfo.getRoomId());
HttpClientBuilder builder = httpFactory.client(clientSettings.isUseEulerstreamEnterprise() ? TIKTOK_CHAT_ENTERPRISE_URL : TIKTOK_CHAT_URL)
.withHeader("Content-Type", "application/json");
if (clientSettings.getApiKey() != null)
builder.withHeader("x-api-key", clientSettings.getApiKey());
var result = builder.withBody(HttpRequest.BodyPublishers.ofString(body.toString())).build().toJsonResponse();
return result.isSuccess();
}
protected ActionResult<HttpResponse<byte[]>> getStartingPayload(LiveConnectionData.Request request) { protected ActionResult<HttpResponse<byte[]>> getStartingPayload(LiveConnectionData.Request request) {
var proxyClientSettings = clientSettings.getHttpSettings().getProxyClientSettings(); var proxyClientSettings = clientSettings.getHttpSettings().getProxyClientSettings();
if (proxyClientSettings.isEnabled()) { if (proxyClientSettings.isEnabled()) {
@@ -195,14 +221,14 @@ public class TikTokLiveHttpClient implements LiveHttpClient
} }
protected ActionResult<HttpResponse<byte[]>> getByteResponse(String room_id) { 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("client", "ttlive-java")
.withParam("room_id", room_id); .withParam("room_id", room_id);
if (clientSettings.getSessionId() != null) // Allows receiving of all comments and Subscribe Events if (clientSettings.getSessionId() != null) // Allows receiving of all comments and Subscribe Events
builder.withParam("session_id", clientSettings.getSessionId()); builder.withParam("session_id", clientSettings.getSessionId());
if (clientSettings.getApiKey() != null) if (clientSettings.getApiKey() != null)
builder.withParam("apiKey", clientSettings.getApiKey()); builder.withHeader("x-api-key", clientSettings.getApiKey());
var result = builder.build().toHttpResponse(HttpResponse.BodyHandlers.ofByteArray()); var result = builder.build().toHttpResponse(HttpResponse.BodyHandlers.ofByteArray());

View File

@@ -26,6 +26,7 @@ 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.User;
import io.github.jwdeveloper.tiktok.data.requests.*; import io.github.jwdeveloper.tiktok.data.requests.*;
import io.github.jwdeveloper.tiktok.http.LiveHttpClient; import io.github.jwdeveloper.tiktok.http.LiveHttpClient;
import io.github.jwdeveloper.tiktok.live.LiveRoomInfo;
import io.github.jwdeveloper.tiktok.messages.webcast.ProtoMessageFetchResult; import io.github.jwdeveloper.tiktok.messages.webcast.ProtoMessageFetchResult;
import java.net.URI; import java.net.URI;
@@ -39,26 +40,32 @@ public class TikTokLiveHttpOfflineClient implements LiveHttpClient {
@Override @Override
public LiveUserData.Response fetchLiveUserData(LiveUserData.Request request) { 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 @Override
public LiveData.Response fetchLiveData(LiveData.Request request) { public LiveData.Response fetchLiveData(LiveData.Request request) {
return new LiveData.Response("", return new LiveData.Response("",
LiveData.LiveStatus.HostOnline, LiveData.LiveStatus.HostOnline,
"offline live", "offline live",
0, 0,
0, 0,
0, 0,
false, false,
new User(0L, "offline user", new Picture("")), new User(0L, "offline user", new Picture("")),
LiveData.LiveType.SOLO); LiveData.LiveType.SOLO);
} }
@Override @Override
public LiveConnectionData.Response fetchLiveConnectionData(LiveConnectionData.Request request) { public LiveConnectionData.Response fetchLiveConnectionData(LiveConnectionData.Request request) {
return new LiveConnectionData.Response("", return new LiveConnectionData.Response("",
URI.create("https://example.live"), URI.create("https://example.live"),
ProtoMessageFetchResult.newBuilder().build()); ProtoMessageFetchResult.newBuilder().build());
}
@Override
public boolean sendChat(LiveRoomInfo roomInfo, String content) {
// DO NOTHING
return false;
} }
} }

View File

@@ -32,37 +32,44 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
@Data @Data
public class TikTokRoomInfo implements LiveRoomInfo { public class TikTokRoomInfo implements LiveRoomInfo
private String roomId; {
private String roomId;
private int likesCount;
private int viewersCount;
private int totalViewersCount;
private long startTime;
private boolean ageRestricted;
private User host;
private List<RankingUser> usersRanking = new LinkedList<>();
private String hostName;
private String title;
private String language = "en";
private ConnectionState connectionState = ConnectionState.DISCONNECTED;
private int likesCount; public boolean hasConnectionState(ConnectionState state) {
return connectionState == state;
}
private int viewersCount; public void updateRanking(List<RankingUser> rankingUsers) {
usersRanking.clear();
usersRanking.addAll(rankingUsers);
}
private int totalViewersCount; @Override
public void copy(LiveRoomInfo roomInfo) {
private long startTime; if (roomInfo == null) return;
this.roomId = roomInfo.getRoomId();
private boolean ageRestricted; this.likesCount = roomInfo.getLikesCount();
this.viewersCount = roomInfo.getViewersCount();
private User host; this.totalViewersCount = roomInfo.getTotalViewersCount();
this.startTime = roomInfo.getStartTime();
private List<RankingUser> usersRanking = new LinkedList<>(); this.ageRestricted = roomInfo.isAgeRestricted();
this.host = roomInfo.getHost();
private String hostName; this.usersRanking = roomInfo.getUsersRanking();
this.hostName = roomInfo.getHostName();
private String title; this.title = roomInfo.getTitle();
this.language = roomInfo.getLanguage();
private String language = "en"; this.connectionState = roomInfo.getConnectionState();
}
private ConnectionState connectionState = ConnectionState.DISCONNECTED;
public boolean hasConnectionState(ConnectionState state) {
return connectionState == state;
}
public void updateRanking(List<RankingUser> rankingUsers) {
usersRanking.clear();
usersRanking.addAll(rankingUsers);
}
} }

View File

@@ -40,11 +40,12 @@ public class HttpClient {
protected final HttpClientSettings httpClientSettings; protected final HttpClientSettings httpClientSettings;
protected final String url; protected final String url;
protected final HttpRequest.BodyPublisher bodyPublisher;
private final Pattern pattern = Pattern.compile("charset=(.*?)(?=&|$)"); private final Pattern pattern = Pattern.compile("charset=(.*?)(?=&|$)");
public <T> ActionResult<HttpResponse<T>> toHttpResponse(HttpResponse.BodyHandler<T> handler) { public <T> ActionResult<HttpResponse<T>> toHttpResponse(HttpResponse.BodyHandler<T> handler) {
var client = prepareClient(); var client = prepareClient();
var request = prepareGetRequest(); var request = prepareRequest();
try { try {
var response = client.send(request, handler); var response = client.send(request, handler);
var result = ActionResult.of(response); var result = ActionResult.of(response);
@@ -99,8 +100,13 @@ public class HttpClient {
return URI.create(stringUrl); return URI.create(stringUrl);
} }
protected HttpRequest prepareGetRequest() { /**
var requestBuilder = HttpRequest.newBuilder().GET(); * @return {@link HttpRequest} with default GET, otherwise POST if {@link #bodyPublisher} is not null
*/
protected HttpRequest prepareRequest() {
var requestBuilder = HttpRequest.newBuilder();
if (bodyPublisher != null)
requestBuilder.POST(bodyPublisher);
requestBuilder.uri(toUri()); requestBuilder.uri(toUri());
requestBuilder.timeout(httpClientSettings.getTimeout()); requestBuilder.timeout(httpClientSettings.getTimeout());
if (!httpClientSettings.getCookies().isEmpty()) { if (!httpClientSettings.getCookies().isEmpty()) {
@@ -124,12 +130,10 @@ public class HttpClient {
} }
protected String prepareUrlWithParameters(String url, Map<String, Object> parameters) { protected String prepareUrlWithParameters(String url, Map<String, Object> parameters) {
if (parameters.isEmpty()) { if (parameters.isEmpty())
return url; return url;
}
return url + "?" + parameters.entrySet().stream().map(entry -> return url + "?" + parameters.entrySet().stream().map(entry -> {
{
var encodedKey = URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8); var encodedKey = URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8);
var encodedValue = URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8); var encodedValue = URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8);
return encodedKey + "=" + encodedValue; return encodedKey + "=" + encodedValue;

View File

@@ -24,6 +24,7 @@ package io.github.jwdeveloper.tiktok.http;
import io.github.jwdeveloper.tiktok.data.settings.HttpClientSettings; import io.github.jwdeveloper.tiktok.data.settings.HttpClientSettings;
import java.net.http.HttpRequest;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -31,6 +32,7 @@ public class HttpClientBuilder {
private final HttpClientSettings httpClientSettings; private final HttpClientSettings httpClientSettings;
private String url; private String url;
private HttpRequest.BodyPublisher bodyPublisher;
public HttpClientBuilder(String url, HttpClientSettings httpClientSettings) { public HttpClientBuilder(String url, HttpClientSettings httpClientSettings) {
this.httpClientSettings = httpClientSettings; this.httpClientSettings = httpClientSettings;
@@ -78,10 +80,15 @@ public class HttpClientBuilder {
return this; return this;
} }
public HttpClientBuilder withBody(HttpRequest.BodyPublisher bodyPublisher) {
this.bodyPublisher = bodyPublisher;
return this;
}
public HttpClient build() { public HttpClient build() {
var proxyClientSettings = httpClientSettings.getProxyClientSettings(); var proxyClientSettings = httpClientSettings.getProxyClientSettings();
if (proxyClientSettings.isEnabled() && proxyClientSettings.hasNext()) if (proxyClientSettings.isEnabled() && proxyClientSettings.hasNext())
return new HttpProxyClient(httpClientSettings, url); return new HttpProxyClient(httpClientSettings, url, bodyPublisher);
return new HttpClient(httpClientSettings, url); return new HttpClient(httpClientSettings, url, bodyPublisher);
} }
} }

View File

@@ -40,8 +40,8 @@ public class HttpProxyClient extends HttpClient {
private final ProxyClientSettings proxySettings; private final ProxyClientSettings proxySettings;
public HttpProxyClient(HttpClientSettings httpClientSettings, String url) { public HttpProxyClient(HttpClientSettings httpClientSettings, String url, HttpRequest.BodyPublisher bodyPublisher) {
super(httpClientSettings, url); super(httpClientSettings, url, bodyPublisher);
this.proxySettings = httpClientSettings.getProxyClientSettings(); this.proxySettings = httpClientSettings.getProxyClientSettings();
} }
@@ -65,7 +65,7 @@ public class HttpProxyClient extends HttpClient {
httpClientSettings.getOnClientCreating().accept(builder); httpClientSettings.getOnClientCreating().accept(builder);
var client = builder.build(); var client = builder.build();
var request = prepareGetRequest(); var request = prepareRequest();
var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() != 200) if (response.statusCode() != 200)

View File

@@ -32,7 +32,7 @@ import java.util.List;
public class GiftsDataMapper { public class GiftsDataMapper {
public GiftsData.Response map(String json) { public static GiftsData.Response map(String json) {
var parsedJson = JsonParser.parseString(json); var parsedJson = JsonParser.parseString(json);
var jsonObject = parsedJson.getAsJsonObject(); var jsonObject = parsedJson.getAsJsonObject();
var gifts = jsonObject.entrySet() var gifts = jsonObject.entrySet()
@@ -43,7 +43,7 @@ public class GiftsDataMapper {
return new GiftsData.Response(json, gifts); return new GiftsData.Response(json, gifts);
} }
private Gift mapSingleGift(JsonElement jsonElement) { private static Gift mapSingleGift(JsonElement jsonElement) {
var jsonObject = jsonElement.getAsJsonObject(); var jsonObject = jsonElement.getAsJsonObject();
var id = jsonObject.get("id").getAsInt(); var id = jsonObject.get("id").getAsInt();
@@ -53,7 +53,7 @@ public class GiftsDataMapper {
return new Gift(id, name, diamondCost, new Picture(image), jsonObject); return new Gift(id, name, diamondCost, new Picture(image), jsonObject);
} }
public GiftsData.Response mapRoom(String json) { public static GiftsData.Response mapRoom(String json) {
var parsedJson = JsonParser.parseString(json); var parsedJson = JsonParser.parseString(json);
var jsonObject = parsedJson.getAsJsonObject(); var jsonObject = parsedJson.getAsJsonObject();
if (jsonObject.get("data") instanceof JsonObject data && data.get("gifts") instanceof JsonArray giftArray) { if (jsonObject.get("data") instanceof JsonObject data && data.get("gifts") instanceof JsonArray giftArray) {
@@ -69,7 +69,7 @@ public class GiftsDataMapper {
return new GiftsData.Response("", List.of()); return new GiftsData.Response("", List.of());
} }
private Gift mapSingleRoomGift(JsonElement jsonElement) { private static Gift mapSingleRoomGift(JsonElement jsonElement) {
var jsonObject = jsonElement.getAsJsonObject(); var jsonObject = jsonElement.getAsJsonObject();
var id = jsonObject.get("id").getAsInt(); var id = jsonObject.get("id").getAsInt();

View File

@@ -41,7 +41,7 @@ public class LiveDataMapper {
* 3 - ? * 3 - ?
* 4 - Offline * 4 - Offline
*/ */
public LiveData.Response map(String json) { public static LiveData.Response map(String json) {
var response = new LiveData.Response(); var response = new LiveData.Response();
response.setJson(json); response.setJson(json);
@@ -128,7 +128,7 @@ public class LiveDataMapper {
return response; return response;
} }
public User getUser(JsonObject jsonElement) { public static User getUser(JsonObject jsonElement) {
var id = jsonElement.get("id").getAsLong(); var id = jsonElement.get("id").getAsLong();
var name = jsonElement.get("display_id").getAsString(); var name = jsonElement.get("display_id").getAsString();
var profileName = jsonElement.get("nickname").getAsString(); var profileName = jsonElement.get("nickname").getAsString();

View File

@@ -23,6 +23,7 @@
package io.github.jwdeveloper.tiktok.http.mappers; package io.github.jwdeveloper.tiktok.http.mappers;
import com.google.gson.*; import com.google.gson.*;
import io.github.jwdeveloper.tiktok.*;
import io.github.jwdeveloper.tiktok.data.models.Picture; 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.User;
import io.github.jwdeveloper.tiktok.data.requests.LiveUserData; import io.github.jwdeveloper.tiktok.data.requests.LiveUserData;
@@ -33,7 +34,7 @@ import java.util.logging.Logger;
public class LiveUserDataMapper public class LiveUserDataMapper
{ {
public LiveUserData.Response map(String json, Logger logger) { public static LiveUserData.Response map(String json, Logger logger) {
try { try {
var jsonObject = JsonParser.parseString(json).getAsJsonObject(); var jsonObject = JsonParser.parseString(json).getAsJsonObject();
@@ -43,14 +44,14 @@ public class LiveUserDataMapper
throw new TikTokLiveRequestException("fetchRoomIdFromTiktokApi -> Unable to fetch roomID, contact the developer"); throw new TikTokLiveRequestException("fetchRoomIdFromTiktokApi -> Unable to fetch roomID, contact the developer");
} }
if (message.equals("user_not_found")) { if (message.equals("user_not_found")) {
return new LiveUserData.Response(json, LiveUserData.UserStatus.NotFound, "", -1, null); return new LiveUserData.Response(json, LiveUserData.UserStatus.NotFound, null);
} }
//live -> status 2 //live -> status 2
//live paused -> 3 //live paused -> 3
//not live -> status 4 //not live -> status 4
var element = jsonObject.get("data"); var element = jsonObject.get("data");
if (element.isJsonNull()) { if (element.isJsonNull()) {
return new LiveUserData.Response(json, LiveUserData.UserStatus.NotFound, "", -1, null); return new LiveUserData.Response(json, LiveUserData.UserStatus.NotFound, null);
} }
var data = element.getAsJsonObject(); var data = element.getAsJsonObject();
var user = data.getAsJsonObject("user"); var user = data.getAsJsonObject("user");
@@ -58,8 +59,17 @@ public class LiveUserDataMapper
var roomId = user.get("roomId").getAsString(); var roomId = user.get("roomId").getAsString();
var status = user.get("status").getAsInt(); var status = user.get("status").getAsInt();
TikTokRoomInfo roomInfo = new TikTokRoomInfo();
roomInfo.setRoomId(roomId);
var liveRoom = data.getAsJsonObject("liveRoom"); var liveRoom = data.getAsJsonObject("liveRoom");
long startTime = liveRoom.get("startTime").getAsLong();
roomInfo.setTitle(liveRoom.get("title").getAsString());
roomInfo.setStartTime(liveRoom.get("startTime").getAsLong());
roomInfo.setTitle(liveRoom.get("title").getAsString());
roomInfo.setViewersCount(liveRoom.getAsJsonObject("liveRoomStats").get("userCount").getAsInt());
roomInfo.setTotalViewersCount(liveRoom.getAsJsonObject("liveRoomStats").get("enterCount").getAsInt());
roomInfo.setAgeRestricted(jsonObject.get("statusCode").getAsInt() == TikTokLiveHttpClient.TIKTOK_AGE_RESTRICTED_CODE);
var statusEnum = switch (status) { var statusEnum = switch (status) {
case 2 -> LiveUserData.UserStatus.Live; case 2 -> LiveUserData.UserStatus.Live;
@@ -78,10 +88,13 @@ public class LiveUserDataMapper
stats.get("followerCount").getAsLong(), stats.get("followerCount").getAsLong(),
List.of()); List.of());
return new LiveUserData.Response(json, statusEnum, roomId, startTime, foundUser); roomInfo.setHost(foundUser);
roomInfo.setHostName(foundUser.getName());
return new LiveUserData.Response(json, statusEnum, roomInfo);
} catch (JsonSyntaxException | IllegalStateException e) { } catch (JsonSyntaxException | IllegalStateException e) {
logger.warning("Malformed Json: '"+json+"' - Error Message: "+e.getMessage()); logger.warning("Malformed Json: '"+json+"' - Error Message: "+e.getMessage());
return new LiveUserData.Response(json, LiveUserData.UserStatus.NotFound, "", -1, null); return new LiveUserData.Response(json, LiveUserData.UserStatus.NotFound, null);
} }
} }
} }

View File

@@ -104,7 +104,7 @@ public class MessagesMapperFactory {
var message = mapperHelper.bytesToWebcastObject(inputBytes, WebcastLinkMicArmies.class); var message = mapperHelper.bytesToWebcastObject(inputBytes, WebcastLinkMicArmies.class);
return MappingResult.of(message, new TikTokLinkMicArmiesEvent(message)); return MappingResult.of(message, new TikTokLinkMicArmiesEvent(message));
}); });
mapper.forMessage(WebcastLinkMessage.class, ((inputBytes, messageName, mapperHelper) -> { mapper.forMessage(WebcastLinkMessage.class, (inputBytes, messageName, mapperHelper) -> {
var message = mapperHelper.bytesToWebcastObject(inputBytes, WebcastLinkMessage.class); var message = mapperHelper.bytesToWebcastObject(inputBytes, WebcastLinkMessage.class);
return MappingResult.of(message, switch (message.getMessageType()) { return MappingResult.of(message, switch (message.getMessageType()) {
case TYPE_LINKER_INVITE -> new TikTokLinkInviteEvent(message); case TYPE_LINKER_INVITE -> new TikTokLinkInviteEvent(message);
@@ -116,8 +116,7 @@ public class MessagesMapperFactory {
case TYPE_LINKER_KICK_OUT -> new TikTokLinkKickOutEvent(message); case TYPE_LINKER_KICK_OUT -> new TikTokLinkKickOutEvent(message);
case TYPE_LINKER_LINKED_LIST_CHANGE -> new TikTokLinkLinkedListChangeEvent(message); case TYPE_LINKER_LINKED_LIST_CHANGE -> new TikTokLinkLinkedListChangeEvent(message);
case TYPE_LINKER_UPDATE_USER -> new TikTokLinkUpdateUserEvent(message); case TYPE_LINKER_UPDATE_USER -> new TikTokLinkUpdateUserEvent(message);
case TYPE_LINKER_WAITING_LIST_CHANGE, TYPE_LINKER_WAITING_LIST_CHANGE_V2 -> case TYPE_LINKER_WAITING_LIST_CHANGE, TYPE_LINKER_WAITING_LIST_CHANGE_V2 -> new TikTokLinkWaitListChangeEvent(message);
new TikTokLinkWaitListChangeEvent(message);
case TYPE_LINKER_MUTE -> new TikTokLinkMuteEvent(message); case TYPE_LINKER_MUTE -> new TikTokLinkMuteEvent(message);
case TYPE_LINKER_MATCH -> new TikTokLinkRandomMatchEvent(message); case TYPE_LINKER_MATCH -> new TikTokLinkRandomMatchEvent(message);
case TYPE_LINKER_UPDATE_USER_SETTING -> new TikTokLinkUpdateUserSettingEvent(message); case TYPE_LINKER_UPDATE_USER_SETTING -> new TikTokLinkUpdateUserSettingEvent(message);
@@ -130,7 +129,11 @@ public class MessagesMapperFactory {
case TYPE_LINKMIC_USER_TOAST -> new TikTokLinkUserToastEvent(message); case TYPE_LINKMIC_USER_TOAST -> new TikTokLinkUserToastEvent(message);
default -> new TikTokLinkEvent(message); default -> new TikTokLinkEvent(message);
}); });
})); });
mapper.forMessage(WebcastLinkMicBattleItemCard.class, (inputBytes, messageName, mapperHelper) -> {
var message = mapperHelper.bytesToWebcastObject(inputBytes, WebcastLinkMicBattleItemCard.class);
return MappingResult.of(message, new TikTokLinkMicBattleItemCard(message));
});
// mapper.webcastObjectToConstructor(WebcastLinkMicMethod.class, TikTokLinkMicMethodEvent.class); // mapper.webcastObjectToConstructor(WebcastLinkMicMethod.class, TikTokLinkMicMethodEvent.class);
// mapper.webcastObjectToConstructor(WebcastLinkMicFanTicketMethod.class, TikTokLinkMicFanTicketEvent.class); // mapper.webcastObjectToConstructor(WebcastLinkMicFanTicketMethod.class, TikTokLinkMicFanTicketEvent.class);
@@ -149,4 +152,4 @@ public class MessagesMapperFactory {
// mapper.bytesToEvents(WebcastEnvelopeMessage.class, commonHandler::handleEnvelop); // mapper.bytesToEvents(WebcastEnvelopeMessage.class, commonHandler::handleEnvelop);
return mapper; return mapper;
} }
} }

View File

@@ -57,7 +57,7 @@ public class TikTokWebSocketClient implements LiveSocketClient {
@Override @Override
public void start(LiveConnectionData.Response connectionData, LiveClient liveClient) { public void start(LiveConnectionData.Response connectionData, LiveClient liveClient) {
if (isConnected()) if (isConnected())
stop(0); stop(LiveClientStopType.NORMAL);
messageHandler.handle(liveClient, connectionData.getWebcastResponse()); messageHandler.handle(liveClient, connectionData.getWebcastResponse());
@@ -77,7 +77,7 @@ public class TikTokWebSocketClient implements LiveSocketClient {
connectDefault(); connectDefault();
} }
private void connectDefault() { public void connectDefault() {
try { try {
webSocketClient.connect(); webSocketClient.connect();
heartbeatTask.run(webSocketClient, clientSettings.getPingInterval()); heartbeatTask.run(webSocketClient, clientSettings.getPingInterval());
@@ -129,20 +129,20 @@ public class TikTokWebSocketClient implements LiveSocketClient {
} }
} }
public void stop(int type) { public void stop(LiveClientStopType type) {
if (isConnected()) { if (isConnected()) {
switch (type) { switch (type) {
case 1 -> { case CLOSE_BLOCKING -> {
try { try {
webSocketClient.closeBlocking(); webSocketClient.closeBlocking();
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new TikTokLiveException("Failed to stop the websocket"); throw new TikTokLiveException("Failed to stop the websocket");
} }
} }
case 2 -> webSocketClient.closeConnection(CloseFrame.NORMAL, ""); case DISCONNECT -> webSocketClient.closeConnection(CloseFrame.NORMAL, "");
default -> webSocketClient.close(); default -> webSocketClient.close();
} }
heartbeatTask.stop(); heartbeatTask.stop(webSocketClient);
} }
webSocketClient = null; webSocketClient = null;
} }

View File

@@ -39,9 +39,9 @@ import java.util.*;
public class TikTokWebSocketListener extends WebSocketClient { public class TikTokWebSocketListener extends WebSocketClient {
private final LiveMessagesHandler messagesHandler; protected final LiveMessagesHandler messagesHandler;
private final LiveEventsHandler eventHandler; protected final LiveEventsHandler eventHandler;
private final LiveClient liveClient; protected final LiveClient liveClient;
public TikTokWebSocketListener(URI serverUri, public TikTokWebSocketListener(URI serverUri,
Map<String, String> httpHeaders, Map<String, String> httpHeaders,
@@ -67,7 +67,7 @@ public class TikTokWebSocketListener extends WebSocketClient {
} }
} }
private void handleBinary(byte[] buffer) { protected void handleBinary(byte[] buffer) {
var websocketPushFrameOptional = getWebcastPushFrame(buffer); var websocketPushFrameOptional = getWebcastPushFrame(buffer);
if (websocketPushFrameOptional.isEmpty()) { if (websocketPushFrameOptional.isEmpty()) {
return; return;
@@ -97,7 +97,7 @@ public class TikTokWebSocketListener extends WebSocketClient {
@Override @Override
public void onClose(int code, String reason, boolean remote) { public void onClose(int code, String reason, boolean remote) {
eventHandler.publish(liveClient, new TikTokDisconnectedEvent(reason)); eventHandler.publish(liveClient, new TikTokDisconnectedEvent(code, reason));
liveClient.disconnect(); liveClient.disconnect();
} }
@@ -111,12 +111,8 @@ public class TikTokWebSocketListener extends WebSocketClient {
private Optional<WebcastPushFrame> getWebcastPushFrame(byte[] buffer) { private Optional<WebcastPushFrame> getWebcastPushFrame(byte[] buffer) {
try { try {
var websocketMessage = WebcastPushFrame.parseFrom(buffer); return Optional.of(WebcastPushFrame.parseFrom(buffer)).filter(msg -> !msg.getPayload().isEmpty());
if (websocketMessage.getPayload().isEmpty()) { } catch (Exception e) {
return Optional.empty();
}
return Optional.of(websocketMessage);
} catch (Exception e) {
throw new TikTokProtocolBufferException("Unable to parse WebcastPushFrame", buffer, e); throw new TikTokProtocolBufferException("Unable to parse WebcastPushFrame", buffer, e);
} }
} }

View File

@@ -44,7 +44,7 @@ public class TikTokWebSocketOfflineClient implements LiveSocketClient {
} }
@Override @Override
public void stop(int type) { public void stop(LiveClientStopType type) {
if (liveClient != null) if (liveClient != null)
handler.publish(liveClient, new TikTokDisconnectedEvent("Stopping")); handler.publish(liveClient, new TikTokDisconnectedEvent("Stopping"));
} }

View File

@@ -24,41 +24,52 @@ package io.github.jwdeveloper.tiktok.websocket;
import org.java_websocket.WebSocket; import org.java_websocket.WebSocket;
import java.util.Base64; import java.util.*;
import java.util.concurrent.*;
public class WebSocketHeartbeatTask public class WebSocketHeartbeatTask
{ {
private Thread thread; // Single shared pool for all heartbeat tasks
private boolean isRunning = false; private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, r -> {
private final int MAX_TIMEOUT = 250; Thread t = new Thread(r, "heartbeat-pool");
private final int SLEEP_TIME = 500; t.setDaemon(true);
return t;
});
private static final Map<WebSocket, ScheduledFuture<?>> tasks = new ConcurrentHashMap<>();
private static final Map<WebSocket, Long> commTime = new ConcurrentHashMap<>();
private final byte[] heartbeatBytes = Base64.getDecoder().decode("MgJwYjoCaGI="); // Used to be '3A026862' aka ':\x02hb', now is '2\x02pb:\x02hb'. private final byte[] heartbeatBytes = Base64.getDecoder().decode("MgJwYjoCaGI="); // Used to be '3A026862' aka ':\x02hb', now is '2\x02pb:\x02hb'.
public void run(WebSocket webSocket, long pingTaskTime) { public void run(WebSocket webSocket, long pingTaskTime) {
stop(); stop(webSocket); // remove existing task if any
thread = new Thread(() -> heartbeatTask(webSocket, pingTaskTime), "heartbeat-task");
isRunning = true;
thread.start();
}
public void stop() { tasks.put(webSocket, scheduler.scheduleAtFixedRate(() -> {
if (thread != null)
thread.interrupt();
isRunning = false;
}
private void heartbeatTask(WebSocket webSocket, long pingTaskTime) {
while (isRunning) {
try { try {
if (webSocket.isOpen()) { if (webSocket.isOpen()) {
webSocket.send(heartbeatBytes); webSocket.send(heartbeatBytes);
Thread.sleep(pingTaskTime + (int) (Math.random() * MAX_TIMEOUT)); commTime.put(webSocket, System.currentTimeMillis());
} else } else {
Thread.sleep(SLEEP_TIME); Long time = commTime.get(webSocket);
if (time != null && System.currentTimeMillis() - time >= 60_000) // Stop if disconnected longer than 60s
stop(webSocket);
}
} catch (Exception e) { } catch (Exception e) {
//TODO we should display some kind of error message e.printStackTrace();
isRunning = false; stop(webSocket);
} }
} }, 0, pingTaskTime, TimeUnit.MILLISECONDS));
}
public void stop(WebSocket webSocket) {
ScheduledFuture<?> future = tasks.remove(webSocket);
if (future != null)
future.cancel(true);
commTime.remove(webSocket);
}
public void shutdown() {
tasks.values().forEach(f -> f.cancel(true));
commTime.clear();
scheduler.shutdownNow();
} }
} }

View File

@@ -0,0 +1,100 @@
/*
* 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.euler;
import io.github.jwdeveloper.tiktok.data.requests.LiveConnectionData;
import io.github.jwdeveloper.tiktok.data.settings.LiveClientSettings;
import io.github.jwdeveloper.tiktok.exceptions.TikTokLiveException;
import io.github.jwdeveloper.tiktok.live.*;
import io.github.jwdeveloper.tiktok.websocket.*;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.framing.CloseFrame;
import java.net.URI;
import java.util.HashMap;
public class TikTokWebSocketEulerClient implements LiveSocketClient {
private final LiveClientSettings clientSettings;
private final LiveMessagesHandler messageHandler;
private final LiveEventsHandler tikTokEventHandler;
private WebSocketClient webSocketClient;
public TikTokWebSocketEulerClient(
LiveClientSettings clientSettings,
LiveMessagesHandler messageHandler,
LiveEventsHandler tikTokEventHandler)
{
this.clientSettings = clientSettings;
this.messageHandler = messageHandler;
this.tikTokEventHandler = tikTokEventHandler;
}
@Override
public void start(LiveConnectionData.Response connectionData, LiveClient liveClient) {
if (isConnected())
stop(LiveClientStopType.NORMAL);
String url = "wss://ws.eulerstream.com?uniqueId=%s&apiKey=%s&features.rawMessages=true".formatted(liveClient.getRoomInfo().getHostName(), clientSettings.getApiKey())
+ (clientSettings.isUseEulerstreamWebsocket() ? "&features.useEnterpriseApi=true" : "");
webSocketClient = new TikTokWebSocketEulerListener(
URI.create(url),
new HashMap<>(clientSettings.getHttpSettings().getHeaders()),
clientSettings.getHttpSettings().getTimeout().toMillisPart(),
messageHandler,
tikTokEventHandler,
liveClient);
connect();
}
public void connect() {
try {
webSocketClient.connect();
} catch (Exception e) {
throw new TikTokLiveException("Failed to connect to the websocket", e);
}
}
public void stop(LiveClientStopType type) {
if (isConnected()) {
switch (type) {
case CLOSE_BLOCKING -> {
try {
webSocketClient.closeBlocking();
} catch (InterruptedException e) {
throw new TikTokLiveException("Failed to stop the websocket");
}
}
case DISCONNECT -> webSocketClient.closeConnection(CloseFrame.NORMAL, "");
default -> webSocketClient.close();
}
}
webSocketClient = null;
}
public boolean isConnected() {
return webSocketClient != null && webSocketClient.isOpen();
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.euler;
import com.google.gson.*;
import io.github.jwdeveloper.tiktok.data.events.TikTokErrorEvent;
import io.github.jwdeveloper.tiktok.data.events.room.TikTokRoomInfoEvent;
import io.github.jwdeveloper.tiktok.data.requests.LiveUserData;
import io.github.jwdeveloper.tiktok.http.mappers.LiveUserDataMapper;
import io.github.jwdeveloper.tiktok.live.*;
import io.github.jwdeveloper.tiktok.websocket.TikTokWebSocketListener;
import java.net.URI;
import java.util.Map;
public class TikTokWebSocketEulerListener extends TikTokWebSocketListener
{
public TikTokWebSocketEulerListener(URI serverUri,
Map<String, String> httpHeaders,
int connectTimeout,
LiveMessagesHandler messageHandler,
LiveEventsHandler tikTokEventHandler,
LiveClient tikTokLiveClient) {
super(serverUri, httpHeaders, connectTimeout, messageHandler, tikTokEventHandler, tikTokLiveClient);
}
@Override
public void onMessage(String raw) {
try {
JsonElement element = JsonParser.parseString(raw);
if (element instanceof JsonObject o) {
if (o.get("messages") instanceof JsonArray msgs) {
for (JsonElement msg : msgs) {
if (msg instanceof JsonObject oMsg) {
switch (oMsg.get("type").getAsString()) { // Should only receive these 2 types ever
case "workerInfo" -> liveClient.getLogger().info(oMsg.toString()); // Always 1st message
case "roomInfo" -> { // Always 2nd message
LiveUserData.Response data = LiveUserDataMapper.map(oMsg.getAsJsonObject("data").getAsJsonObject("data").getAsJsonObject("raw").toString(), liveClient.getLogger());
liveClient.getRoomInfo().copy(data.getRoomInfo());
eventHandler.publish(liveClient, new TikTokRoomInfoEvent(liveClient.getRoomInfo()));
}
}
}
}
}
} else
throw new IllegalArgumentException("Invalid JsonObject: "+element);
} catch (Exception e) {
eventHandler.publish(liveClient, new TikTokErrorEvent(e));
}
if (isOpen()) {
sendPing();
}
}
}

View File

@@ -70,7 +70,7 @@ Maven
<dependency> <dependency>
<groupId>com.github.jwdeveloper.TikTok-Live-Java</groupId> <groupId>com.github.jwdeveloper.TikTok-Live-Java</groupId>
<artifactId>Client</artifactId> <artifactId>Client</artifactId>
<version>1.10.0-Release</version> <version>1.11.0-Release</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@@ -41,7 +41,7 @@
<parent> <parent>
<artifactId>TikTokLiveJava</artifactId> <artifactId>TikTokLiveJava</artifactId>
<groupId>io.github.jwdeveloper.tiktok</groupId> <groupId>io.github.jwdeveloper.tiktok</groupId>
<version>1.10.6-Release</version> <version>1.11.0-Release</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>io.github.jwdeveloper.tiktok</groupId> <groupId>io.github.jwdeveloper.tiktok</groupId>
<artifactId>TikTokLiveJava</artifactId> <artifactId>TikTokLiveJava</artifactId>
<version>1.10.6-Release</version> <version>1.11.0-Release</version>
</parent> </parent>

View File

@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>TikTokLiveJava</artifactId> <artifactId>TikTokLiveJava</artifactId>
<groupId>io.github.jwdeveloper.tiktok</groupId> <groupId>io.github.jwdeveloper.tiktok</groupId>
<version>1.10.6-Release</version> <version>1.11.0-Release</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>extension-recorder</artifactId> <artifactId>extension-recorder</artifactId>

View File

@@ -7,7 +7,7 @@
<groupId>io.github.jwdeveloper.tiktok</groupId> <groupId>io.github.jwdeveloper.tiktok</groupId>
<artifactId>TikTokLiveJava</artifactId> <artifactId>TikTokLiveJava</artifactId>
<packaging>pom</packaging> <packaging>pom</packaging>
<version>1.10.6-Release</version> <version>1.11.0-Release</version>
<modules> <modules>
<module>API</module> <module>API</module>
<module>Client</module> <module>Client</module>

View File

@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>TikTokLiveJava</artifactId> <artifactId>TikTokLiveJava</artifactId>
<groupId>io.github.jwdeveloper.tiktok</groupId> <groupId>io.github.jwdeveloper.tiktok</groupId>
<version>1.10.6-Release</version> <version>1.11.0-Release</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>