Breaking changes:

'Gift': changed from class to enum, so now you can handle
incoming gifts in switch

`Events`
- new:
     onGiftComboFinished
- Removed:
      onGiftBrodcast
- Rename:
     onGiftMessage -> onGift
     onRoomPinMessage -> onRoomPin
     onRoomMessage -> onRoom
     onLinkMessage -> onLink
     onBarrageMessage -> onBarrage
     onPollMessage -> onPoll
     onShopMessage -> onShop
     onDetectMessage -> onDetect

`GiftManager`
   added:
      registerGift
      findById
      findByName
      getGifts
   removed:
      getActiveGifts
This commit is contained in:
JW
2023-10-12 03:41:36 +02:00
parent b18ca25865
commit 2d6111ef4d
48 changed files with 657 additions and 180 deletions

View File

@@ -41,6 +41,7 @@ import io.github.jwdeveloper.tiktok.exceptions.TikTokLiveException;
import io.github.jwdeveloper.tiktok.gifts.TikTokGiftManager;
import io.github.jwdeveloper.tiktok.handlers.TikTokEventObserver;
import io.github.jwdeveloper.tiktok.handlers.TikTokMessageHandlerRegistration;
import io.github.jwdeveloper.tiktok.handlers.events.TikTokGiftEventHandler;
import io.github.jwdeveloper.tiktok.http.TikTokApiService;
import io.github.jwdeveloper.tiktok.http.TikTokCookieJar;
import io.github.jwdeveloper.tiktok.http.TikTokHttpClient;
@@ -50,6 +51,7 @@ import io.github.jwdeveloper.tiktok.listener.TikTokListenersManager;
import io.github.jwdeveloper.tiktok.live.LiveClient;
import io.github.jwdeveloper.tiktok.live.builder.EventConsumer;
import io.github.jwdeveloper.tiktok.live.builder.LiveClientBuilder;
import io.github.jwdeveloper.tiktok.mappers.TikTokGenericEventMapper;
import io.github.jwdeveloper.tiktok.utils.ConsoleColors;
import io.github.jwdeveloper.tiktok.websocket.TikTokWebSocketClient;
@@ -147,10 +149,14 @@ public class TikTokLiveClientBuilder implements LiveClientBuilder {
var apiClient = new TikTokHttpClient(cookieJar, requestFactory);
var apiService = new TikTokApiService(apiClient, logger, clientSettings);
var giftManager = new TikTokGiftManager();
var eventMapper = new TikTokGenericEventMapper();
var giftHandler = new TikTokGiftEventHandler(giftManager);
var webResponseHandler = new TikTokMessageHandlerRegistration(tikTokEventHandler,
giftManager,
tiktokRoomInfo);
tiktokRoomInfo,
eventMapper,
giftHandler
);
var webSocketClient = new TikTokWebSocketClient(logger,
cookieJar,

View File

@@ -22,7 +22,7 @@
*/
package io.github.jwdeveloper.tiktok.gifts;
import io.github.jwdeveloper.tiktok.data.models.Gift;
import io.github.jwdeveloper.tiktok.data.models.gifts.Gift;
import io.github.jwdeveloper.tiktok.data.models.Picture;
import io.github.jwdeveloper.tiktok.exceptions.TikTokLiveException;
import io.github.jwdeveloper.tiktok.live.GiftManager;

View File

@@ -23,18 +23,20 @@
package io.github.jwdeveloper.tiktok.handlers;
import io.github.jwdeveloper.tiktok.data.events.common.TikTokEvent;
import io.github.jwdeveloper.tiktok.data.events.TikTokErrorEvent;
import io.github.jwdeveloper.tiktok.data.events.common.TikTokEvent;
import io.github.jwdeveloper.tiktok.data.events.websocket.TikTokWebsocketMessageEvent;
import io.github.jwdeveloper.tiktok.data.events.websocket.TikTokWebsocketResponseEvent;
import io.github.jwdeveloper.tiktok.data.events.websocket.TikTokWebsocketUnhandledMessageEvent;
import io.github.jwdeveloper.tiktok.exceptions.TikTokLiveMessageException;
import io.github.jwdeveloper.tiktok.exceptions.TikTokMessageMappingException;
import io.github.jwdeveloper.tiktok.live.LiveClient;
import io.github.jwdeveloper.tiktok.mappers.TikTokGenericEventMapper;
import io.github.jwdeveloper.tiktok.messages.webcast.WebcastResponse;
import io.github.jwdeveloper.tiktok.utils.Stopwatch;
import java.util.Arrays;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@@ -43,21 +45,24 @@ public abstract class TikTokMessageHandler {
private final Map<String, io.github.jwdeveloper.tiktok.handler.TikTokMessageHandler> handlers;
private final TikTokEventObserver tikTokEventHandler;
protected final TikTokGenericEventMapper mapper;
public TikTokMessageHandler(TikTokEventObserver tikTokEventHandler) {
public TikTokMessageHandler(TikTokEventObserver tikTokEventHandler, TikTokGenericEventMapper mapper) {
handlers = new HashMap<>();
this.tikTokEventHandler = tikTokEventHandler;
init();
this.mapper = mapper;
}
public abstract void init();
public void registerMapping(Class<?> clazz, Function<byte[], TikTokEvent> func) {
handlers.put(clazz.getSimpleName(), messagePayload -> List.of(func.apply(messagePayload)));
}
public void registerMappings(Class<?> clazz, Function<byte[], List<TikTokEvent>> func) {
handlers.put(clazz.getSimpleName(), func::apply);
}
public void registerMapping(Class<?> input, Class<?> output) {
registerMapping(input, (e) -> mapMessageToEvent(input, output, e));
registerMapping(input, (e) -> mapper.mapToEvent(input, output, e));
}
public void handle(LiveClient client, WebcastResponse webcastResponse) {
@@ -71,18 +76,6 @@ public abstract class TikTokMessageHandler {
}
}
}
public void handleSingleMessage(LiveClient client, String type, byte[] bytes) throws Exception {
if (!handlers.containsKey(type)) {
tikTokEventHandler.publish(client, new TikTokWebsocketUnhandledMessageEvent(WebcastResponse.Message.newBuilder().setMethod(type).build()));
return;
}
var handler = handlers.get(type);
var tiktokEvent = handler.handle(bytes);
tikTokEventHandler.publish(client, new TikTokWebsocketMessageEvent(tiktokEvent, WebcastResponse.Message.newBuilder().build()));
tikTokEventHandler.publish(client, tiktokEvent);
}
public void handleSingleMessage(LiveClient client, WebcastResponse.Message message) throws Exception {
var messageClassName = message.getMethod();
@@ -91,29 +84,16 @@ public abstract class TikTokMessageHandler {
return;
}
var handler = handlers.get(messageClassName);
var tiktokEvent = handler.handle(message.getPayload().toByteArray());
tikTokEventHandler.publish(client, new TikTokWebsocketMessageEvent(tiktokEvent, message));
tikTokEventHandler.publish(client, tiktokEvent);
}
var stopwatch = new Stopwatch();
stopwatch.start();
var events = handler.handle(message.getPayload().toByteArray());
var handlingTimeInMs = stopwatch.stop();
var metadata = new TikTokWebsocketMessageEvent.MetaData(Duration.ofNanos(handlingTimeInMs));
protected TikTokEvent mapMessageToEvent(Class<?> inputClazz, Class<?> outputClass, byte[] payload) {
try {
var parseMethod = inputClazz.getDeclaredMethod("parseFrom", byte[].class);
var deserialized = parseMethod.invoke(null,payload);
var constructors = Arrays.stream(outputClass.getConstructors())
.filter(ea -> Arrays.stream(ea.getParameterTypes())
.toList()
.contains(inputClazz))
.findFirst();
if (constructors.isEmpty()) {
throw new TikTokMessageMappingException(inputClazz, outputClass, "Unable to find constructor with input class type");
}
var tiktokEvent = constructors.get().newInstance(deserialized);
return (TikTokEvent) tiktokEvent;
} catch (Exception ex) {
throw new TikTokMessageMappingException(inputClazz, outputClass, ex);
for (var event : events) {
tikTokEventHandler.publish(client, new TikTokWebsocketMessageEvent(event, message, metadata));
tikTokEventHandler.publish(client, event);
}
}
}

View File

@@ -23,11 +23,8 @@
package io.github.jwdeveloper.tiktok.handlers;
import io.github.jwdeveloper.tiktok.TikTokRoomInfo;
import io.github.jwdeveloper.tiktok.data.events.common.TikTokEvent;
import io.github.jwdeveloper.tiktok.data.events.*;
import io.github.jwdeveloper.tiktok.data.events.TikTokBarrageEvent;
import io.github.jwdeveloper.tiktok.data.events.gift.TikTokGiftComboEvent;
import io.github.jwdeveloper.tiktok.data.events.gift.TikTokGiftEvent;
import io.github.jwdeveloper.tiktok.data.events.common.TikTokEvent;
import io.github.jwdeveloper.tiktok.data.events.poll.TikTokPollEndEvent;
import io.github.jwdeveloper.tiktok.data.events.poll.TikTokPollEvent;
import io.github.jwdeveloper.tiktok.data.events.poll.TikTokPollStartEvent;
@@ -39,10 +36,9 @@ import io.github.jwdeveloper.tiktok.data.events.social.TikTokFollowEvent;
import io.github.jwdeveloper.tiktok.data.events.social.TikTokJoinEvent;
import io.github.jwdeveloper.tiktok.data.events.social.TikTokLikeEvent;
import io.github.jwdeveloper.tiktok.data.events.social.TikTokShareEvent;
import io.github.jwdeveloper.tiktok.data.models.Gift;
import io.github.jwdeveloper.tiktok.data.models.Picture;
import io.github.jwdeveloper.tiktok.data.models.Text;
import io.github.jwdeveloper.tiktok.gifts.TikTokGiftManager;
import io.github.jwdeveloper.tiktok.handlers.events.TikTokGiftEventHandler;
import io.github.jwdeveloper.tiktok.mappers.TikTokGenericEventMapper;
import io.github.jwdeveloper.tiktok.messages.webcast.*;
import io.github.jwdeveloper.tiktok.models.SocialTypes;
import lombok.SneakyThrows;
@@ -50,19 +46,21 @@ import lombok.SneakyThrows;
import java.util.regex.Pattern;
public class TikTokMessageHandlerRegistration extends TikTokMessageHandler {
private final TikTokGiftManager giftManager;
private final TikTokRoomInfo roomInfo;
private final TikTokGiftEventHandler giftHandler;
private final Pattern socialMediaPattern = Pattern.compile("pm_mt_guidance_viewer_([0-9]+)_share");
public TikTokMessageHandlerRegistration(TikTokEventObserver tikTokEventHandler,
TikTokGiftManager giftManager,
TikTokRoomInfo roomInfo) {
super(tikTokEventHandler);
this.giftManager = giftManager;
TikTokRoomInfo roomInfo,
TikTokGenericEventMapper genericTikTokEventMapper,
TikTokGiftEventHandler tikTokGiftEventHandler) {
super(tikTokEventHandler, genericTikTokEventMapper);
this.giftHandler = tikTokGiftEventHandler;
this.roomInfo = roomInfo;
init();
}
@Override
public void init() {
//ConnectionEvents events
@@ -80,7 +78,7 @@ public class TikTokMessageHandlerRegistration extends TikTokMessageHandler {
//User Interactions events
registerMapping(WebcastChatMessage.class, TikTokCommentEvent.class);
registerMapping(WebcastLikeMessage.class, this::handleLike);
registerMapping(WebcastGiftMessage.class, this::handleGift);
registerMappings(WebcastGiftMessage.class, giftHandler::handleGift);
registerMapping(WebcastSocialMessage.class, this::handleSocialMedia);
registerMapping(WebcastMemberMessage.class, this::handleMemberMessage);
@@ -124,29 +122,6 @@ public class TikTokMessageHandlerRegistration extends TikTokMessageHandler {
};
}
@SneakyThrows
private TikTokEvent handleGift(byte[] msg) {
var giftMessage = WebcastGiftMessage.parseFrom(msg);
var gift = giftManager.findById((int) giftMessage.getGiftId());
if (gift == Gift.UNDEFINED) {
gift = giftManager.findByName(giftMessage.getGift().getName());
}
if (gift == Gift.UNDEFINED) {
gift = giftManager.registerGift(
(int) giftMessage.getGift().getId(),
giftMessage.getGift().getName(),
giftMessage.getGift().getDiamondCount(),
Picture.map(giftMessage.getGift().getImage()));
}
if (giftMessage.getRepeatEnd() > 0) {
return new TikTokGiftComboEvent(gift, giftMessage);
}
return new TikTokGiftEvent(gift, giftMessage);
}
@SneakyThrows
private TikTokEvent handleSocialMedia(byte[] msg) {
@@ -181,13 +156,13 @@ public class TikTokMessageHandlerRegistration extends TikTokMessageHandler {
}
private TikTokEvent handleRoomUserSeqMessage(byte[] msg) {
var event = (TikTokRoomUserInfoEvent) mapMessageToEvent(WebcastRoomUserSeqMessage.class, TikTokRoomUserInfoEvent.class, msg);
var event = (TikTokRoomUserInfoEvent) mapper.mapToEvent(WebcastRoomUserSeqMessage.class, TikTokRoomUserInfoEvent.class, msg);
roomInfo.setViewersCount(event.getTotalUsers());
return event;
}
private TikTokEvent handleLike(byte[] msg) {
var event = (TikTokLikeEvent) mapMessageToEvent(WebcastLikeMessage.class, TikTokLikeEvent.class, msg);
var event = (TikTokLikeEvent) mapper.mapToEvent(WebcastLikeMessage.class, TikTokLikeEvent.class, msg);
roomInfo.setLikesCount(event.getTotalLikes());
return event;
}
@@ -200,7 +175,7 @@ public class TikTokMessageHandlerRegistration extends TikTokMessageHandler {
return new TikTokRoomPinEvent(pinMessage, chatEvent);
}
//TODO check
//TODO Probably not working
@SneakyThrows
private TikTokEvent handlePollEvent(byte[] msg) {
var poolMessage = WebcastPollMessage.parseFrom(msg);

View File

@@ -0,0 +1,86 @@
package io.github.jwdeveloper.tiktok.handlers.events;
import io.github.jwdeveloper.tiktok.data.events.common.TikTokEvent;
import io.github.jwdeveloper.tiktok.data.events.gift.TikTokGiftComboEvent;
import io.github.jwdeveloper.tiktok.data.events.gift.TikTokGiftEvent;
import io.github.jwdeveloper.tiktok.data.models.Picture;
import io.github.jwdeveloper.tiktok.data.models.gifts.Gift;
import io.github.jwdeveloper.tiktok.data.models.gifts.GiftSendType;
import io.github.jwdeveloper.tiktok.live.GiftManager;
import io.github.jwdeveloper.tiktok.messages.webcast.WebcastGiftMessage;
import lombok.SneakyThrows;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TikTokGiftEventHandler {
private final GiftManager giftManager;
private final Map<Long, WebcastGiftMessage> giftsMessages;
public TikTokGiftEventHandler(GiftManager giftManager) {
this.giftManager = giftManager;
giftsMessages = new HashMap<>();
}
@SneakyThrows
public List<TikTokEvent> handleGift(byte[] msg) {
var currentMessage = WebcastGiftMessage.parseFrom(msg);
var userId = currentMessage.getUser().getId();
var currentType = GiftSendType.fromNumber(currentMessage.getSendType());
var containsPreviousMessage = giftsMessages.containsKey(userId);
if (!containsPreviousMessage) {
if (currentType == GiftSendType.Finished) {
return List.of(getGiftEvent(currentMessage));
} else {
giftsMessages.put(userId, currentMessage);
return List.of(getGiftComboEvent(currentMessage, GiftSendType.Begin));
}
}
var previousMessage = giftsMessages.get(userId);
var previousType = GiftSendType.fromNumber(previousMessage.getSendType());
if (currentType == GiftSendType.Active &&
previousType == GiftSendType.Active) {
giftsMessages.put(userId, currentMessage);
return List.of(getGiftComboEvent(currentMessage, GiftSendType.Active));
}
if (currentType == GiftSendType.Finished &&
previousType == GiftSendType.Active) {
giftsMessages.clear();
return List.of(
getGiftComboEvent(currentMessage, GiftSendType.Finished),
getGiftEvent(currentMessage));
}
return List.of();
}
private TikTokGiftEvent getGiftEvent(WebcastGiftMessage message) {
var gift = getGiftObject(message);
return new TikTokGiftEvent(gift, message);
}
private TikTokGiftEvent getGiftComboEvent(WebcastGiftMessage message, GiftSendType state) {
var gift = getGiftObject(message);
return new TikTokGiftComboEvent(gift, message, state);
}
private Gift getGiftObject(WebcastGiftMessage giftMessage) {
var gift = giftManager.findById((int) giftMessage.getGiftId());
if (gift == Gift.UNDEFINED) {
gift = giftManager.findByName(giftMessage.getGift().getName());
}
if (gift == Gift.UNDEFINED) {
gift = giftManager.registerGift(
(int) giftMessage.getGift().getId(),
giftMessage.getGift().getName(),
giftMessage.getGift().getDiamondCount(),
Picture.map(giftMessage.getGift().getImage()));
}
return gift;
}
}

View File

@@ -133,12 +133,12 @@ public class TikTokHttpClient {
private String getSignedUrl(String url, Map<String, Object> parameters) {
var fullUrl = HttpUtils.parseParameters(url,parameters);
var singHeaders = new TreeMap<String,Object>();
singHeaders.put("client", "ttlive-java");
singHeaders.put("uuc", 1);
singHeaders.put("url", fullUrl);
var signParams = new TreeMap<String,Object>();
signParams.put("client", "ttlive-java");
signParams.put("uuc", 1);
signParams.put("url", fullUrl);
var request = requestFactory.setQueries(singHeaders);
var request = requestFactory.setQueries(signParams);
var content = request.get(Constants.TIKTOK_SIGN_API);

View File

@@ -0,0 +1,85 @@
package io.github.jwdeveloper.tiktok.mappers;
import io.github.jwdeveloper.tiktok.data.events.common.TikTokEvent;
import io.github.jwdeveloper.tiktok.exceptions.TikTokMessageMappingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Goal of this class is to map ProtocolBuffer objects to TikTok Event in generic way
*
* First parameter is ProtocolBuffer class type
* Second parameters is TikTokEvent class type
* Third parameters is bytes payload
*
* mapToEvent(WebcastGiftMessage.class, TikTokGiftEvent.class, payload)
*
* How does it work?
* 1. Finds method `parseFrom(byte[] bytes)` inside ProtocolBuffer class
* 2. put payload to the method methods and create new instance of ProtcolBuffer object
* 3. Finds in TikTokEvent constructor that takes ProtocolBuffer type as parameter
* 4. create new Instance in TikTokEvents using object from step 2 and constructor from step 3
*
* methodCache and constructorCache are used to boost performance
*/
public class TikTokGenericEventMapper {
private record TypePair(Class<?> a, Class<?> b) {
}
private final Map<Class<?>, Method> methodCache;
private final Map<TypePair, Constructor<?>> constructorCache;
public TikTokGenericEventMapper() {
this.methodCache = new HashMap<>();
this.constructorCache = new HashMap<>();
}
public TikTokEvent mapToEvent(Class<?> inputClazz, Class<?> outputClass, byte[] payload) {
try {
var method = getParsingMethod(inputClazz);
var deserializedMessage = method.invoke(null, payload);
var constructor = getParsingConstructor(inputClazz, outputClass);
var tiktokEvent = constructor.newInstance(deserializedMessage);
return (TikTokEvent) tiktokEvent;
} catch (Exception ex) {
throw new TikTokMessageMappingException(inputClazz, outputClass, ex);
}
}
private Method getParsingMethod(Class<?> input) throws NoSuchMethodException {
if (methodCache.containsKey(input)) {
return methodCache.get(input);
}
var method = input.getDeclaredMethod("parseFrom", byte[].class);
methodCache.put(input, method);
return method;
}
private Constructor<?> getParsingConstructor(Class<?> input, Class<?> output) {
var pair = new TypePair(input, output);
if (constructorCache.containsKey(pair)) {
return constructorCache.get(pair);
}
var optional = Arrays.stream(output.getConstructors())
.filter(ea -> Arrays.stream(ea.getParameterTypes())
.toList()
.contains(input))
.findFirst();
if (optional.isEmpty()) {
throw new TikTokMessageMappingException(input, output, "Unable to find constructor with input class type");
}
constructorCache.put(pair, optional.get());
return optional.get();
}
}

View File

@@ -125,7 +125,7 @@ public class TikTokWebSocketListener extends WebSocketClient {
}
return Optional.of(websocketMessage);
} catch (Exception e) {
throw new TikTokProtocolBufferException("Unable to parse WebcastWebsocketMessage", buffer, e);
throw new TikTokProtocolBufferException("Unable to parse WebcastPushFrame", buffer, e);
}
}

View File

@@ -28,7 +28,6 @@ import java.util.Random;
public class TikTokWebSocketPingingTask
{
private Thread thread;
private boolean isRunning = false;
private final int MIN_TIMEOUT = 5;
private final int MAX_TIMEOUT = 100;
@@ -37,7 +36,7 @@ public class TikTokWebSocketPingingTask
public void run(WebSocket webSocket)
{
stop();
var thread = new Thread(() ->
thread = new Thread(() ->
{
pingTask(webSocket);
});

View File

@@ -22,7 +22,7 @@
*/
package io.github.jwdeveloper.tiktok.gifts;
import io.github.jwdeveloper.tiktok.data.models.Gift;
import io.github.jwdeveloper.tiktok.data.models.gifts.Gift;
import io.github.jwdeveloper.tiktok.data.models.Picture;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;