Compare commits

...

25 Commits

Author SHA1 Message Date
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
kohlerpop1
54b0216bf3 Downgrade junit jupiter to use Java 16 only! 2025-07-19 20:58:21 -04:00
David Kohler
4443fbe554 Add Paused state for live data due to false positive of returning HostNotFound when they were only paused! (#135)
* Add Paused state for live data due to false positive of returning HostNotFound when they were only paused!

* Fix accidental pom.xml comment!
2025-07-19 20:12:19 -04:00
GitHub Action
a6188d8bb0 Update version in pom.xml 2025-05-21 22:48:07 +00:00
David Kohler
81fd7dc85c Merge pull request #131 from jwdeveloper/develop-1.10.6
Add session id to websocket connection to get authenticated WS as well as optional customizable type for disconnecting websocket client in various ways.
2025-05-21 18:45:17 -04:00
kohlerpop1
7e59099793 Add session id to websocket connection to get authenticated WS as well as optional customizable type for disconnecting websocket client in various ways. 2025-05-21 18:43:14 -04:00
GitHub Action
dd2f311539 Update version in pom.xml 2025-05-19 18:47:52 +00:00
David Kohler
ba69f5f5eb Merge pull request #129 from jwdeveloper/develop-1.10.5
Add TikTok Target Identity Data Center cookie to make sessionid effective and verifiable.
2025-05-19 14:46:16 -04:00
kohlerpop1
e9a91f5741 Add TikTok Target Identity Data Center cookie to make sessionid effective and verifiable. 2025-05-19 14:40:41 -04:00
GitHub Action
053bb5e3dc Update version in pom.xml 2025-05-12 02:21:39 +00:00
David Kohler
906796dc23 Merge pull request #128 from jwdeveloper/develop-1.10.4
Fix throwing error bug when connecting using proxy!
2025-05-11 22:19:37 -04:00
kohlerpop1
162092c638 Fix throwing error bug when connecting using proxy! 2025-05-11 22:19:04 -04:00
GitHub Action
a72d134796 Update version in pom.xml 2025-05-11 02:09:04 +00:00
David Kohler
75f6368f2c Merge pull request #125 from jwdeveloper/develop-1.10.3
Change websocket connection logic!
2025-05-10 22:06:35 -04:00
kohlerpop1
b9eb0eba93 Removal of debug print statements! 2025-05-04 21:59:22 -04:00
GitHub Action
50d6d6e515 Update version in pom.xml 2025-04-25 21:00:20 +00:00
kohlerpop1
42f9fe360b Removal of debug print statements! 2025-04-25 16:58:24 -04:00
GitHub Action
dff226740c Update version in pom.xml 2025-04-25 19:42:02 +00:00
David Kohler
951d30e6a7 Merge pull request #124 from jwdeveloper/develop-1.10.1
Add additional helper methods back to TikTokLinkMicBattleEvent!
2025-04-25 15:40:08 -04:00
32 changed files with 402 additions and 136 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.0-Release</version> <version>1.10.9-Release</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>API</artifactId> <artifactId>API</artifactId>

View File

@@ -51,7 +51,6 @@ public class TikTokLinkMicArmiesEvent extends TikTokHeaderEvent {
public TikTokLinkMicArmiesEvent(WebcastLinkMicArmies msg) { public TikTokLinkMicArmiesEvent(WebcastLinkMicArmies msg) {
super(msg.getCommon()); super(msg.getCommon());
System.out.println(msg);
battleId = msg.getBattleId(); battleId = msg.getBattleId();
armies = new HashMap<>(); armies = new HashMap<>();
picture = Picture.map(msg.getGifIconImage()); picture = Picture.map(msg.getGifIconImage());

View File

@@ -51,7 +51,6 @@ public class TikTokLinkMicBattleEvent extends TikTokHeaderEvent
public TikTokLinkMicBattleEvent(WebcastLinkMicBattle msg) { public TikTokLinkMicBattleEvent(WebcastLinkMicBattle msg) {
super(msg.getCommon()); super(msg.getCommon());
System.out.println(msg);
battleId = msg.getBattleId(); battleId = msg.getBattleId();
finished = msg.getAction() == BattleAction.BATTLE_ACTION_FINISH; finished = msg.getAction() == BattleAction.BATTLE_ACTION_FINISH;
battleType = msg.getBattleSetting().getBattleType(); battleType = msg.getBattleSetting().getBattleType();

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

@@ -52,6 +52,7 @@ public class LiveData {
public enum LiveStatus { public enum LiveStatus {
HostNotFound, HostNotFound,
HostOnline, HostOnline,
HostPaused,
HostOffline, HostOffline,
} }

View File

@@ -90,11 +90,18 @@ public class LiveClientSettings {
private boolean throwOnAgeRestriction; private boolean throwOnAgeRestriction;
/** /**
* 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.
* @see <a href="https://github.com/isaackogan/TikTok-Live-Connector#send-chat-messages">Documentation: How to obtain sessionId</a> * <p>This requires {@link #ttTargetIdc} also being set correctly for sessionid to be effective.
* @apiNote This cookie is supplied by <a href="https://www.tiktok.com">TikTok</a> and can be found in your browser cookies.
*/ */
private String sessionId; private String sessionId;
/**
* Used with {@link #sessionId} to verify it is valid and return extra chat messages and 18+ content.
* @apiNote This cookie is supplied by <a href="https://www.tiktok.com">TikTok</a> and can be found in your browser cookies.
*/
private String ttTargetIdc;
/** /**
* Optional: By default roomID is fetched before connect to live, but you can set it manually * Optional: By default roomID is fetched before connect to live, but you can set it manually
*/ */

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

@@ -36,7 +36,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 +47,25 @@ public interface LiveClient {
*/ */
CompletableFuture<LiveClient> connectAsync(); CompletableFuture<LiveClient> connectAsync();
/** /**
* Disconnects the connection. * 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
*/ */
void disconnect(); void disconnect(int type);
default void disconnect() {
disconnect(0);
}
/** /**
* Use to manually invoke event * Use to manually invoke event
*/ */
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
@@ -88,4 +93,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

@@ -27,5 +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(); void stop(int type);
} 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.0-Release</version> <version>1.10.9-Release</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@@ -153,13 +153,12 @@ public class TikTokLiveClient implements LiveClient
tikTokEventHandler.publish(this, new TikTokRoomInfoEvent(roomInfo)); tikTokEventHandler.publish(this, new TikTokRoomInfoEvent(roomInfo));
} }
public void disconnect() { public void disconnect(int type) {
if (roomInfo.hasConnectionState(ConnectionState.DISCONNECTED)) { if (webSocketClient.isConnected())
return; webSocketClient.stop(type);
} if (!roomInfo.hasConnectionState(ConnectionState.DISCONNECTED))
setState(ConnectionState.DISCONNECTED); setState(ConnectionState.DISCONNECTED);
webSocketClient.stop(); }
}
private void setState(ConnectionState connectionState) { private void setState(ConnectionState connectionState) {
logger.info("TikTokLive client state: " + connectionState.name()); logger.info("TikTokLive client state: " + connectionState.name());
@@ -174,9 +173,9 @@ public class TikTokLiveClient implements LiveClient
public void publishMessage(String webcastMessageName, String payloadBase64) { public void publishMessage(String webcastMessageName, String payloadBase64) {
this.publishMessage(webcastMessageName, Base64.getDecoder().decode(payloadBase64)); this.publishMessage(webcastMessageName, Base64.getDecoder().decode(payloadBase64));
} }
@Override @Override
public void publishMessage(String webcastMessageName, byte[] payload) { public void publishMessage(String webcastMessageName, byte[] payload) {
var builder = ProtoMessageFetchResult.BaseProtoMessage.newBuilder(); var builder = ProtoMessageFetchResult.BaseProtoMessage.newBuilder();
builder.setMethod(webcastMessageName); builder.setMethod(webcastMessageName);
builder.setPayload(ByteString.copyFrom(payload)); builder.setPayload(ByteString.copyFrom(payload));
@@ -184,6 +183,10 @@ 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

@@ -132,7 +132,7 @@ 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);

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,9 +44,11 @@ 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_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;
@@ -113,6 +117,7 @@ public class TikTokLiveHttpClient implements LiveHttpClient
.withParam("uniqueId", request.getUserName()) .withParam("uniqueId", request.getUserName())
.withParam("sourceType", "54") //MAGIC NUMBER, WHAT 54 means? .withParam("sourceType", "54") //MAGIC NUMBER, WHAT 54 means?
.withCookie("sessionid", clientSettings.getSessionId()) .withCookie("sessionid", clientSettings.getSessionId())
.withCookie("tt-target-idc", clientSettings.getTtTargetIdc())
.build() .build()
.toJsonResponse(); .toJsonResponse();
@@ -137,10 +142,10 @@ 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())
.build() .build()
.toJsonResponse(); .toJsonResponse();
@@ -180,6 +185,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(TIKTOK_CHAT_URL)
.withHeader("Content-Type", "application/json");
if (clientSettings.getApiKey() != null)
builder.withHeader("apiKey", 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()) {
@@ -197,6 +229,8 @@ public class TikTokLiveHttpClient implements LiveHttpClient
.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
builder.withParam("session_id", clientSettings.getSessionId());
if (clientSettings.getApiKey() != null) if (clientSettings.getApiKey() != null)
builder.withParam("apiKey", clientSettings.getApiKey()); builder.withParam("apiKey", clientSettings.getApiKey());
@@ -207,4 +241,4 @@ public class TikTokLiveHttpClient implements LiveHttpClient
return result; return result;
} }
} }

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;
@@ -45,20 +46,26 @@ public class TikTokLiveHttpOfflineClient implements LiveHttpClient {
@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

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

@@ -61,6 +61,7 @@ public class LiveDataMapper {
var statusId = status.getAsInt(); var statusId = status.getAsInt();
var statusValue = switch (statusId) { var statusValue = switch (statusId) {
case 2 -> LiveData.LiveStatus.HostOnline; case 2 -> LiveData.LiveStatus.HostOnline;
case 3 -> LiveData.LiveStatus.HostPaused;
case 4 -> LiveData.LiveStatus.HostOffline; case 4 -> LiveData.LiveStatus.HostOffline;
default -> LiveData.LiveStatus.HostNotFound; default -> LiveData.LiveStatus.HostNotFound;
}; };

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

@@ -26,10 +26,9 @@ import io.github.jwdeveloper.tiktok.data.dto.ProxyData;
import io.github.jwdeveloper.tiktok.data.requests.LiveConnectionData; import io.github.jwdeveloper.tiktok.data.requests.LiveConnectionData;
import io.github.jwdeveloper.tiktok.data.settings.*; import io.github.jwdeveloper.tiktok.data.settings.*;
import io.github.jwdeveloper.tiktok.exceptions.*; import io.github.jwdeveloper.tiktok.exceptions.*;
import io.github.jwdeveloper.tiktok.live.LiveClient; import io.github.jwdeveloper.tiktok.live.*;
import io.github.jwdeveloper.tiktok.live.LiveEventsHandler;
import io.github.jwdeveloper.tiktok.live.LiveMessagesHandler;
import org.java_websocket.client.WebSocketClient; import org.java_websocket.client.WebSocketClient;
import org.java_websocket.framing.CloseFrame;
import javax.net.ssl.*; import javax.net.ssl.*;
import java.net.Proxy; import java.net.Proxy;
@@ -42,7 +41,6 @@ public class TikTokWebSocketClient implements LiveSocketClient {
private final LiveEventsHandler tikTokEventHandler; private final LiveEventsHandler tikTokEventHandler;
private final WebSocketHeartbeatTask heartbeatTask; private final WebSocketHeartbeatTask heartbeatTask;
private WebSocketClient webSocketClient; private WebSocketClient webSocketClient;
private boolean isConnected;
public TikTokWebSocketClient( public TikTokWebSocketClient(
LiveClientSettings clientSettings, LiveClientSettings clientSettings,
@@ -54,25 +52,23 @@ public class TikTokWebSocketClient implements LiveSocketClient {
this.messageHandler = messageHandler; this.messageHandler = messageHandler;
this.tikTokEventHandler = tikTokEventHandler; this.tikTokEventHandler = tikTokEventHandler;
this.heartbeatTask = heartbeatTask; this.heartbeatTask = heartbeatTask;
isConnected = false;
} }
@Override @Override
public void start(LiveConnectionData.Response connectionData, LiveClient liveClient) { public void start(LiveConnectionData.Response connectionData, LiveClient liveClient) {
if (isConnected) { if (isConnected())
stop(); stop(0);
}
messageHandler.handle(liveClient, connectionData.getWebcastResponse()); messageHandler.handle(liveClient, connectionData.getWebcastResponse());
var headers = new HashMap<>(clientSettings.getHttpSettings().getHeaders()); var headers = new HashMap<>(clientSettings.getHttpSettings().getHeaders());
headers.put("Cookie", connectionData.getWebsocketCookies()); headers.put("Cookie", connectionData.getWebsocketCookies());
webSocketClient = new TikTokWebSocketListener(connectionData.getWebsocketUrl(), webSocketClient = new TikTokWebSocketListener(connectionData.getWebsocketUrl(),
headers, headers,
clientSettings.getHttpSettings().getTimeout().toMillisPart(), clientSettings.getHttpSettings().getTimeout().toMillisPart(),
messageHandler, messageHandler,
tikTokEventHandler, tikTokEventHandler,
liveClient); liveClient);
ProxyClientSettings proxyClientSettings = clientSettings.getHttpSettings().getProxyClientSettings(); ProxyClientSettings proxyClientSettings = clientSettings.getHttpSettings().getProxyClientSettings();
if (proxyClientSettings.isEnabled() && proxyClientSettings.isAllowWebsocket()) if (proxyClientSettings.isEnabled() && proxyClientSettings.isAllowWebsocket())
@@ -85,9 +81,7 @@ public class TikTokWebSocketClient implements LiveSocketClient {
try { try {
webSocketClient.connect(); webSocketClient.connect();
heartbeatTask.run(webSocketClient, clientSettings.getPingInterval()); heartbeatTask.run(webSocketClient, clientSettings.getPingInterval());
isConnected = true;
} catch (Exception e) { } catch (Exception e) {
isConnected = false;
throw new TikTokLiveException("Failed to connect to the websocket", e); throw new TikTokLiveException("Failed to connect to the websocket", e);
} }
} }
@@ -117,14 +111,12 @@ public class TikTokWebSocketClient implements LiveSocketClient {
ProxyData proxyData = proxySettings.next(); ProxyData proxyData = proxySettings.next();
if (tryProxyConnection(proxySettings, proxyData)) { if (tryProxyConnection(proxySettings, proxyData)) {
heartbeatTask.run(webSocketClient, clientSettings.getPingInterval()); heartbeatTask.run(webSocketClient, clientSettings.getPingInterval());
isConnected = true; return;
break;
} }
if (proxySettings.isAutoDiscard()) if (proxySettings.isAutoDiscard())
proxySettings.remove(); proxySettings.remove();
} }
if (!isConnected) throw new TikTokLiveException("Failed to connect to the websocket");
throw new TikTokLiveException("Failed to connect to the websocket");
} }
public boolean tryProxyConnection(ProxyClientSettings proxySettings, ProxyData proxyData) { public boolean tryProxyConnection(ProxyClientSettings proxySettings, ProxyData proxyData) {
@@ -137,12 +129,25 @@ public class TikTokWebSocketClient implements LiveSocketClient {
} }
} }
public void stop() { public void stop(int type) {
if (isConnected && webSocketClient != null && webSocketClient.isOpen()) { if (isConnected()) {
webSocketClient.closeConnection(0, ""); switch (type) {
heartbeatTask.stop(); case 1 -> {
try {
webSocketClient.closeBlocking();
} catch (InterruptedException e) {
throw new TikTokLiveException("Failed to stop the websocket");
}
}
case 2 -> webSocketClient.closeConnection(CloseFrame.NORMAL, "");
default -> webSocketClient.close();
}
heartbeatTask.stop(webSocketClient);
} }
webSocketClient = null; webSocketClient = null;
isConnected = false; }
public boolean isConnected() {
return webSocketClient != null && webSocketClient.isOpen();
} }
} }

View File

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

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

@@ -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.0-Release</version> <version>1.10.9-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.0-Release</version> <version>1.10.9-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.0-Release</version> <version>1.10.9-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.0-Release</version> <version>1.10.9-Release</version>
<modules> <modules>
<module>API</module> <module>API</module>
<module>Client</module> <module>Client</module>
@@ -78,7 +78,6 @@
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
@@ -110,7 +109,7 @@
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId> <artifactId>junit-jupiter</artifactId>
<version>RELEASE</version> <version>5.9.3</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>

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.0-Release</version> <version>1.10.9-Release</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>