diff --git a/API/src/main/java/io/github/jwdeveloper/tiktok/data/events/TikTokLinkMicBattleItemCard.java b/API/src/main/java/io/github/jwdeveloper/tiktok/data/events/TikTokLinkMicBattleItemCard.java new file mode 100644 index 0000000..a8258a6 --- /dev/null +++ b/API/src/main/java/io/github/jwdeveloper/tiktok/data/events/TikTokLinkMicBattleItemCard.java @@ -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()); + } +} \ No newline at end of file diff --git a/API/src/main/java/io/github/jwdeveloper/tiktok/data/models/battles/Team.java b/API/src/main/java/io/github/jwdeveloper/tiktok/data/models/battles/Team.java index 03edff7..35fa9b9 100644 --- a/API/src/main/java/io/github/jwdeveloper/tiktok/data/models/battles/Team.java +++ b/API/src/main/java/io/github/jwdeveloper/tiktok/data/models/battles/Team.java @@ -23,6 +23,7 @@ package io.github.jwdeveloper.tiktok.data.models.battles; 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.webcast.WebcastLinkMicBattle; import lombok.Data; @@ -72,12 +73,12 @@ public Team(long teamId, List hosts) { this.hosts = List.copyOf(hosts); } - public Team(WebcastLinkMicBattle.BattleUserInfo anchorInfo) { + public Team(BattleUserInfo anchorInfo) { this.hosts = List.of(new User(anchorInfo.getUser())); this.teamId = hosts.get(0).getId(); } - public Team(WebcastLinkMicBattle.BattleUserInfo anchorInfo, WebcastLinkMicBattle.BattleComboInfo battleCombo) { + public Team(BattleUserInfo anchorInfo, WebcastLinkMicBattle.BattleComboInfo battleCombo) { this(anchorInfo); this.winStreak = (int) battleCombo.getComboCount(); } diff --git a/API/src/main/java/io/github/jwdeveloper/tiktok/data/models/users/User.java b/API/src/main/java/io/github/jwdeveloper/tiktok/data/models/users/User.java index fa79f8d..7dd2346 100644 --- a/API/src/main/java/io/github/jwdeveloper/tiktok/data/models/users/User.java +++ b/API/src/main/java/io/github/jwdeveloper/tiktok/data/models/users/User.java @@ -24,7 +24,7 @@ import io.github.jwdeveloper.tiktok.data.models.Picture; 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 lombok.*; @@ -140,7 +140,7 @@ public User(long id, String name, String profileId, Picture picture) { 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())); } diff --git a/API/src/main/java/io/github/jwdeveloper/tiktok/http/LiveHttpClient.java b/API/src/main/java/io/github/jwdeveloper/tiktok/http/LiveHttpClient.java index 218bcbd..2eb671d 100644 --- a/API/src/main/java/io/github/jwdeveloper/tiktok/http/LiveHttpClient.java +++ b/API/src/main/java/io/github/jwdeveloper/tiktok/http/LiveHttpClient.java @@ -26,6 +26,7 @@ import io.github.jwdeveloper.tiktok.data.requests.LiveConnectionData; import io.github.jwdeveloper.tiktok.data.requests.LiveData; import io.github.jwdeveloper.tiktok.data.requests.LiveUserData; +import io.github.jwdeveloper.tiktok.live.LiveRoomInfo; public interface LiveHttpClient { @@ -64,4 +65,6 @@ default LiveConnectionData.Response fetchLiveConnectionData(String roomId) { } LiveConnectionData.Response fetchLiveConnectionData(LiveConnectionData.Request request); + + boolean sendChat(LiveRoomInfo roomInfo, String content); } \ No newline at end of file diff --git a/API/src/main/java/io/github/jwdeveloper/tiktok/live/LiveClient.java b/API/src/main/java/io/github/jwdeveloper/tiktok/live/LiveClient.java index 962c05b..c0ce8a4 100644 --- a/API/src/main/java/io/github/jwdeveloper/tiktok/live/LiveClient.java +++ b/API/src/main/java/io/github/jwdeveloper/tiktok/live/LiveClient.java @@ -36,7 +36,6 @@ public interface LiveClient { */ void connect(); - /** * Connects in asynchronous way * When connected Consumer returns instance of LiveClient @@ -48,7 +47,6 @@ public interface LiveClient { */ CompletableFuture connectAsync(); - /** * Disconnects the connection. * @param type @@ -68,7 +66,6 @@ default void disconnect() { */ void publishEvent(TikTokEvent event); - /** * @param webcastMessageName name of TikTok protocol-buffer message * @param payloadBase64 protocol-buffer message bytes payload @@ -96,4 +93,12 @@ default void disconnect() { * Logger */ 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! + *

We cannot fix this as it is a TikTok issue, not a library issue. + */ + boolean sendChat(String content); } \ No newline at end of file diff --git a/API/src/main/proto/data.proto b/API/src/main/proto/data.proto index e431a0b..923fba7 100644 --- a/API/src/main/proto/data.proto +++ b/API/src/main/proto/data.proto @@ -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 { int64 gift_id = 1; string gift_name_key = 2; diff --git a/API/src/main/proto/enums.proto b/API/src/main/proto/enums.proto index af6dd6c..a2499b7 100644 --- a/API/src/main/proto/enums.proto +++ b/API/src/main/proto/enums.proto @@ -820,4 +820,19 @@ enum BattleType { enum BattleInviteType { BATTLE_INVITE_TYPE_NORMAL = 0; 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; } \ No newline at end of file diff --git a/API/src/main/proto/webcast.proto b/API/src/main/proto/webcast.proto index e6298bc..6007e2f 100644 --- a/API/src/main/proto/webcast.proto +++ b/API/src/main/proto/webcast.proto @@ -1219,24 +1219,6 @@ message WebcastLinkMicBattle { // 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 { int64 uid = 1; BattleABTestList ab_test_list = 2; @@ -1471,4 +1453,139 @@ message RoomVerifyMessage { string content = 3; int64 noticeType = 4; 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; + } + } } \ No newline at end of file diff --git a/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveClient.java b/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveClient.java index 074723e..08d65a4 100644 --- a/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveClient.java +++ b/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveClient.java @@ -183,6 +183,11 @@ public void publishMessage(String webcastMessageName, byte[] payload) { messageHandler.handleSingleMessage(this, message); } + @Override + public boolean sendChat(String content) { + return httpClient.sendChat(roomInfo, content); + } + public void connectAsync(Consumer onConnection) { connectAsync().thenAccept(onConnection); } diff --git a/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveClientBuilder.java b/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveClientBuilder.java index 9e9a0b7..9370b11 100644 --- a/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveClientBuilder.java +++ b/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveClientBuilder.java @@ -132,7 +132,7 @@ public LiveClient build() { //networking 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()) { dependance.registerSingleton(LiveSocketClient.class, TikTokWebSocketOfflineClient.class); dependance.registerSingleton(LiveHttpClient.class, TikTokLiveHttpOfflineClient.class); diff --git a/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveHttpClient.java b/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveHttpClient.java index 71fd9a6..7fd9082 100644 --- a/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveHttpClient.java +++ b/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveHttpClient.java @@ -22,6 +22,7 @@ */ package io.github.jwdeveloper.tiktok; +import com.google.gson.JsonObject; import com.google.protobuf.InvalidProtocolBufferException; import io.github.jwdeveloper.dependance.injector.api.annotations.Inject; import io.github.jwdeveloper.tiktok.common.*; @@ -30,9 +31,10 @@ import io.github.jwdeveloper.tiktok.exceptions.*; import io.github.jwdeveloper.tiktok.http.*; import io.github.jwdeveloper.tiktok.http.mappers.*; +import io.github.jwdeveloper.tiktok.live.LiveRoomInfo; 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.logging.Logger; @@ -42,6 +44,7 @@ public class TikTokLiveHttpClient implements LiveHttpClient * Signing API by Isaac Kogan */ 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_WEBCAST = "https://webcast.tiktok.com/webcast/"; public static final String TIKTOK_ROOM_GIFTS_URL = TIKTOK_URL_WEBCAST+"gift/list/"; @@ -182,6 +185,33 @@ public LiveConnectionData.Response fetchLiveConnectionData(LiveConnectionData.Re } } + @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> getStartingPayload(LiveConnectionData.Request request) { var proxyClientSettings = clientSettings.getHttpSettings().getProxyClientSettings(); if (proxyClientSettings.isEnabled()) { diff --git a/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveHttpOfflineClient.java b/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveHttpOfflineClient.java index f2d5537..a9dd5be 100644 --- a/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveHttpOfflineClient.java +++ b/Client/src/main/java/io/github/jwdeveloper/tiktok/TikTokLiveHttpOfflineClient.java @@ -26,6 +26,7 @@ import io.github.jwdeveloper.tiktok.data.models.users.User; import io.github.jwdeveloper.tiktok.data.requests.*; import io.github.jwdeveloper.tiktok.http.LiveHttpClient; +import io.github.jwdeveloper.tiktok.live.LiveRoomInfo; import io.github.jwdeveloper.tiktok.messages.webcast.ProtoMessageFetchResult; import java.net.URI; @@ -45,20 +46,26 @@ public LiveUserData.Response fetchLiveUserData(LiveUserData.Request request) { @Override public LiveData.Response fetchLiveData(LiveData.Request request) { return new LiveData.Response("", - LiveData.LiveStatus.HostOnline, - "offline live", - 0, - 0, - 0, - false, - new User(0L, "offline user", new Picture("")), - LiveData.LiveType.SOLO); + LiveData.LiveStatus.HostOnline, + "offline live", + 0, + 0, + 0, + false, + new User(0L, "offline user", new Picture("")), + LiveData.LiveType.SOLO); } @Override public LiveConnectionData.Response fetchLiveConnectionData(LiveConnectionData.Request request) { return new LiveConnectionData.Response("", - URI.create("https://example.live"), - ProtoMessageFetchResult.newBuilder().build()); + URI.create("https://example.live"), + ProtoMessageFetchResult.newBuilder().build()); + } + + @Override + public boolean sendChat(LiveRoomInfo roomInfo, String content) { + // DO NOTHING + return false; } } \ No newline at end of file diff --git a/Client/src/main/java/io/github/jwdeveloper/tiktok/http/HttpClient.java b/Client/src/main/java/io/github/jwdeveloper/tiktok/http/HttpClient.java index f0fee06..678b262 100644 --- a/Client/src/main/java/io/github/jwdeveloper/tiktok/http/HttpClient.java +++ b/Client/src/main/java/io/github/jwdeveloper/tiktok/http/HttpClient.java @@ -40,11 +40,12 @@ public class HttpClient { protected final HttpClientSettings httpClientSettings; protected final String url; + protected final HttpRequest.BodyPublisher bodyPublisher; private final Pattern pattern = Pattern.compile("charset=(.*?)(?=&|$)"); public ActionResult> toHttpResponse(HttpResponse.BodyHandler handler) { var client = prepareClient(); - var request = prepareGetRequest(); + var request = prepareRequest(); try { var response = client.send(request, handler); var result = ActionResult.of(response); @@ -99,8 +100,13 @@ public URI toUri() { 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.timeout(httpClientSettings.getTimeout()); if (!httpClientSettings.getCookies().isEmpty()) { @@ -124,12 +130,10 @@ protected java.net.http.HttpClient prepareClient() { } protected String prepareUrlWithParameters(String url, Map parameters) { - if (parameters.isEmpty()) { - return url; - } + if (parameters.isEmpty()) + 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 encodedValue = URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8); return encodedKey + "=" + encodedValue; diff --git a/Client/src/main/java/io/github/jwdeveloper/tiktok/http/HttpClientBuilder.java b/Client/src/main/java/io/github/jwdeveloper/tiktok/http/HttpClientBuilder.java index dd1ab12..d00f168 100644 --- a/Client/src/main/java/io/github/jwdeveloper/tiktok/http/HttpClientBuilder.java +++ b/Client/src/main/java/io/github/jwdeveloper/tiktok/http/HttpClientBuilder.java @@ -24,6 +24,7 @@ import io.github.jwdeveloper.tiktok.data.settings.HttpClientSettings; +import java.net.http.HttpRequest; import java.util.Map; import java.util.function.Consumer; @@ -31,6 +32,7 @@ public class HttpClientBuilder { private final HttpClientSettings httpClientSettings; private String url; + private HttpRequest.BodyPublisher bodyPublisher; public HttpClientBuilder(String url, HttpClientSettings httpClientSettings) { this.httpClientSettings = httpClientSettings; @@ -78,10 +80,15 @@ public HttpClientBuilder withHeaders(Map headers) { return this; } + public HttpClientBuilder withBody(HttpRequest.BodyPublisher bodyPublisher) { + this.bodyPublisher = bodyPublisher; + return this; + } + public HttpClient build() { var proxyClientSettings = httpClientSettings.getProxyClientSettings(); if (proxyClientSettings.isEnabled() && proxyClientSettings.hasNext()) - return new HttpProxyClient(httpClientSettings, url); - return new HttpClient(httpClientSettings, url); + return new HttpProxyClient(httpClientSettings, url, bodyPublisher); + return new HttpClient(httpClientSettings, url, bodyPublisher); } } \ No newline at end of file diff --git a/Client/src/main/java/io/github/jwdeveloper/tiktok/http/HttpProxyClient.java b/Client/src/main/java/io/github/jwdeveloper/tiktok/http/HttpProxyClient.java index 31331ad..a40451b 100644 --- a/Client/src/main/java/io/github/jwdeveloper/tiktok/http/HttpProxyClient.java +++ b/Client/src/main/java/io/github/jwdeveloper/tiktok/http/HttpProxyClient.java @@ -40,8 +40,8 @@ public class HttpProxyClient extends HttpClient { private final ProxyClientSettings proxySettings; - public HttpProxyClient(HttpClientSettings httpClientSettings, String url) { - super(httpClientSettings, url); + public HttpProxyClient(HttpClientSettings httpClientSettings, String url, HttpRequest.BodyPublisher bodyPublisher) { + super(httpClientSettings, url, bodyPublisher); this.proxySettings = httpClientSettings.getProxyClientSettings(); } @@ -65,7 +65,7 @@ public ActionResult> handleHttpProxyRequest() { httpClientSettings.getOnClientCreating().accept(builder); var client = builder.build(); - var request = prepareGetRequest(); + var request = prepareRequest(); var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); if (response.statusCode() != 200) diff --git a/Client/src/main/java/io/github/jwdeveloper/tiktok/mappers/MessagesMapperFactory.java b/Client/src/main/java/io/github/jwdeveloper/tiktok/mappers/MessagesMapperFactory.java index 131de0b..ebf3a1d 100644 --- a/Client/src/main/java/io/github/jwdeveloper/tiktok/mappers/MessagesMapperFactory.java +++ b/Client/src/main/java/io/github/jwdeveloper/tiktok/mappers/MessagesMapperFactory.java @@ -104,7 +104,7 @@ public static TikTokLiveMapper create(DependanceContainer container) { var message = mapperHelper.bytesToWebcastObject(inputBytes, WebcastLinkMicArmies.class); 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); return MappingResult.of(message, switch (message.getMessageType()) { case TYPE_LINKER_INVITE -> new TikTokLinkInviteEvent(message); @@ -116,8 +116,7 @@ public static TikTokLiveMapper create(DependanceContainer container) { case TYPE_LINKER_KICK_OUT -> new TikTokLinkKickOutEvent(message); case TYPE_LINKER_LINKED_LIST_CHANGE -> new TikTokLinkLinkedListChangeEvent(message); case TYPE_LINKER_UPDATE_USER -> new TikTokLinkUpdateUserEvent(message); - case TYPE_LINKER_WAITING_LIST_CHANGE, TYPE_LINKER_WAITING_LIST_CHANGE_V2 -> - new TikTokLinkWaitListChangeEvent(message); + case TYPE_LINKER_WAITING_LIST_CHANGE, TYPE_LINKER_WAITING_LIST_CHANGE_V2 -> new TikTokLinkWaitListChangeEvent(message); case TYPE_LINKER_MUTE -> new TikTokLinkMuteEvent(message); case TYPE_LINKER_MATCH -> new TikTokLinkRandomMatchEvent(message); case TYPE_LINKER_UPDATE_USER_SETTING -> new TikTokLinkUpdateUserSettingEvent(message); @@ -130,7 +129,11 @@ public static TikTokLiveMapper create(DependanceContainer container) { case TYPE_LINKMIC_USER_TOAST -> new TikTokLinkUserToastEvent(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(WebcastLinkMicFanTicketMethod.class, TikTokLinkMicFanTicketEvent.class); @@ -149,4 +152,4 @@ public static TikTokLiveMapper create(DependanceContainer container) { // mapper.bytesToEvents(WebcastEnvelopeMessage.class, commonHandler::handleEnvelop); return mapper; } -} +} \ No newline at end of file diff --git a/Client/src/main/java/io/github/jwdeveloper/tiktok/websocket/TikTokWebSocketClient.java b/Client/src/main/java/io/github/jwdeveloper/tiktok/websocket/TikTokWebSocketClient.java index 498acf3..aa5a630 100644 --- a/Client/src/main/java/io/github/jwdeveloper/tiktok/websocket/TikTokWebSocketClient.java +++ b/Client/src/main/java/io/github/jwdeveloper/tiktok/websocket/TikTokWebSocketClient.java @@ -142,7 +142,7 @@ public void stop(int type) { case 2 -> webSocketClient.closeConnection(CloseFrame.NORMAL, ""); default -> webSocketClient.close(); } - heartbeatTask.stop(); + heartbeatTask.stop(webSocketClient); } webSocketClient = null; } diff --git a/Client/src/main/java/io/github/jwdeveloper/tiktok/websocket/WebSocketHeartbeatTask.java b/Client/src/main/java/io/github/jwdeveloper/tiktok/websocket/WebSocketHeartbeatTask.java index 504dd20..6c196b3 100644 --- a/Client/src/main/java/io/github/jwdeveloper/tiktok/websocket/WebSocketHeartbeatTask.java +++ b/Client/src/main/java/io/github/jwdeveloper/tiktok/websocket/WebSocketHeartbeatTask.java @@ -24,41 +24,52 @@ import org.java_websocket.WebSocket; -import java.util.Base64; +import java.util.*; +import java.util.concurrent.*; public class WebSocketHeartbeatTask { - private Thread thread; - private boolean isRunning = false; - private final int MAX_TIMEOUT = 250; - private final int SLEEP_TIME = 500; + // Single shared pool for all heartbeat tasks + private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, r -> { + Thread t = new Thread(r, "heartbeat-pool"); + t.setDaemon(true); + return t; + }); + private static final Map> tasks = new ConcurrentHashMap<>(); + private static final Map commTime = new ConcurrentHashMap<>(); + 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) { - stop(); - thread = new Thread(() -> heartbeatTask(webSocket, pingTaskTime), "heartbeat-task"); - isRunning = true; - thread.start(); - } - - public void stop() { - if (thread != null) - thread.interrupt(); - isRunning = false; - } + stop(webSocket); // remove existing task if any - private void heartbeatTask(WebSocket webSocket, long pingTaskTime) { - while (isRunning) { + tasks.put(webSocket, scheduler.scheduleAtFixedRate(() -> { try { if (webSocket.isOpen()) { webSocket.send(heartbeatBytes); - Thread.sleep(pingTaskTime + (int) (Math.random() * MAX_TIMEOUT)); - } else - Thread.sleep(SLEEP_TIME); + commTime.put(webSocket, System.currentTimeMillis()); + } else { + Long time = commTime.get(webSocket); + if (time != null && System.currentTimeMillis() - time >= 60_000) // Stop if disconnected longer than 60s + stop(webSocket); + } } catch (Exception e) { - //TODO we should display some kind of error message - isRunning = false; + e.printStackTrace(); + 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(); } } \ No newline at end of file