mirror of
https://github.com/jwdeveloper/TikTokLiveJava.git
synced 2026-02-27 08:49:40 -05:00
Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Project exclude paths
|
||||||
|
/API/target/
|
||||||
|
/Client/target/
|
||||||
28
API/pom.xml
Normal file
28
API/pom.xml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>TikTokLiveJava</artifactId>
|
||||||
|
<groupId>org.example</groupId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>API</artifactId>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.22</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>16</maven.compiler.source>
|
||||||
|
<maven.compiler.target>16</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ClientSettings {
|
||||||
|
/// <summary>
|
||||||
|
/// Timeout for Connections
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
private Duration Timeout;
|
||||||
|
/// <summary>
|
||||||
|
/// Polling-Interval for Socket-Connection
|
||||||
|
/// </summary
|
||||||
|
|
||||||
|
private Duration PollingInterval;
|
||||||
|
/// <summary>
|
||||||
|
/// Proxy for Connection
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
// public RotatingProxy Proxy;
|
||||||
|
/// <summary>
|
||||||
|
/// ISO-Language for Client
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
private String ClientLanguage;
|
||||||
|
/// <summary>
|
||||||
|
/// Size for Buffer for Socket-Connection
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
private int SocketBufferSize;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to Retry if Connection Fails
|
||||||
|
/// </summary>
|
||||||
|
private boolean RetryOnConnectionFailure;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to handle Messages received from Room when Connecting
|
||||||
|
/// </summary>
|
||||||
|
private boolean HandleExistingMessagesOnConnect;
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to download List of Gifts for Room when Connecting
|
||||||
|
/// </summary>
|
||||||
|
private boolean DownloadGiftInfo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to print Logs to Console
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
private boolean PrintToConsole;
|
||||||
|
/// <summary>
|
||||||
|
/// LoggingLevel for Logs
|
||||||
|
/// </summary>
|
||||||
|
private Level LogLevel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to print Base64-Data for Messages to Console
|
||||||
|
/// </summary>
|
||||||
|
private boolean PrintMessageData;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to check Messages for Unparsed Data
|
||||||
|
/// </summary>
|
||||||
|
private boolean CheckForUnparsedData;
|
||||||
|
}
|
||||||
|
|
||||||
121
API/src/main/java/io/github/jwdeveloper/tiktok/Constants.java
Normal file
121
API/src/main/java/io/github/jwdeveloper/tiktok/Constants.java
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public class Constants {
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Web-URL for TikTok
|
||||||
|
/// </summary>
|
||||||
|
public static final String TIKTOK_URL_WEB = "https://www.tiktok.com/";
|
||||||
|
/// <summary>
|
||||||
|
/// WebCast-BaseURL for TikTok
|
||||||
|
/// </summary>
|
||||||
|
public static final String TIKTOK_URL_WEBCAST = "https://webcast.tiktok.com/webcast/";
|
||||||
|
/// <summary>
|
||||||
|
/// Signing API by Isaac Kogan
|
||||||
|
/// https://github-wiki-see.page/m/isaackogan/TikTokLive/wiki/All-About-Signatures
|
||||||
|
/// </summary>
|
||||||
|
public static final String TIKTOK_SIGN_API = "https://tiktok.eulerstream.com/webcast/sign_url";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default TimeOut for Connections
|
||||||
|
/// </summary>
|
||||||
|
public static final int DEFAULT_TIMEOUT = 20;
|
||||||
|
/// <summary>
|
||||||
|
/// Default Polling-Time for Socket-Connection
|
||||||
|
/// </summary>
|
||||||
|
public static final int DEFAULT_POLLTIME = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default Settings for Client
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static ClientSettings DefaultClientSettings() {
|
||||||
|
var clientSettings = new ClientSettings();
|
||||||
|
|
||||||
|
clientSettings.setTimeout(Duration.ofSeconds(DEFAULT_TIMEOUT));
|
||||||
|
clientSettings.setPollingInterval(Duration.ofSeconds(DEFAULT_POLLTIME));
|
||||||
|
clientSettings.setClientLanguage("en-US");
|
||||||
|
clientSettings.setHandleExistingMessagesOnConnect(true);
|
||||||
|
clientSettings.setDownloadGiftInfo(true);
|
||||||
|
clientSettings.setRetryOnConnectionFailure(true);
|
||||||
|
clientSettings.setSocketBufferSize(500_000);
|
||||||
|
clientSettings.setPrintToConsole(true);
|
||||||
|
clientSettings.setLogLevel(Level.ALL);
|
||||||
|
clientSettings.setCheckForUnparsedData(false);
|
||||||
|
clientSettings.setPrintMessageData(false);
|
||||||
|
return clientSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default Parameters for HTTP-Request
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
|
||||||
|
public static Map<String,Object> DefaultClientParams() {
|
||||||
|
var clientParams = new TreeMap<String,Object>();
|
||||||
|
clientParams.put("aid", 1988);
|
||||||
|
clientParams.put("app_language", "en-US");
|
||||||
|
clientParams.put("app_name", "tiktok_web");
|
||||||
|
clientParams.put("browser_language", "en");
|
||||||
|
clientParams.put("browser_name", "Mozilla");
|
||||||
|
clientParams.put("browser_online", true);
|
||||||
|
clientParams.put("browser_platform", "Win32");
|
||||||
|
clientParams.put("browser_version", "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36");
|
||||||
|
clientParams.put("cookie_enabled", true);
|
||||||
|
clientParams.put("cursor", "");
|
||||||
|
clientParams.put("internal_ext", "");
|
||||||
|
clientParams.put("device_platform", "web");
|
||||||
|
clientParams.put("focus_state", true);
|
||||||
|
clientParams.put("from_page", "user");
|
||||||
|
clientParams.put("history_len", 4);
|
||||||
|
clientParams.put("is_fullscreen", false);
|
||||||
|
clientParams.put("is_page_visible", true);
|
||||||
|
clientParams.put("did_rule", 3);
|
||||||
|
clientParams.put("fetch_rule", 1);
|
||||||
|
clientParams.put("identity", "audience");
|
||||||
|
clientParams.put("last_rtt", 0);
|
||||||
|
clientParams.put("live_id", 12);
|
||||||
|
clientParams.put("resp_content_type", "protobuf");
|
||||||
|
clientParams.put("screen_height", 1152);
|
||||||
|
clientParams.put("screen_width", 2048);
|
||||||
|
clientParams.put("tz_name", "Europe/Berlin");
|
||||||
|
clientParams.put("referer", "https, //www.tiktok.com/");
|
||||||
|
clientParams.put("root_referer", "https, //www.tiktok.com/");
|
||||||
|
clientParams.put("msToken", "");
|
||||||
|
clientParams.put("version_code", 180800);
|
||||||
|
clientParams.put("webcast_sdk_version", "1.3.0");
|
||||||
|
clientParams.put("update_version_code", "1.3.0");
|
||||||
|
|
||||||
|
|
||||||
|
return clientParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default Headers for HTTP-Request
|
||||||
|
/// </summary>
|
||||||
|
public static Map<String,String> DefaultRequestHeaders() {
|
||||||
|
var headers = new HashMap<String,String>();
|
||||||
|
|
||||||
|
// headers.put("Connection", "keep-alive");
|
||||||
|
headers.put("Cache-Control", "max-age=0");
|
||||||
|
headers.put("Accept", "text/html,application/json,application/protobuf");
|
||||||
|
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36");
|
||||||
|
headers.put("Referer", "https://www.tiktok.com/");
|
||||||
|
headers.put("Origin", "https://www.tiktok.com");
|
||||||
|
headers.put("Accept-Language", "en-US,en; q=0.9");
|
||||||
|
// headers.put("Accept-Encoding", "gzip, deflate");
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.http.Resource;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ClientFetchDataResponse {
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.http;
|
||||||
|
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface TikTokHttpRequest {
|
||||||
|
TikTokHttpRequest SetQueries(Map<String, Object> queries);
|
||||||
|
|
||||||
|
TikTokHttpRequest setHeader(String key, String value);
|
||||||
|
String Get(String url);
|
||||||
|
|
||||||
|
String Post(String url, HttpRequest.BodyPublisher data);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live;
|
||||||
|
|
||||||
|
public enum ConnectionState
|
||||||
|
{
|
||||||
|
CONNECTING,CONNECTED,DISCONNECTED
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live;
|
||||||
|
|
||||||
|
public interface LiveClient {
|
||||||
|
void run();
|
||||||
|
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
LiveMeta getMeta();
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live;
|
||||||
|
|
||||||
|
public interface LiveMeta
|
||||||
|
{
|
||||||
|
int getViewersCount();
|
||||||
|
|
||||||
|
String getRoomId();
|
||||||
|
String getUserName();
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LiveRoomInfo
|
||||||
|
{
|
||||||
|
private int status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class TikTokLiveMeta implements LiveMeta
|
||||||
|
{
|
||||||
|
private int viewersCount;
|
||||||
|
|
||||||
|
private String roomId;
|
||||||
|
|
||||||
|
private String userName;
|
||||||
|
private ConnectionState connectionState = ConnectionState.DISCONNECTED;
|
||||||
|
|
||||||
|
public boolean hasConnectionState(ConnectionState state)
|
||||||
|
{
|
||||||
|
return connectionState == state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live.models.gift;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class DefaultFormat
|
||||||
|
{
|
||||||
|
private boolean bold ;
|
||||||
|
private String color ;
|
||||||
|
private int font_size ;
|
||||||
|
private boolean italic ;
|
||||||
|
private int italic_angle ;
|
||||||
|
private boolean use_highlight_color ;
|
||||||
|
private boolean use_remote_color ;
|
||||||
|
private int weight ;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live.models.gift;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class DisplayText
|
||||||
|
{
|
||||||
|
private DefaultFormat default_format ;
|
||||||
|
private String default_pattern ;
|
||||||
|
private String key ;
|
||||||
|
private List<Object> pieces ;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live.models.gift;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GiftLabelIcon
|
||||||
|
{
|
||||||
|
private String avg_color ;
|
||||||
|
private int height ;
|
||||||
|
private int image_type ;
|
||||||
|
private boolean is_animated ;
|
||||||
|
private String open_web_url ;
|
||||||
|
private String uri ;
|
||||||
|
private List<String> url_list ;
|
||||||
|
private int width ;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live.models.gift;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class GiftPanelBanner
|
||||||
|
{
|
||||||
|
private List<Object> bg_color_values ;
|
||||||
|
private DisplayText display_text ;
|
||||||
|
private LeftIcon left_icon ;
|
||||||
|
private String schema_url ;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live.models.gift;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Icon {
|
||||||
|
private String avg_color;
|
||||||
|
private int height;
|
||||||
|
private int image_type;
|
||||||
|
private boolean is_animated;
|
||||||
|
private String open_web_url;
|
||||||
|
private String uri;
|
||||||
|
private List<String> url_list;
|
||||||
|
private int width;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live.models.gift;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Image
|
||||||
|
{
|
||||||
|
private String avg_color ;
|
||||||
|
private int height ;
|
||||||
|
private int image_type ;
|
||||||
|
private boolean is_animated ;
|
||||||
|
private String open_web_url ;
|
||||||
|
private String uri ;
|
||||||
|
private List<String> url_list ;
|
||||||
|
private int width ;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live.models.gift;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LeftIcon
|
||||||
|
{
|
||||||
|
private String avg_color ;
|
||||||
|
private int height ;
|
||||||
|
private int image_type ;
|
||||||
|
private boolean is_animated ;
|
||||||
|
private String open_web_url ;
|
||||||
|
private String uri ;
|
||||||
|
private List<String> url_list ;
|
||||||
|
private int width ;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live.models.gift;
|
||||||
|
|
||||||
|
public class LockInfo
|
||||||
|
{
|
||||||
|
public int lock_type;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live.models.gift;
|
||||||
|
|
||||||
|
public class SpecialEffects {
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live.models.gift;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class TikTokGift
|
||||||
|
{
|
||||||
|
private int action_type;
|
||||||
|
private int app_id;
|
||||||
|
private String business_text;
|
||||||
|
private boolean can_put_in_gift_box;
|
||||||
|
private List<Object> color_infos;
|
||||||
|
private boolean combo;
|
||||||
|
private String describe;
|
||||||
|
private int diamond_count;
|
||||||
|
private int duration;
|
||||||
|
private String event_name;
|
||||||
|
private boolean for_custom;
|
||||||
|
private boolean for_linkmic;
|
||||||
|
private GiftLabelIcon gift_label_icon;
|
||||||
|
private GiftPanelBanner gift_panel_banner;
|
||||||
|
private String gift_rank_recommend_info;
|
||||||
|
private int gift_scene;
|
||||||
|
private String gold_effect;
|
||||||
|
private String gray_scheme_url;
|
||||||
|
private String guide_url;
|
||||||
|
private Icon icon;
|
||||||
|
private int id;
|
||||||
|
private Image image;
|
||||||
|
private boolean is_box_gift;
|
||||||
|
private boolean is_broadcast_gift;
|
||||||
|
private boolean is_displayed_on_panel;
|
||||||
|
private boolean is_effect_befview;
|
||||||
|
private boolean is_gray;
|
||||||
|
private boolean is_random_gift;
|
||||||
|
private int item_type;
|
||||||
|
private LockInfo lock_info;
|
||||||
|
private String manual;
|
||||||
|
private String name;
|
||||||
|
private boolean notify;
|
||||||
|
private int primary_effect_id;
|
||||||
|
private String region;
|
||||||
|
private String scheme_url;
|
||||||
|
private SpecialEffects special_effects;
|
||||||
|
private TrackerParams tracker_params;
|
||||||
|
private List<Object> trigger_words;
|
||||||
|
private int type;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.live.models.gift;
|
||||||
|
|
||||||
|
public class TrackerParams {
|
||||||
|
}
|
||||||
109
Client/pom.xml
Normal file
109
Client/pom.xml
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>TikTokLiveJava</artifactId>
|
||||||
|
<groupId>org.example</groupId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>Client</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>16</maven.compiler.source>
|
||||||
|
<maven.compiler.target>16</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<sourceDirectory>target/generated-sources/protobuf/java</sourceDirectory>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>kr.motd.maven</groupId>
|
||||||
|
<artifactId>os-maven-plugin</artifactId>
|
||||||
|
<version>1.7.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>initialize</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>detect</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.xolstice.maven.plugins</groupId>
|
||||||
|
<artifactId>protobuf-maven-plugin</artifactId>
|
||||||
|
<version>0.6.1</version>
|
||||||
|
<configuration>
|
||||||
|
<protocArtifact>com.google.protobuf:protoc:3.17.3:exe:${os.detected.classifier}</protocArtifact>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>compile</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Add other plugins as needed -->
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.java-websocket</groupId>
|
||||||
|
<artifactId>Java-WebSocket</artifactId>
|
||||||
|
<version>1.5.4</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.protobuf</groupId>
|
||||||
|
<artifactId>protobuf-java</artifactId>
|
||||||
|
<version>3.23.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.22</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.example</groupId>
|
||||||
|
<artifactId>API</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.gson</groupId>
|
||||||
|
<artifactId>gson</artifactId>
|
||||||
|
<version>2.10.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>3.12.4</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>4.13.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>RELEASE</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
11
Client/src/main/java/io/github/jwdeveloper/tiktok/Main.java
Normal file
11
Client/src/main/java/io/github/jwdeveloper/tiktok/Main.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class Main
|
||||||
|
{
|
||||||
|
public static void main(String[] args)
|
||||||
|
{
|
||||||
|
System.out.println("Hello world!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class TikTokLive
|
||||||
|
{
|
||||||
|
public static TikTokClientBuilder newClient(String userName)
|
||||||
|
{
|
||||||
|
return new TikTokClientBuilder(userName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.events;
|
||||||
|
|
||||||
|
public class TikTokChatEvent {
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.events;
|
||||||
|
|
||||||
|
public class TikTokEvent
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package io.github.jwdeveloper.tiktok.events;
|
||||||
|
|
||||||
|
public class TikTokGiftEvent {
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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};";
|
||||||
|
}*/
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
310
Client/src/main/proto/tiktokSchema.proto
Normal file
310
Client/src/main/proto/tiktokSchema.proto
Normal 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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
20456
Client/src/test/resources/gifts.json
Normal file
20456
Client/src/test/resources/gifts.json
Normal file
File diff suppressed because it is too large
Load Diff
22
pom.xml
Normal file
22
pom.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>org.example</groupId>
|
||||||
|
<artifactId>TikTokLiveJava</artifactId>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<modules>
|
||||||
|
<module>API</module>
|
||||||
|
<module>Client</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>16</maven.compiler.source>
|
||||||
|
<maven.compiler.target>16</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
</project>
|
||||||
Reference in New Issue
Block a user