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!
This commit is contained in:
David Kohler
2025-09-04 23:41:18 -04:00
committed by GitHub
parent 77eeedc15c
commit fc02239d48
18 changed files with 321 additions and 80 deletions

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

@@ -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,7 +47,6 @@ public interface LiveClient {
*/ */
CompletableFuture<LiveClient> connectAsync(); CompletableFuture<LiveClient> connectAsync();
/** /**
* Disconnects the connection. * Disconnects the connection.
* @param type * @param type
@@ -68,7 +66,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 +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

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

@@ -183,6 +183,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

@@ -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,6 +44,7 @@ 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/"; public static final String TIKTOK_ROOM_GIFTS_URL = TIKTOK_URL_WEBCAST+"gift/list/";
@@ -182,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());
var result = httpFactory.client(TIKTOK_CHAT_URL)
.withHeader("Content-Type", "application/json")
.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()) {

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

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

@@ -142,7 +142,7 @@ public class TikTokWebSocketClient implements LiveSocketClient {
case 2 -> webSocketClient.closeConnection(CloseFrame.NORMAL, ""); case 2 -> webSocketClient.closeConnection(CloseFrame.NORMAL, "");
default -> webSocketClient.close(); default -> webSocketClient.close();
} }
heartbeatTask.stop(); heartbeatTask.stop(webSocketClient);
} }
webSocketClient = null; webSocketClient = null;
} }

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();
} }
} }