Initial commit

This commit is contained in:
JW
2023-08-05 18:15:37 +02:00
commit a58612d4c4
44 changed files with 22359 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
package io.github.jwdeveloper.tiktok;
public class Main
{
public static void main(String[] args)
{
System.out.println("Hello world!");
}
}

View File

@@ -0,0 +1,95 @@
package io.github.jwdeveloper.tiktok;
import io.github.jwdeveloper.tiktok.http.TikTokApiService;
import io.github.jwdeveloper.tiktok.http.TikTokHttpApiClient;
import io.github.jwdeveloper.tiktok.http.TikTokHttpRequestFactory;
import io.github.jwdeveloper.tiktok.live.LiveClient;
import io.github.jwdeveloper.tiktok.live.TikTokLiveMeta;
import io.github.jwdeveloper.tiktok.websocket.TikTokWebsocketClient;
import java.time.Duration;
import java.util.Map;
import java.util.function.Consumer;
import java.util.logging.Logger;
public class TikTokClientBuilder {
private String userName;
private final ClientSettings clientSettings;
private Map<String, Object> clientParameters;
private Logger logger;
public TikTokClientBuilder(String userName) {
this.userName = userName;
this.clientSettings = Constants.DefaultClientSettings();
this.clientParameters = Constants.DefaultClientParams();
this.logger = Logger.getLogger(TikTokLive.class.getName());
}
public TikTokClientBuilder clientSettings(Consumer<ClientSettings> consumer) {
consumer.accept(clientSettings);
return this;
}
public TikTokClientBuilder hostUserName(String userName) {
this.userName = userName;
return this;
}
public TikTokClientBuilder clientParameters(Map<String, Object> clientParameters) {
this.clientParameters = clientParameters;
return this;
}
public TikTokClientBuilder addClientParameters(String key, Object value) {
this.clientParameters.put(key, value);
return this;
}
private void validate() {
if (clientSettings.getTimeout() == null) {
clientSettings.setTimeout(Duration.ofSeconds(Constants.DEFAULT_TIMEOUT));
}
if (clientSettings.getPollingInterval() == null) {
clientSettings.setPollingInterval(Duration.ofSeconds(Constants.DEFAULT_POLLTIME));
}
if (clientSettings.getClientLanguage() == null || clientSettings.getClientLanguage().equals("")) {
clientSettings.setClientLanguage(Constants.DefaultClientSettings().getClientLanguage());
}
if (clientSettings.getSocketBufferSize() < 500_000) {
clientSettings.setSocketBufferSize(Constants.DefaultClientSettings().getSocketBufferSize());
}
if (userName == null || userName.equals("")) {
throw new RuntimeException("UserName can not be null");
}
if (clientParameters == null) {
clientParameters = Constants.DefaultClientParams();
}
clientParameters.put("app_language", clientSettings.getClientLanguage());
clientParameters.put("webcast_language", clientSettings.getClientLanguage());
}
public LiveClient build() {
validate();
var meta = new TikTokLiveMeta();
meta.setUserName(userName);
var requestFactory = new TikTokHttpRequestFactory();
var apiClient = new TikTokHttpApiClient(clientSettings, requestFactory);
var apiService = new TikTokApiService(apiClient, logger,clientParameters);
var webSocketClient = new TikTokWebsocketClient(logger,clientParameters, clientSettings);
var giftManager =new TikTokGiftManager(logger, apiService, clientSettings);
return new TikTokLiveClient(meta,apiService, webSocketClient, giftManager, logger);
}
}

View File

@@ -0,0 +1,36 @@
package io.github.jwdeveloper.tiktok;
import io.github.jwdeveloper.tiktok.http.TikTokApiService;
import io.github.jwdeveloper.tiktok.live.models.gift.TikTokGift;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
public class TikTokGiftManager {
private Logger logger;
private ClientSettings clientSettings;
private TikTokApiService apiService;
private Map<Integer, TikTokGift> gifts;
public TikTokGiftManager(Logger logger, TikTokApiService apiService, ClientSettings clientSettings) {
this.logger = logger;
this.clientSettings = clientSettings;
this.apiService = apiService;
this.gifts = new HashMap<>();
}
public void loadGifts() {
if (!clientSettings.isDownloadGiftInfo()) {
return;
}
logger.info("Fetching gifts");
gifts =apiService.fetchAvailableGifts();
}
public List<TikTokGift> getGifts()
{
return gifts.values().stream().toList();
}
}

View File

@@ -0,0 +1,11 @@
package io.github.jwdeveloper.tiktok;
public class TikTokLive
{
public static TikTokClientBuilder newClient(String userName)
{
return new TikTokClientBuilder(userName);
}
}

View File

@@ -0,0 +1,93 @@
package io.github.jwdeveloper.tiktok;
import io.github.jwdeveloper.tiktok.http.TikTokApiService;
import io.github.jwdeveloper.tiktok.live.ConnectionState;
import io.github.jwdeveloper.tiktok.live.LiveClient;
import io.github.jwdeveloper.tiktok.live.LiveMeta;
import io.github.jwdeveloper.tiktok.live.TikTokLiveMeta;
import io.github.jwdeveloper.tiktok.websocket.TikTokWebsocketClient;
import java.util.logging.Logger;
public class TikTokLiveClient implements LiveClient {
private final TikTokLiveMeta meta;
private final TikTokGiftManager giftManager;
private final TikTokApiService apiClient;
private final TikTokWebsocketClient webSocketClient;
private final Logger logger;
public TikTokLiveClient(TikTokLiveMeta tikTokLiveMeta,
TikTokApiService tikTokApiService,
TikTokWebsocketClient webSocketClient,
TikTokGiftManager tikTokGiftManager,
Logger logger) {
this.meta = tikTokLiveMeta;
this.giftManager = tikTokGiftManager;
this.apiClient = tikTokApiService;
this.webSocketClient = webSocketClient;
this.logger = logger;
}
public void run() {
tryConnect();
}
public void stop() {
if (!meta.hasConnectionState(ConnectionState.CONNECTED)) {
return;
}
disconnect();
setState(ConnectionState.DISCONNECTED);
}
public void tryConnect() {
try {
connect();
} catch (Exception e) {
e.printStackTrace();
setState(ConnectionState.DISCONNECTED);
}
}
public void connect() {
if (meta.hasConnectionState(ConnectionState.CONNECTED))
throw new RuntimeException("Already connected");
if (meta.hasConnectionState(ConnectionState.CONNECTING))
throw new RuntimeException("Already connecting");
logger.info("Connecting");
setState(ConnectionState.CONNECTING);
var roomId = apiClient.fetchRoomId(meta.getUserName());
meta.setRoomId(roomId);
var roomData =apiClient.fetchRoomInfo();
if (roomData.getStatus() == 0 || roomData.getStatus() == 4)
{
throw new TikTokLiveException("LiveStream for HostID could not be found. Is the Host online?");
}
// giftManager.loadGifts();
var clientData = apiClient.fetchClientData();
webSocketClient.start(clientData);
setState(ConnectionState.CONNECTED);
}
public void disconnect() {
}
public LiveMeta getMeta() {
return meta;
}
private void setState(ConnectionState connectionState) {
logger.info("TikTokLive client state: " + connectionState.name());
meta.setConnectionState(connectionState);
}
}

View File

@@ -0,0 +1,23 @@
package io.github.jwdeveloper.tiktok;
public class TikTokLiveException extends RuntimeException
{
public TikTokLiveException() {
}
public TikTokLiveException(String message) {
super(message);
}
public TikTokLiveException(String message, Throwable cause) {
super(message, cause);
}
public TikTokLiveException(Throwable cause) {
super(cause);
}
public TikTokLiveException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -0,0 +1,4 @@
package io.github.jwdeveloper.tiktok.events;
public class TikTokChatEvent {
}

View File

@@ -0,0 +1,6 @@
package io.github.jwdeveloper.tiktok.events;
public class TikTokEvent
{
}

View File

@@ -0,0 +1,4 @@
package io.github.jwdeveloper.tiktok.events;
public class TikTokGiftEvent {
}

View File

@@ -0,0 +1,57 @@
package io.github.jwdeveloper.tiktok.http;
import lombok.SneakyThrows;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class HttpUtils
{
public static String parseParameters(String url, Map<String,Object> parameters)
{
var parameterString = "";
if (!parameters.isEmpty()) {
var builder = new StringBuilder();
builder.append("?");
var first = false;
for (var param : parameters.entrySet()) {
if (first) {
builder.append("&");
}
builder.append(param.getKey()).append("=").append(param.getValue());
first = true;
}
parameterString = builder.toString();
}
return url+parameterString;
}
@SneakyThrows
public static String parseParametersEncode(String url, Map<String,Object> parameters)
{
var parameterString = "";
if (!parameters.isEmpty()) {
var builder = new StringBuilder();
builder.append("?");
var first = false;
for (var param : parameters.entrySet()) {
if (first) {
builder.append("&");
}
final String encodedKey = URLEncoder.encode(param.getKey().toString(), StandardCharsets.UTF_8.toString());
final String encodedValue = URLEncoder.encode(param.getValue().toString(), StandardCharsets.UTF_8.toString());
builder.append(encodedKey).append("=").append(encodedValue);
first = true;
}
parameterString = builder.toString();
}
return url+parameterString;
}
}

View File

@@ -0,0 +1,127 @@
package io.github.jwdeveloper.tiktok.http;
import com.google.gson.Gson;
import com.google.gson.JsonParser;
import io.github.jwdeveloper.generated.WebcastResponse;
import io.github.jwdeveloper.tiktok.TikTokLiveException;
import io.github.jwdeveloper.tiktok.live.LiveRoomInfo;
import io.github.jwdeveloper.tiktok.live.models.gift.TikTokGift;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TikTokApiService {
private final TikTokHttpApiClient apiClient;
private final Logger logger;
private final Map<String, Object> clientParams;
public TikTokApiService(TikTokHttpApiClient apiClient, Logger logger, Map<String, Object> clientParams) {
this.apiClient = apiClient;
this.logger = logger;
this.clientParams = clientParams;
}
public String fetchRoomId(String userName) {
logger.info("Fetching room ID");
String html;
try {
html = apiClient.GetLivestreamPage(userName);
} catch (Exception e) {
throw new RuntimeException("Failed to fetch room id from WebCast, see stacktrace for more info.", e);
}
Pattern firstPattern = Pattern.compile("room_id=([0-9]*)");
Matcher firstMatcher = firstPattern.matcher(html);
String id = "";
if (firstMatcher.find()) {
id = firstMatcher.group(1);
} else {
Pattern secondPattern = Pattern.compile("\"roomId\":\"([0-9]*)\"");
Matcher secondMatcher = secondPattern.matcher(html);
if (secondMatcher.find()) {
id = secondMatcher.group(1);
}
}
if (id.isEmpty()) {
throw new TikTokLiveException("Unable to fetch room ID");
}
clientParams.put("room_id", id);
logger.info("RoomID -> "+id);
return id;
}
public LiveRoomInfo fetchRoomInfo() {
logger.info("Fetch RoomInfo");
try {
var response = apiClient.GetJObjectFromWebcastAPI("room/info/", clientParams);
if (!response.has("data")) {
return new LiveRoomInfo();
}
var data = response.getAsJsonObject("data");
if (!data.has("status")) {
return new LiveRoomInfo();
}
var status = data.get("status");
var info = new LiveRoomInfo();
info.setStatus(status.getAsInt());
logger.info("RoomInfo status -> "+info.getStatus());
return info;
} catch (Exception e) {
throw new TikTokLiveException("Failed to fetch room info from WebCast, see stacktrace for more info.", e);
}
}
public WebcastResponse fetchClientData()
{
logger.info("Fetch ClientData");
try {
var response = apiClient.GetDeserializedMessage("im/fetch/", clientParams);
clientParams.put("cursor",response.getCursor());
clientParams.put("internal_ext", response.getInternalExt());
return response;
}
catch (Exception e)
{
throw new TikTokLiveException("Failed to fetch client data", e);
}
}
public Map<Integer, TikTokGift> fetchAvailableGifts() {
try {
var response = apiClient.GetJObjectFromWebcastAPI("gift/list/", clientParams);
if(!response.has("data"))
{
return new HashMap<>();
}
var dataJson = response.getAsJsonObject("data");
if(!dataJson.has("gifts"))
{
return new HashMap<>();
}
var giftsJsonList = dataJson.get("gifts").getAsJsonArray();
var gifts = new HashMap<Integer, TikTokGift>();
var gson = new Gson();
for(var jsonGift : giftsJsonList)
{
var gift = gson.fromJson(jsonGift, TikTokGift.class);
logger.info("Found Available Gift "+ gift.getName()+ " with ID "+gift.getId());
gifts.put(gift.getId(),gift);
}
return gifts;
} catch (Exception e) {
throw new TikTokLiveException("Failed to fetch giftTokens from WebCast, see stacktrace for more info.", e);
}
}
}

View File

@@ -0,0 +1,44 @@
package io.github.jwdeveloper.tiktok.http;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class TikTokCookieJar {
/// <summary>
/// Cookies in Jar
/// </summary>
private final Map<String, String> cookies;
/// <summary>
/// Create a TikTok cookie jar instance.
/// </summary>
public TikTokCookieJar() {
cookies = new HashMap<>();
}
public String get(String key) {
return cookies.get(key);
}
public void set(String key, String value) {
cookies.put(key, value);
}
/// <summary>
/// Enumerates Cookies
/// </summary>
public Set<Map.Entry<String, String>> GetEnumerator() {
return cookies.entrySet();
}
/* /// <summary>
/// Enumerates Cookies
/// </summary>
public IEnumerator<string> GetEnumerator()
{
foreach (var cookie in cookies)
yield return $"{cookie.Key}={cookie.Value};";
}*/
}

View File

@@ -0,0 +1,106 @@
package io.github.jwdeveloper.tiktok.http;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.github.jwdeveloper.generated.WebcastResponse;
import io.github.jwdeveloper.tiktok.ClientSettings;
import io.github.jwdeveloper.tiktok.Constants;
import io.github.jwdeveloper.tiktok.TikTokLiveException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class TikTokHttpApiClient {
private final ClientSettings clientSettings;
private final TikTokHttpRequestFactory requestFactory;
public TikTokHttpApiClient(ClientSettings clientSettings, TikTokHttpRequestFactory requestFactory) {
this.clientSettings = clientSettings;
this.requestFactory = requestFactory;
}
public String GetLivestreamPage(String userName) {
var url = Constants.TIKTOK_URL_WEB + "@" + userName + "/live/";
var get = getRequest(url, null, false);
return get;
}
public JsonObject GetJObjectFromWebcastAPI(String path, Map<String, Object> parameters) {
var get = getRequest(Constants.TIKTOK_URL_WEBCAST + path, parameters, false);
var json = JsonParser.parseString(get);
var jsonObject = json.getAsJsonObject();
return jsonObject;
}
public WebcastResponse GetDeserializedMessage(String path, Map<String, Object> parameters) {
var bytes = getSignRequest(Constants.TIKTOK_URL_WEBCAST + path, parameters);
try {
return WebcastResponse.parseFrom(bytes);
}
catch (Exception e)
{
throw new TikTokLiveException("Unable to deserialize message: "+path,e);
}
}
private String getRequest(String url, Map<String, Object> parameters, boolean signURL) {
if (parameters == null) {
parameters = new HashMap<>();
}
var request = requestFactory.SetQueries(parameters);
return request.Get(url);
}
private byte[] getSignRequest(String url, Map<String, Object> parameters) {
url = GetSignedUrl(url, parameters);
try {
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.uri(new URI(url))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
return response.body();
}
catch (Exception e)
{
throw new TikTokLiveException("unabel to send signature");
}
}
private String GetSignedUrl(String url, Map<String, Object> parameters) {
var fullUrl = HttpUtils.parseParameters(url,parameters);
var singHeaders = new HashMap<String, Object>();
singHeaders.put("client", "ttlive-net");
singHeaders.put("uuc", 1);
singHeaders.put("url", fullUrl);
var request = requestFactory.SetQueries(singHeaders);
var content = request.Get(Constants.TIKTOK_SIGN_API);
try {
var json = JsonParser.parseString(content);
var jsonObject = json.getAsJsonObject();
var signedUrl = jsonObject.get("signedUrl").getAsString();
var userAgent = jsonObject.get("User-Agent").getAsString();
//requestFactory.setHeader()
requestFactory.setAgent(userAgent);
return signedUrl;
} catch (Exception e) {
throw new TikTokLiveException("Insufficent values have been supplied for signing. Likely due to an update. Post an issue on GitHub.", e);
}
}
}

View File

@@ -0,0 +1,136 @@
package io.github.jwdeveloper.tiktok.http;
import io.github.jwdeveloper.tiktok.Constants;
import lombok.SneakyThrows;
import java.net.CookieManager;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TikTokHttpRequestFactory implements TikTokHttpRequest
{
private CookieManager cookieManager;
private HttpClient client;
private Duration timeout;
private ProxySelector webProxy;
private String query;
private Boolean sent;
private Map<String, String> defaultHeaders;
public TikTokHttpRequestFactory() {
cookieManager = new CookieManager();
defaultHeaders = Constants.DefaultRequestHeaders();
client = HttpClient.newBuilder()
.cookieHandler(cookieManager)
.connectTimeout(Duration.ofSeconds(2))
.build();
}
@SneakyThrows
public String Get(String url) {
var uri = URI.create(url);
var request = HttpRequest.newBuilder().GET();
for(var header : defaultHeaders.entrySet())
{
//request.setHeader(header.getKey(),header.getValue());
}
if (query != null) {
var baseUri = uri.toString();
var requestUri = URI.create(baseUri + "?" + query);
request.uri(requestUri);
}
return GetContent(request.build());
}
@SneakyThrows
public String Post(String url, HttpRequest.BodyPublisher data) {
var uri = URI.create(url);
var request = HttpRequest.newBuilder().POST(data);
for(var header : defaultHeaders.entrySet())
{
request.setHeader(header.getKey(),header.getValue());
}
if (query != null) {
var baseUri = uri.toString();
var requestUri = URI.create(baseUri + "?" + query);
request.uri(requestUri);
}
return GetContent(request.build());
}
public TikTokHttpRequest setHeader(String key, String value)
{
defaultHeaders.put(key,value);
return this;
}
public TikTokHttpRequest setAgent( String value)
{
defaultHeaders.put("User-Agent", value);
return this;
}
public TikTokHttpRequest SetQueries(Map<String, Object> queries) {
if (queries == null)
return this;
query = String.join("&", queries.entrySet().stream().map(x ->
{
var key = x.getKey();
var value = "";
try {
value = URLEncoder.encode(x.getValue().toString(), StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
return key + "=" + value;
}).toList());
return this;
}
private String GetContent(HttpRequest request) throws Exception {
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
sent = true;
if (response.statusCode() == 404)
{
throw new RuntimeException("Request responded with 404 NOT_FOUND");
}
if(response.statusCode() != 200)
{
throw new RuntimeException("Request was unsuccessful "+response.statusCode());
}
var cookies = response.headers().allValues("Set-Cookie");
for(var cookie : cookies)
{
var split = cookie.split(";")[0].split("=");
var uri = request.uri();
var map = new HashMap<String,List<String>>();
map.put(split[0],List.of(split[1]));
cookieManager.put(uri,map);
}
return response.body();
}
}

View File

@@ -0,0 +1,31 @@
package io.github.jwdeveloper.tiktok.websocket;
import java.util.concurrent.CompletionStage;
public class TikTokWebSocketListener implements java.net.http.WebSocket.Listener {
//Insert Body here
@Override
public void onOpen(java.net.http.WebSocket webSocket) {
System.out.println("WebSocket opened");
}
@Override
public void onError(java.net.http.WebSocket webSocket, Throwable error) {
System.out.println("Error occurred: " + error.getMessage());
}
@Override
public CompletionStage<?> onText(java.net.http.WebSocket webSocket, CharSequence data, boolean last) {
System.out.println("Received message: " + data);
return java.net.http.WebSocket.Listener.super.onText(webSocket, data, last);
}
@Override
public CompletionStage<?> onClose(java.net.http.WebSocket webSocket, int statusCode, String reason) {
System.out.println("WebSocket closed with status code: " + statusCode + " and reason: " + reason);
return java.net.http.WebSocket.Listener.super.onClose(webSocket, statusCode, reason);
}
}

View File

@@ -0,0 +1,83 @@
package io.github.jwdeveloper.tiktok.websocket;
import io.github.jwdeveloper.generated.WebcastResponse;
import io.github.jwdeveloper.tiktok.ClientSettings;
import io.github.jwdeveloper.tiktok.TikTokLiveException;
import io.github.jwdeveloper.tiktok.http.HttpUtils;
import java.net.URI;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.Map;
import java.util.logging.Logger;
public class TikTokWebsocketClient {
private final Logger logger;
private final Map<String, Object> clientParams;
private final ClientSettings clientSettings;
public TikTokWebsocketClient(Logger logger, Map<String, Object> clientParams, ClientSettings clientSettings) {
this.logger = logger;
this.clientParams = clientParams;
this.clientSettings = clientSettings;
}
public void start(WebcastResponse webcastResponse) {
if (webcastResponse.getWsUrl().isEmpty() || webcastResponse.getWsParam().getAllFields().isEmpty()) {
throw new TikTokLiveException("Could not find Room");
}
try {
for (var param : webcastResponse.getWsParam().getAllFields().entrySet()) {
var name = param.getKey().getName();
var value = param.getValue();
clientParams.put(name, value);
logger.info("Adding Custom Param" + param.getKey().getName() + " " + param.getValue());
}
var url = webcastResponse.getWsUrl();
var wsUrl = HttpUtils.parseParametersEncode(url, clientParams);
logger.info("Creating Socket with URL " + wsUrl);
//socketClient = new TikTokWebSocket(TikTokHttpRequest.CookieJar, token, settings.SocketBufferSize);
//connectedSocketUrl = url;
//await socketClient.Connect(url);
logger.info("Starting Socket-Threads");
//runningTask = Task.Run(WebSocketLoop, token);
//pollingTask = Task.Run(PingLoop, token);
startWS(wsUrl);
} catch (Exception e) {
throw new TikTokLiveException("Failed to connect to the websocket", e);
}
if (clientSettings.isHandleExistingMessagesOnConnect()) {
try {
// HandleWebcastMessages(webcastResponse);
} catch (Exception e) {
throw new TikTokLiveException("Error Handling Initial Messages", e);
}
}
}
public void startWS(String url)
{
try {
var cookie = "tt_csrf_token=Fh92faHZ-fVnWZ8CG58Wb_kIC1hb-QzizkRM;ttwid=1%7CergNdYee4w-v_96VkhyDxkJ8NIavveA-NvCEdWF68Ik%7C1691076950%7C154533521f698b079ff5300fbd058e85e81a8ef64c41349f1d218124aa74a6db;";
// var url = "wss://webcast16-ws-useast1a.tiktok.com/webcast/im/push/?aid=1988&app_language=en-US&app_name=tiktok_web&browser_language=en&browser_name=Mozilla&browser_online=True&browser_platform=Win32&browser_version=5.0+(Windows+NT+10.0%3b+Win64%3b+x64)+AppleWebKit%2f537.36+(KHTML%2c+like+Gecko)+Chrome%2f102.0.5005.63+Safari%2f537.36&cookie_enabled=True&cursor=1691242057374_7263829320139870326_1_1_0_0&internal_ext=fetch_time%3a1691242057374%7cstart_time%3a0%7cack_ids%3a%2c%7cflag%3a0%7cseq%3a1%7cnext_cursor%3a1691242057374_7263829320139870326_1_1_0_0%7cwss_info%3a0-1691242057374-0-0&device_platform=web&focus_state=True&from_page=user&history_len=4&is_fullscreen=False&is_page_visible=True&did_rule=3&fetch_rule=1&identity=audience&last_rtt=0&live_id=12&resp_content_type=protobuf&screen_height=1152&screen_width=2048&tz_name=Europe%2fBerlin&referer=https%2c+%2f%2fwww.tiktok.com%2f&root_referer=https%2c+%2f%2fwww.tiktok.com%2f&msToken=&version_code=180800&webcast_sdk_version=1.3.0&update_version_code=1.3.0&webcast_language=en-US&room_id=7263759223213132577&imprp=u65Ja_b3czc3iEAb4x6oLXindKyTO";
HttpClient client = HttpClient.newHttpClient();
var ws = client.newWebSocketBuilder()
.subprotocols("echo-protocol")
.connectTimeout(Duration.ofSeconds(15))
.header("cookie",cookie)
.buildAsync(URI.create(url),new TikTokWebSocketListener()).get();
}
catch (Exception e)
{
e.printStackTrace();
}
}
public void stop() {
}
}

View File

@@ -0,0 +1,310 @@
syntax = "proto3";
package TikTok;
option java_package = "io.github.jwdeveloper.generated";
option java_multiple_files = true;
// Data structure from im/fetch/ response
message WebcastResponse {
repeated Message messages = 1;
string cursor = 2;
int32 fetchInterval = 3;
int64 serverTimestamp = 4;
string internalExt = 5;
int32 fetchType = 6; // ws (1) or polling (2)
WebsocketParam wsParam = 7;
int32 heartbeatDuration = 8;
bool needAck = 9;
string wsUrl = 10;
}
message Message {
string type = 1;
bytes binary = 2;
}
message WebsocketParam {
string name = 1;
string value = 2;
}
// Message types depending on Message.tyoe
message WebcastControlMessage {
int32 action = 2;
}
// Statistics like viewer count
message WebcastRoomUserSeqMessage {
repeated TopUser topViewers = 2;
int32 viewerCount = 3;
}
message TopUser {
uint64 coinCount = 1;
User user = 2;
}
message WebcastChatMessage {
WebcastMessageEvent event = 1;
User user = 2;
string comment = 3;
}
message WebcastMemberMessage {
WebcastMessageEvent event = 1;
User user = 2;
int32 actionId = 10;
}
message WebcastGiftMessage {
WebcastMessageEvent event = 1;
int32 giftId = 2;
int32 repeatCount = 5;
User user = 7;
int32 repeatEnd = 9;
uint64 groupId = 11;
WebcastGiftMessageGiftDetails giftDetails = 15;
string monitorExtra = 22;
WebcastGiftMessageGiftExtra giftExtra = 23;
}
message WebcastGiftMessageGiftDetails {
WebcastGiftMessageGiftImage giftImage = 1;
string giftName = 16;
string describe = 2;
int32 giftType = 11;
int32 diamondCount = 12;
}
// Taken from https://github.com/Davincible/gotiktoklive/blob/da4630622bc586629a53faae64e8c53509af29de/proto/tiktok.proto#L57
message WebcastGiftMessageGiftExtra {
uint64 timestamp = 6;
uint64 receiverUserId = 8;
}
message WebcastGiftMessageGiftImage {
string giftPictureUrl = 1;
}
// Battle start
message WebcastLinkMicBattle {
repeated WebcastLinkMicBattleItems battleUsers = 10;
}
message WebcastLinkMicBattleItems {
WebcastLinkMicBattleGroup battleGroup = 2;
}
message WebcastLinkMicBattleGroup {
LinkUser user = 1;
}
// Battle status
message WebcastLinkMicArmies {
repeated WebcastLinkMicArmiesItems battleItems = 3;
int32 battleStatus = 7;
}
message WebcastLinkMicArmiesItems {
uint64 hostUserId = 1;
repeated WebcastLinkMicArmiesGroup battleGroups = 2;
}
message WebcastLinkMicArmiesGroup {
repeated User users = 1;
int32 points = 2;
}
// Follow & share event
message WebcastSocialMessage {
WebcastMessageEvent event = 1;
User user = 2;
}
// Like event (is only sent from time to time, not with every like)
message WebcastLikeMessage {
WebcastMessageEvent event = 1;
User user = 5;
int32 likeCount = 2;
int32 totalLikeCount = 3;
}
// New question event
message WebcastQuestionNewMessage {
QuestionDetails questionDetails = 2;
}
message QuestionDetails {
string questionText = 2;
User user = 5;
}
message WebcastMessageEvent {
uint64 msgId = 2;
uint64 createTime = 4;
WebcastMessageEventDetails eventDetails = 8;
}
// Contains UI information
message WebcastMessageEventDetails {
string displayType = 1;
string label = 2;
}
// Source: Co-opted https://github.com/zerodytrash/TikTok-Livestream-Chat-Connector/issues/19#issuecomment-1074150342
message WebcastLiveIntroMessage {
uint64 id = 2;
string description = 4;
User user = 5;
}
message SystemMessage {
string description = 2;
}
message WebcastInRoomBannerMessage {
string data = 2;
}
message RankItem {
string colour = 1;
uint64 id = 4;
}
message WeeklyRanking {
string type = 1;
string label = 2;
RankItem rank = 3;
}
message RankContainer {
WeeklyRanking rankings = 4;
}
message WebcastHourlyRankMessage {
RankContainer data = 2;
}
// Chat Emotes (Subscriber)
message WebcastEmoteChatMessage {
User user = 2;
EmoteDetails emote = 3;
}
message EmoteDetails {
string emoteId = 1;
EmoteImage image = 2;
}
message EmoteImage {
string imageUrl = 1;
}
// Envelope (treasure boxes)
// Taken from https://github.com/ThanoFish/TikTok-Live-Connector/blob/9b215b96792adfddfb638344b152fa9efa581b4c/src/proto/tiktokSchema.proto
message WebcastEnvelopeMessage {
TreasureBoxData treasureBoxData = 2;
TreasureBoxUser treasureBoxUser = 1;
}
message TreasureBoxUser {
TreasureBoxUser2 user2 = 8;
}
message TreasureBoxUser2 {
repeated TreasureBoxUser3 user3 = 4;
}
message TreasureBoxUser3 {
TreasureBoxUser4 user4 = 21;
}
message TreasureBoxUser4 {
User user = 1;
}
message TreasureBoxData {
uint32 coins = 5;
uint32 canOpen = 6;
uint64 timestamp = 7;
}
// New Subscriber message
message WebcastSubNotifyMessage {
WebcastMessageEvent event = 1;
User user = 2;
int32 exhibitionType = 3;
int32 subMonth = 4;
int32 subscribeType = 5;
int32 oldSubscribeStatus = 6;
int32 subscribingStatus = 8;
}
// ==================================
// Generic stuff
message User {
uint64 userId = 1;
string nickname = 3;
ProfilePicture profilePicture = 9;
string uniqueId = 38;
string secUid = 46;
repeated UserBadgesAttributes badges = 64;
uint64 createTime = 16;
string bioDescription = 5;
FollowInfo followInfo = 22;
}
message FollowInfo {
int32 followingCount = 1;
int32 followerCount = 2;
int32 followStatus = 3;
int32 pushStatus = 4;
}
message LinkUser {
uint64 userId = 1;
string nickname = 2;
ProfilePicture profilePicture = 3;
string uniqueId = 4;
}
message ProfilePicture {
repeated string urls = 1;
}
message UserBadgesAttributes {
int32 badgeSceneType = 3;
repeated UserImageBadge imageBadges = 20;
repeated UserBadge badges = 21;
}
message UserBadge {
string type = 2;
string name = 3;
}
message UserImageBadge {
int32 displayType = 1;
UserImageBadgeImage image = 2;
}
message UserImageBadgeImage {
string url = 1;
}
// Websocket incoming message structure
message WebcastWebsocketMessage {
uint64 id = 2;
string type = 7;
bytes binary = 8;
}
// Websocket acknowledgment message
message WebcastWebsocketAck {
uint64 id = 2;
string type = 7;
}

View File

@@ -0,0 +1,23 @@
package io.github.jwdeveloper.tiktok;
import org.junit.jupiter.api.Test;
import java.io.IOException;
public class TikTokLiveTest
{
public static String TEST_USER_SUBJECT = "moniczkka";
@Test
public void ShouldConnect() throws IOException {
var client = TikTokLive.newClient(TEST_USER_SUBJECT).build();
client.run();
System.in.read();
}
}

View File

@@ -0,0 +1,88 @@
package io.github.jwdeveloper.tiktok.http;
import com.google.gson.JsonParser;
import io.github.jwdeveloper.tiktok.live.models.gift.TikTokGift;
import org.java_websocket.WebSocket;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class TikTokApiServiceTest {
@Test
void testFetchAvailableGifts() {
// Arrange
var mockApiClient = mock(TikTokHttpApiClient.class);
var mockLogger = mock(Logger.class);
var clientParams = new HashMap<String,Object>();
var tikTokApiService = new TikTokApiService(mockApiClient, mockLogger, clientParams);
var inputStream = getClass().getClassLoader().getResourceAsStream("gifts.json");
String jsonContent;
try (var scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name())) {
jsonContent = scanner.useDelimiter("\\A").next(); // Read entire content
}
var json = JsonParser.parseString(jsonContent);
var jsonObject = json.getAsJsonObject();
when(mockApiClient.GetJObjectFromWebcastAPI("gift/list/", clientParams))
.thenReturn(jsonObject);
var gifts = tikTokApiService.fetchAvailableGifts();
assertNotNull(gifts);
}
@Test
void test() throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://tiktok.eulerstream.com/webcast/fetch/?room_id=7263690606554188577&client=ttlive-net&uuc=1&apiKey=&isSignRedirect=1&iph=658d90239052e48dabc4e5b61004661e"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Response code: " + response.statusCode());
HttpHeaders headers = response.headers();
headers.map().forEach((k, v) -> System.out.println(k + ":" + v));
System.out.println("Response body: " + response.body());
}
@Test
void testws2()
{
var url = "wss://webcast16-ws-useast1a.tiktok.com/webcast/im/push/?cursor=1691243226540_7263834340956643180_1_1_0_0&room_id=7263759223213132577&app_language=en-US&focus_state=true&last_rtt=0&did_rule=3&is_fullscreen=false&from_page=user&update_version_code=1.3.0&screen_height=1152&tz_name=Europe/Berlin&cookie_enabled=true&identity=audience&browser_platform=Win32&browser_version=5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36&browser_language=en&fetch_rule=1&value=u6Laa_b3czc3iEAb4x6oLXindKyTO&internal_ext=fetch_time:1691243226540|start_time:0|ack_ids:,|flag:0|seq:1|next_cursor:1691243226540_7263834340956643180_1_1_0_0|wss_info:0-1691243226540-0-0&screen_width=2048&version_code=180800&history_len=4&webcast_sdk_version=1.3.0&msToken=&app_name=tiktok_web&browser_name=Mozilla&resp_content_type=protobuf&live_id=12&webcast_language=en-US&name=imprp&device_platform=web&is_page_visible=true&aid=1988&browser_online=true";
var split = url.substring(373,url.length()-1);
var i =0;
var uri = URI.create(url);
}
}

File diff suppressed because it is too large Load Diff