├── jitpack.yml ├── settings.gradle.kts ├── gradle.properties ├── image ├── banner.png ├── scope_chat.png └── doc_install │ ├── 1.png │ └── 2.png ├── .gitbook.yaml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ └── java │ │ └── xyz │ │ └── r2turntrue │ │ └── chzzk4j │ │ ├── chat │ │ ├── event │ │ │ ├── ChzzkEvent.java │ │ │ ├── ChatMessageEvent.java │ │ │ ├── NormalDonationEvent.java │ │ │ ├── MissionDonationEvent.java │ │ │ ├── SubscriptionMessageEvent.java │ │ │ ├── ErrorEvent.java │ │ │ ├── InternalChzzkMsgEvent.java │ │ │ ├── ConnectEvent.java │ │ │ └── ConnectionClosedEvent.java │ │ ├── WsMessageServerboundPing.java │ │ ├── WsMessageServerboundPong.java │ │ ├── DonationMessage.java │ │ ├── SubscriptionMessage.java │ │ ├── WsMessageClientboundConnected.java │ │ ├── WsMessageBase.java │ │ ├── WsMessageServerboundRequestRecentChat.java │ │ ├── WsMessageServerboundConnect.java │ │ ├── ChzzkChatBuilder.java │ │ ├── MissionDonationMessage.java │ │ ├── WsMessageServerboundSendChat.java │ │ ├── WsMessageTypes.java │ │ ├── WsMessageClientboundChat.java │ │ ├── WsMessageClientboundRecentChat.java │ │ ├── ChzzkChat.java │ │ └── ChatMessage.java │ │ ├── session │ │ ├── event │ │ │ ├── SessionEvent.java │ │ │ ├── SessionRecreateEvent.java │ │ │ ├── SessionConnectedEvent.java │ │ │ ├── SessionDisconnectedEvent.java │ │ │ ├── SessionChatMessageEvent.java │ │ │ ├── SessionDonationEvent.java │ │ │ ├── SessionNewSubscriberEvent.java │ │ │ ├── SessionSubscribedEvent.java │ │ │ ├── SessionUnsubscribedEvent.java │ │ │ └── SessionSubscriptionRevokedEvent.java │ │ ├── ChzzkClientSession.java │ │ ├── ChzzkUserSession.java │ │ ├── message │ │ │ ├── system │ │ │ │ ├── ClientboundSystemConnected.java │ │ │ │ ├── ClientboundSystemRevoked.java │ │ │ │ ├── ClientboundSystemSubscribed.java │ │ │ │ └── ClientboundSystemUnsubscribed.java │ │ │ ├── SessionNewSubscriberMessage.java │ │ │ ├── SessionDonationMessage.java │ │ │ └── SessionChatMessage.java │ │ ├── ChzzkSessionSubscriptionType.java │ │ └── ChzzkSessionBuilder.java │ │ ├── types │ │ ├── channel │ │ │ ├── RestrictChannelRequestBody.java │ │ │ ├── live │ │ │ │ ├── Resolution.java │ │ │ │ ├── ChzzkLiveChannel.java │ │ │ │ ├── ChzzkLiveCategory.java │ │ │ │ ├── ChzzkLiveSettings.java │ │ │ │ ├── ChzzkLiveDetail.java │ │ │ │ └── ChzzkLiveStatus.java │ │ │ ├── recommendation │ │ │ │ ├── ChzzkRecommendationChannel.java │ │ │ │ └── ChzzkRecommendationChannels.java │ │ │ ├── emoticon │ │ │ │ ├── ChzzkChannelEmoticonData.java │ │ │ │ └── ChzzkChannelEmotePackData.java │ │ │ ├── ChzzkChannelPersonalData.java │ │ │ ├── ChzzkChannelFollower.java │ │ │ ├── ChzzkChannelFollowerResponse.java │ │ │ ├── ChzzkChannelSubscriberResponse.java │ │ │ ├── ChzzkChannelFollowingData.java │ │ │ ├── ChzzkChannel.java │ │ │ ├── ChzzkChannelSubscriber.java │ │ │ ├── ChzzkChannelManager.java │ │ │ ├── ChzzkChannelRules.java │ │ │ └── ChzzkPartialChannel.java │ │ ├── ChzzkFollowingStatusResponse.java │ │ ├── ChzzkRestrictedChannel.java │ │ ├── ChzzkRestrictedChannelResponse.java │ │ ├── ChzzkUser.java │ │ └── ChzzkChatSettings.java │ │ ├── exception │ │ ├── NotLoggedInException.java │ │ ├── NoAccessTokenOnlySupported.java │ │ ├── ChannelNotExistsException.java │ │ ├── NotExistsException.java │ │ └── ChatFailedConnectException.java │ │ ├── auth │ │ ├── oauth │ │ │ ├── TokenResponseBody.java │ │ │ ├── TokenRefreshRequestBody.java │ │ │ └── TokenRequestBody.java │ │ ├── ChzzkLoginAdapter.java │ │ ├── ChzzkLegacyLoginAdapter.java │ │ ├── ChzzkSimpleUserLoginAdapter.java │ │ ├── ChzzkLoginResult.java │ │ ├── ChzzkOauthCodeLoginAdapter.java │ │ └── ChzzkOauthLoginAdapter.java │ │ ├── util │ │ ├── Chrome.java │ │ ├── HttpUtils.java │ │ └── RawApiUtils.java │ │ ├── ChzzkClientBuilder.java │ │ └── naver │ │ └── NaverAutologinAdapter.java └── test │ └── java │ ├── ChatReconnectTest.java │ ├── BothLoginTest.java │ ├── LiveApiTest.java │ ├── OauthLoginTest.java │ ├── ChatTest.java │ ├── NaverLoginTest.java │ ├── ChzzkTestBase.java │ ├── SessionApiTest.java │ ├── ChannelApiTest.java │ └── OpenChatApiTest.java ├── publish.properties.example ├── docs ├── LiveInfo.md ├── Introduction.md ├── OpenAPIChatSend.md ├── ChannelInfo.md ├── ChatConnect.md ├── Migrate0012to010.md ├── LoginBoth.md ├── SUMMARY.md ├── AuthenticatedUserInfo.md ├── Installation.md ├── Login.md ├── SessionConnect.md ├── SessionEvent.md ├── GettingStarted.md ├── BroadcastSettings.md ├── ChatMessage.md └── LoginOauth.md ├── env.properties.example ├── .gitignore ├── LICENSE ├── .github └── workflows │ └── gradle-publish.yml ├── gradlew.bat ├── README.md └── gradlew /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "chzzk4j" 2 | 3 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Dfile.encoding=UTF-8 2 | org.gradle.console=plain -------------------------------------------------------------------------------- /image/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R2turnTrue/chzzk4j/HEAD/image/banner.png -------------------------------------------------------------------------------- /image/scope_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R2turnTrue/chzzk4j/HEAD/image/scope_chat.png -------------------------------------------------------------------------------- /image/doc_install/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R2turnTrue/chzzk4j/HEAD/image/doc_install/1.png -------------------------------------------------------------------------------- /image/doc_install/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R2turnTrue/chzzk4j/HEAD/image/doc_install/2.png -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs 2 | 3 | structure: 4 | readme: ./docs/Introduction.md 5 | summary: ./docs/SUMMARY.md -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R2turnTrue/chzzk4j/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/event/ChzzkEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat.event; 2 | 3 | public class ChzzkEvent { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/event/SessionEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.event; 2 | 3 | public class SessionEvent { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/event/SessionRecreateEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.event; 2 | 3 | public class SessionRecreateEvent extends SessionEvent { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/event/SessionConnectedEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.event; 2 | 3 | public class SessionConnectedEvent extends SessionEvent { 4 | } 5 | -------------------------------------------------------------------------------- /publish.properties.example: -------------------------------------------------------------------------------- 1 | signing.keyId=KEY ID 2 | signing.password=SIGNING PASSWORD 3 | signing.secretKeyRingFile=.gradle/secring.gpg 4 | 5 | nexusUsername=NEXUS USERNAME 6 | nexusPassword=NEXUS PASSWORD -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/event/SessionDisconnectedEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.event; 2 | 3 | public class SessionDisconnectedEvent extends SessionEvent { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/RestrictChannelRequestBody.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel; 2 | 3 | public record RestrictChannelRequestBody(String targetChannelId) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/WsMessageServerboundPing.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | class WsMessageServerboundPing { 4 | public int cmd = WsMessageTypes.Commands.PING; 5 | public String ver = "3"; 6 | } -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/WsMessageServerboundPong.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | class WsMessageServerboundPong { 4 | public int cmd = WsMessageTypes.Commands.PONG; 5 | public String ver = "3"; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/DonationMessage.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | public class DonationMessage extends ChatMessage { 4 | public int getPayAmount() { 5 | return extras.payAmount; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /docs/LiveInfo.md: -------------------------------------------------------------------------------- 1 | # Live Info 2 | 이 페이지에서는 생방송 정보를 가져오는 방법에 대해 설명합니다. 3 | 4 | {% hint style="info" %} 5 | 해당 API는 API 키를 필요로 하지 않습니다! 6 | {% endhint %} 7 | 8 | ## ID를 통해 채널 라이브 정보 가져오기 9 | ```java 10 | ChzzkLiveStatus status = client.fetchLiveStatus("채널 ID"); 11 | ``` -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/exception/NotLoggedInException.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.exception; 2 | 3 | public class NotLoggedInException extends Exception { 4 | public NotLoggedInException(String reason) { 5 | super(reason); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/exception/NoAccessTokenOnlySupported.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.exception; 2 | 3 | public class NoAccessTokenOnlySupported extends Exception { 4 | public NoAccessTokenOnlySupported(String reason) { 5 | super(reason); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/auth/oauth/TokenResponseBody.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.auth.oauth; 2 | 3 | public record TokenResponseBody( 4 | String accessToken, 5 | String refreshToken, 6 | String tokenType, 7 | int expiresIn 8 | ) { 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/auth/oauth/TokenRefreshRequestBody.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.auth.oauth; 2 | 3 | public record TokenRefreshRequestBody( 4 | String grantType, 5 | String refreshToken, 6 | String clientId, 7 | String clientSecret 8 | ) { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/auth/oauth/TokenRequestBody.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.auth.oauth; 2 | 3 | public record TokenRequestBody( 4 | String grantType, 5 | String clientId, 6 | String clientSecret, 7 | String code, 8 | String state 9 | ) { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/auth/ChzzkLoginAdapter.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.auth; 2 | 3 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 4 | 5 | import java.util.concurrent.CompletableFuture; 6 | 7 | public interface ChzzkLoginAdapter { 8 | CompletableFuture authorize(ChzzkClient client); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/event/ChatMessageEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat.event; 2 | 3 | import xyz.r2turntrue.chzzk4j.chat.ChatMessage; 4 | 5 | public class ChatMessageEvent extends InternalChzzkMsgEvent { 6 | public ChatMessageEvent(ChatMessage msg) { 7 | super(msg); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/SubscriptionMessage.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | public class SubscriptionMessage extends ChatMessage { 4 | public int getSubscriptionMonth() { 5 | return extras.month; 6 | } 7 | 8 | public String getSubscriptionTierName() { 9 | return extras.tierName; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/event/NormalDonationEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat.event; 2 | 3 | import xyz.r2turntrue.chzzk4j.chat.DonationMessage; 4 | 5 | public class NormalDonationEvent extends InternalChzzkMsgEvent { 6 | public NormalDonationEvent(DonationMessage msg) { 7 | super(msg); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /env.properties.example: -------------------------------------------------------------------------------- 1 | NID_AUT=NID_AUT cookie value 2 | NID_SES=NID_SES cookie value 3 | 4 | CURRENT_USER_ID=id of user that current logged in with NID_AUT and NID_SES (ex: 5e1fca3b495440342ddc82182b1e36fe) 5 | 6 | NAVER_ID=your naver id 7 | NAVER_PW=your naver pw 8 | 9 | CHROMEDRIVER=your chromedriver path 10 | 11 | API_CLIENT_ID=chzzk api client id 12 | API_SECRET=chzzk api secret key -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/event/MissionDonationEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat.event; 2 | 3 | import xyz.r2turntrue.chzzk4j.chat.MissionDonationMessage; 4 | 5 | public class MissionDonationEvent extends InternalChzzkMsgEvent { 6 | public MissionDonationEvent(MissionDonationMessage msg) { 7 | super(msg); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/WsMessageClientboundConnected.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | class WsMessageClientboundConnected extends WsMessageBase { 4 | static class Body { 5 | public String sid; 6 | } 7 | 8 | public WsMessageClientboundConnected.Body bdy = new Body(); 9 | public int retCode; 10 | public String retMsg; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/event/SubscriptionMessageEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat.event; 2 | 3 | import xyz.r2turntrue.chzzk4j.chat.SubscriptionMessage; 4 | 5 | public class SubscriptionMessageEvent extends InternalChzzkMsgEvent { 6 | public SubscriptionMessageEvent(SubscriptionMessage msg) { 7 | super(msg); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/event/ErrorEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat.event; 2 | 3 | public class ErrorEvent extends ChzzkEvent { 4 | private Exception exception; 5 | 6 | public ErrorEvent(Exception exception) { 7 | this.exception = exception; 8 | } 9 | 10 | public Exception getException() { 11 | return exception; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/event/InternalChzzkMsgEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat.event; 2 | 3 | import xyz.r2turntrue.chzzk4j.chat.ChatMessage; 4 | 5 | class InternalChzzkMsgEvent extends ChzzkEvent { 6 | private T msg; 7 | 8 | public InternalChzzkMsgEvent(T msg) { 9 | this.msg = msg; 10 | } 11 | 12 | public T getMessage() { 13 | return msg; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/Introduction.md: -------------------------------------------------------------------------------- 1 | ![](../image/banner.png) 2 | 3 | ![](https://img.shields.io/maven-central/v/io.github.R2turnTrue/chzzk4j) / 4 | [![](https://img.shields.io/badge/Discord%20Server-7289da)](https://discord.gg/GgNXbzZeDk) 5 | 6 | # About 7 | 8 | * **0.0.12 이하 버전을 사용하신다면, [과거 버전의 문서](https://github.com/R2turnTrue/chzzk4j/blob/2a5936c9570220957c3ef4f13462d12d4d19e4ff/README.md) 파일을 확인해주세요!** 9 | 10 | chzzk4j는 네이버 스트리밍 서비스인 [치지직](https://chzzk.naver.com)의 비공식 Java API 라이브러리입니다. -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/WsMessageBase.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | class WsMessageBase { 4 | public String cid; 5 | public String svcid = "game"; 6 | public String ver = "3"; 7 | /** 8 | * * Note: only used in serverbound messages 9 | */ 10 | public int cmd = -1; 11 | 12 | public WsMessageBase(int cmd) { 13 | this.cmd = cmd; 14 | } 15 | 16 | public WsMessageBase() { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/exception/ChannelNotExistsException.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.exception; 2 | 3 | public class ChannelNotExistsException extends NotExistsException { 4 | /** 5 | * Constructs an {@code ChannelNotExistsException}. 6 | * 7 | * @param reason Detailed message explaining the reason for the failure. 8 | */ 9 | public ChannelNotExistsException(String reason) { 10 | super(reason); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/ChzzkClientSession.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session; 2 | 3 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 4 | 5 | public class ChzzkClientSession extends ChzzkSession { 6 | 7 | ChzzkClientSession(ChzzkClient chzzk, boolean autoRecreate) { 8 | super(chzzk, autoRecreate); 9 | 10 | sessionListUrl = "/open/v1/sessions/client"; 11 | sessionCreateUrl = "/open/v1/sessions/auth/client"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/ChzzkUserSession.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session; 2 | 3 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 4 | 5 | public class ChzzkUserSession extends ChzzkSession { 6 | 7 | ChzzkUserSession(ChzzkClient chzzk, boolean autoRecreate) { 8 | super(chzzk, autoRecreate); 9 | 10 | sessionListUrl = "/open/v1/sessions"; 11 | sessionCreateUrl = "/open/v1/sessions/auth"; 12 | userSession = true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/exception/NotExistsException.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.exception; 2 | 3 | import java.io.InvalidObjectException; 4 | 5 | public class NotExistsException extends InvalidObjectException { 6 | /** 7 | * Constructs an {@code NotExistsException}. 8 | * 9 | * @param reason Detailed message explaining the reason for the failure. 10 | */ 11 | public NotExistsException(String reason) { 12 | super(reason); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/message/system/ClientboundSystemConnected.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.message.system; 2 | 3 | public class ClientboundSystemConnected { 4 | private String sessionKey; 5 | 6 | public String getSessionKey() { 7 | return sessionKey; 8 | } 9 | 10 | @Override 11 | public String toString() { 12 | return "ClientboundSystemConnected{" + 13 | "sessionKey='" + sessionKey + '\'' + 14 | '}'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/event/SessionChatMessageEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.event; 2 | 3 | import xyz.r2turntrue.chzzk4j.session.message.SessionChatMessage; 4 | 5 | public class SessionChatMessageEvent extends SessionEvent { 6 | private SessionChatMessage message; 7 | 8 | public SessionChatMessageEvent(SessionChatMessage message) { 9 | this.message = message; 10 | } 11 | 12 | public SessionChatMessage getMessage() { 13 | return message; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/event/SessionDonationEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.event; 2 | 3 | import xyz.r2turntrue.chzzk4j.session.message.SessionDonationMessage; 4 | 5 | public class SessionDonationEvent extends SessionEvent { 6 | private SessionDonationMessage message; 7 | 8 | public SessionDonationEvent(SessionDonationMessage message) { 9 | this.message = message; 10 | } 11 | 12 | public SessionDonationMessage getMessage() { 13 | return message; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/exception/ChatFailedConnectException.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.exception; 2 | 3 | public class ChatFailedConnectException extends IllegalStateException { 4 | public int errorCode; 5 | public String errorMessage; 6 | 7 | public ChatFailedConnectException(int errorCode, String errorMessage) { 8 | super("Failed to connect to chat! (Message: " + errorMessage + ", Code: " + errorCode + ")"); 9 | this.errorCode = errorCode; 10 | this.errorMessage = errorMessage; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/event/SessionNewSubscriberEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.event; 2 | 3 | import xyz.r2turntrue.chzzk4j.session.message.SessionNewSubscriberMessage; 4 | 5 | public class SessionNewSubscriberEvent extends SessionEvent { 6 | private SessionNewSubscriberMessage message; 7 | 8 | public SessionNewSubscriberEvent(SessionNewSubscriberMessage message) { 9 | this.message = message; 10 | } 11 | 12 | public SessionNewSubscriberMessage getMessage() { 13 | return message; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/OpenAPIChatSend.md: -------------------------------------------------------------------------------- 1 | # OpenAPIChatSend 2 | 이 페이지에서는 OpenAPI를 이용해 채팅을 전송하는 방법에 대해 설명합니다. 3 | 4 | {% hint style="info" %} 5 | 해당 API는 [API 키](GettingStarted.md), 그리고 [OpenAPI를 통한 인증](LoginOauth.md)를 필요로 합니다! 6 | {% endhint %} 7 | 8 | ## 채팅 전송 9 | 10 | `ChzzkClient`의 `sendChatToLoggedInChannel`을 통해 채팅을 전송할 수 있습니다. 11 | 12 | 이 때, **API 키를 소유한 계정**이 **`ChzzkClient`에 로그인된 채널**에 채팅을 전송합니다. 13 | 14 | ```java 15 | client.sendChatToLoggedInChannel("안녕, 세상!").join(); 16 | ``` 17 | 18 | ### 결과 19 | 20 | ```text 21 | (ChzzkClient에 로그인된 채널) 22 | 23 | // ... 24 | [API 키 소유자]: 안녕, 세상! 25 | ``` -------------------------------------------------------------------------------- /docs/ChannelInfo.md: -------------------------------------------------------------------------------- 1 | # Channel Info 2 | 이 페이지에서는 채널 정보를 가져오는 방법에 대해 설명합니다. 3 | 4 | {% hint style="info" %} 5 | 해당 API는 API 키를 필요로 하지 않습니다! 6 | {% endhint %} 7 | 8 | ## ID를 통해 채널 정보 가져오기 9 | ```java 10 | String CHANNEL_ID = "채널 ID"; 11 | ChzzkChannel channel = client.getChannel("CHANNEL_ID"); 12 | 13 | // 채널 이름 출력하기 14 | System.out.println(channel.getChannelName()); 15 | 16 | ChzzkChannelRules rules = channel.getRules(client); 17 | // 또는... 18 | ChzzkChannelRules rules = client.getChannelChatRules("7ce8032370ac5121dcabce7bad375ced"); 19 | 20 | // 채널 규칙 출력하기 21 | System.out.println(rules.getRule()); 22 | ``` -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/event/ConnectEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat.event; 2 | 3 | import xyz.r2turntrue.chzzk4j.chat.ChzzkChat; 4 | 5 | public class ConnectEvent extends ChzzkEvent { 6 | private ChzzkChat chat; 7 | private boolean reconnecting; 8 | 9 | public ConnectEvent(ChzzkChat chat, boolean reconnecting) { 10 | this.chat = chat; 11 | this.reconnecting = reconnecting; 12 | } 13 | 14 | public ChzzkChat getChat() { 15 | return chat; 16 | } 17 | 18 | public boolean isReconnecting() { 19 | return reconnecting; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/WsMessageServerboundRequestRecentChat.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | class WsMessageServerboundRequestRecentChat extends WsMessageBase { 4 | public WsMessageServerboundRequestRecentChat() { 5 | super(WsMessageTypes.Commands.REQUEST_RECENT_CHAT); 6 | } 7 | 8 | static class Body { 9 | public int recentMessageCount; 10 | } 11 | 12 | //public String ver = "3"; 13 | public WsMessageServerboundRequestRecentChat.Body bdy = new WsMessageServerboundRequestRecentChat.Body(); 14 | public String sid; 15 | public int tid = 2; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/live/Resolution.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel.live; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | public enum Resolution { 6 | 7 | R_1080(1080), 8 | R_720(720), 9 | R_480(480), 10 | R_360(360), 11 | R_270(270), 12 | R_144(144); 13 | 14 | private final int raw; 15 | 16 | Resolution(int raw) { 17 | this.raw = raw; 18 | } 19 | 20 | public int getRaw() { 21 | return raw; 22 | } 23 | 24 | public @NotNull String getRawAsString() { 25 | return Integer.toString(raw); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/ChatReconnectTest.java: -------------------------------------------------------------------------------- 1 | import com.google.gson.Gson; 2 | import org.junit.jupiter.api.Test; 3 | import xyz.r2turntrue.chzzk4j.chat.*; 4 | import xyz.r2turntrue.chzzk4j.util.RawApiUtils; 5 | 6 | import java.io.IOException; 7 | 8 | public class ChatReconnectTest extends ChzzkTestBase { 9 | /* 10 | // to test memory leaks 11 | @Test 12 | void testingChatReconnect() throws IOException, InterruptedException { 13 | ChzzkChat chat = chzzk.chat("e23a2e05c02e77a3516f14f3f4f4312b") 14 | .build(); 15 | chat.connectBlocking(); 16 | Thread.sleep(700000); 17 | chat.closeBlocking(); 18 | } 19 | */ 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea 9 | *.iws 10 | *.iml 11 | *.ipr 12 | out/ 13 | !**/src/main/**/out/ 14 | !**/src/test/**/out/ 15 | 16 | ### Eclipse ### 17 | .apt_generated 18 | .classpath 19 | .factorypath 20 | .project 21 | .settings 22 | .springBeans 23 | .sts4-cache 24 | bin/ 25 | !**/src/main/**/bin/ 26 | !**/src/test/**/bin/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | /nbdist/ 33 | /.nb-gradle/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | ### Mac OS ### 39 | .DS_Store 40 | 41 | env.properties 42 | publish.properties -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/event/SessionSubscribedEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.event; 2 | 3 | import xyz.r2turntrue.chzzk4j.session.ChzzkSessionSubscriptionType; 4 | 5 | public class SessionSubscribedEvent extends SessionEvent { 6 | private ChzzkSessionSubscriptionType eventType; 7 | private String channelId; 8 | 9 | public SessionSubscribedEvent(ChzzkSessionSubscriptionType eventType, String channelId) { 10 | this.eventType = eventType; 11 | this.channelId = channelId; 12 | } 13 | 14 | public ChzzkSessionSubscriptionType getEventType() { 15 | return eventType; 16 | } 17 | 18 | public String getChannelId() { 19 | return channelId; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/event/SessionUnsubscribedEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.event; 2 | 3 | import xyz.r2turntrue.chzzk4j.session.ChzzkSessionSubscriptionType; 4 | 5 | public class SessionUnsubscribedEvent extends SessionEvent { 6 | private ChzzkSessionSubscriptionType eventType; 7 | private String channelId; 8 | 9 | public SessionUnsubscribedEvent(ChzzkSessionSubscriptionType eventType, String channelId) { 10 | this.eventType = eventType; 11 | this.channelId = channelId; 12 | } 13 | 14 | public ChzzkSessionSubscriptionType getEventType() { 15 | return eventType; 16 | } 17 | 18 | public String getChannelId() { 19 | return channelId; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/ChatConnect.md: -------------------------------------------------------------------------------- 1 | # 채팅 연결하기 2 | 이 페이지에서는 채팅에 연결하는 방법에 대해 다룹니다. 3 | 4 | {% hint style="warning" %} 5 | 현재 chzzk4j는 치지직의 비공개 API에서 OpenAPI로 마이그레이션 중에 있습니다. 6 | 되도록이면 공식 API를 활용한 채팅을 이용해주세요. 7 | {% endhint %} 8 | 9 | ## `ChzzkChat` 생성하기 10 | ```java 11 | ChzzkChat chat = new ChzzkChatBuilder(client, 12 | "연결하려는 채널의 ID") 13 | .build(); 14 | 15 | chat.on(ConnectEvent.class, (evt) -> { 16 | System.out.println("Connected to chat!"); 17 | if (!evt.isReconnecting()) { 18 | chat.requestRecentChat(50); // 만약 재연결이 아니라면 최근 50개의 채팅을 요청합니다. 19 | // 요청한 채팅은 채팅 이벤트로 받게 됩니다. 20 | } 21 | }); 22 | ``` 23 | 24 | ## `ChzzkChat` 연결하기 25 | ```java 26 | // 비동기 27 | chat.connectAsync(); 28 | 29 | // 동기 30 | chat.connectBlocking(); 31 | ``` -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/event/SessionSubscriptionRevokedEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.event; 2 | 3 | import xyz.r2turntrue.chzzk4j.session.ChzzkSessionSubscriptionType; 4 | 5 | public class SessionSubscriptionRevokedEvent extends SessionEvent { 6 | private ChzzkSessionSubscriptionType eventType; 7 | private String channelId; 8 | 9 | public SessionSubscriptionRevokedEvent(ChzzkSessionSubscriptionType eventType, String channelId) { 10 | this.eventType = eventType; 11 | this.channelId = channelId; 12 | } 13 | 14 | public ChzzkSessionSubscriptionType getEventType() { 15 | return eventType; 16 | } 17 | 18 | public String getChannelId() { 19 | return channelId; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/Migrate0012to010.md: -------------------------------------------------------------------------------- 1 | # Migrate 2 | 3 | chzzk4j의 `0.1.0` 버전에서는 많은 사항이 변경되었습니다. 이에 따라 업그레이드를 위한 문서를 작성하였습니다. 4 | 5 | ## `ChzzkClient` 6 | 7 | `Chzzk` 클래스가 `ChzzkClient`로, `ChzzkBuilder`가 `ChzzkClientBuilder`로 변경되었습니다. 8 | 9 | ## `getXXX` -> `fetchXXX` 10 | 11 | `Chzzk` 클래스의 `getChannel`, `getLiveStatus` 등 정보를 받는 메서드의 이름의 `fetchChannel`, `fetchLiveStatus` 등으로 변경되었습니다. 12 | 13 | ## OpenAPI 14 | 15 | 채팅, 로그인 등 다양한 부분에서 OpenAPI 지원이 추가되었습니다. 최대한 레거시 API 사용을 지양하고 OpenAPI를 활용한 코드로 대체해주세요. 16 | 17 | ## (레거시) 채팅 18 | 19 | `Chzzk` 클래스의 `chat` 메서드가 사라지고, 채팅 이벤트 핸들링 방식이 변경되는 등 다양한 수정 사항이 있었습니다. 채팅 연동 부분은 [채팅 연결하기](ChatConnect.md), [채팅 메시지](ChatMessage.md) 등을 참고해 수정해주세요. 20 | 21 | ## 로그인/인증 22 | 23 | 다양한 로그인 방식을 수용할 수 있도록 로그인 코드를 대폭 수정하였습니다. [로그인하기](Login.md) 문서를 참고해 로그인 관련 코드을 변경해주세요. -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/WsMessageServerboundConnect.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | class WsMessageServerboundConnect extends WsMessageBase { 4 | public WsMessageServerboundConnect() { 5 | super(WsMessageTypes.Commands.CONNECT); 6 | } 7 | 8 | static class Body { 9 | public String accTkn; 10 | public String auth; 11 | public String devName = "Google Chrome/141.0.0.0"; 12 | public int devType = 2001; 13 | public String libVer = "4.9.3"; 14 | public String locale = "ko-KR"; 15 | public String osVer = "Windows/10"; 16 | public String timezone = "Asia/Seoul"; 17 | public String uid; 18 | } 19 | 20 | public Body bdy; 21 | public int tid = 1; 22 | } 23 | -------------------------------------------------------------------------------- /docs/LoginBoth.md: -------------------------------------------------------------------------------- 1 | # LoginBoth 2 | 이 페이지에서는 `ChzzkClient`에서 두 개 이상의 `ChzzkLoginAdapter`를 사용하는 방법에 대해 설명합니다. 3 | 4 | ## 두 개 이상의 `ChzzkLoginAdapter`를 사용하는 이유가 있나요? 5 | 6 | 현재 치지직의 공식 API는 모두 완성되지 않은 상태로, 로그인된 유저의 팔로우 정보와 같은 일부 정보는 가져올 수 없습니다. 7 | 8 | 따라서 OpenAPI를 이용한 인증과 네이버 자동 로그인 혹은 쿠키를 이용한 인증 방식 모두 동시에 이용할 수 있도록 구현하였습니다. 9 | 10 | ## 어떻게 하나요? 11 | 12 | 간단합니다. `ChzzkClientBuilder`를 이용할 때 `withLoginAdapter`를 여러 번 호출하면 됩니다. 13 | 14 | ```java 15 | var naverAdapter = new NaverAutologinAdapter("네이버 ID", "네이버 비밀번호"); 16 | var oauthAdapter = new ChzzkOauthLoginAdapter("localhost", 8080); 17 | 18 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 19 | .withDebugMode() 20 | .withLoginAdapter(naverAdapter) 21 | .withLoginAdapter(oauthAdapter) 22 | .build(); 23 | ``` -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | ## 시작하기 4 | 5 | * [소개](Introduction.md) 6 | * [설치](Installation.md) 7 | * [시작하기](GettingStarted.md) 8 | 9 | ## 유저 인증 10 | 11 | * [OpenAPI를 통한 인증](LoginOauth.md) 12 | * [비공개 API를 통한 인증](Login.md) 13 | * [동시에 사용하기](LoginBoth.md) 14 | 15 | ## 비공식 API 사용하기 16 | 17 | * [채널 정보 가져오기](ChannelInfo.md) 18 | * [라이브 정보 가져오기](LiveInfo.md) 19 | * [로그인한 사용자의 정보 가져오기](AuthenticatedUserInfo.md) 20 | 21 | ## 공식 API 사용하기 22 | 23 | * [방송 / 채팅 설정](BroadcastSettings.md) 24 | 25 | ## 채팅 (OpenAPI) 26 | 27 | * [세션 생성 / 연결하기](SessionConnect.md) 28 | * [세션 이벤트](SessionEvent.md) 29 | * [채팅 전송](OpenAPIChatSend.md) 30 | 31 | ## 채팅 (레거시) 32 | 33 | * [(레거시) 채팅 연결하기](ChatConnect.md) 34 | * [(레거시) 채팅 이벤트](ChatMessage.md) 35 | 36 | ## 기타 37 | 38 | * [0.0.12에서 업그레이드하기](Migrate0012to010.md) -------------------------------------------------------------------------------- /docs/AuthenticatedUserInfo.md: -------------------------------------------------------------------------------- 1 | # UserInfo 2 | 이 페이지에서는 로그인한 사용자의 정보를 가져오는 방법에 대해 설명합니다. 3 | 4 | {% hint style="warning" %} 5 | 해당 API는 [내부 API를 통한 인증](Login.md)를 필요로 합니다! 6 | {% endhint %} 7 | 8 | {% hint style="info" %} 9 | 해당 API는 API 키를 필요로 하지 않습니다! 10 | {% endhint %} 11 | 12 | ## 로그인한 사용자의 정보 가져오기 13 | ```java 14 | ChzzkUser myself = client.fetchLoggedUser(); 15 | ``` 16 | 17 | ## 로그인한 사용자의 채널 팔로우 상태 확인하기 18 | ```java 19 | ChzzkChannelFollowingData following = loginChzzk.fetchFollowingStatus("채널 ID"); 20 | 21 | if (followingStatus.isFollowing()) { 22 | System.out.println("현재 해당 채널을 팔로우 중입니다!"); 23 | } else { 24 | System.out.println("현재 해당 채널을 팔로우하지 않았습니다!"); 25 | } 26 | ``` 27 | 28 | ## 로그인한 사용자의 팔로우 채널 목록 가져오기 29 | ```java 30 | ChzzkChannel[] followingChannels = client.fetchFollowingChannels(); 31 | ``` -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/message/system/ClientboundSystemRevoked.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.message.system; 2 | 3 | import xyz.r2turntrue.chzzk4j.session.ChzzkSessionSubscriptionType; 4 | 5 | public class ClientboundSystemRevoked { 6 | private String eventType; 7 | private String channelId; 8 | 9 | public ChzzkSessionSubscriptionType getEventType() { 10 | return ChzzkSessionSubscriptionType.valueOf(eventType); 11 | } 12 | 13 | public String getChannelId() { 14 | return channelId; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return "ClientboundSystemRevoked{" + 20 | "eventType='" + eventType + '\'' + 21 | ", channelId='" + channelId + '\'' + 22 | '}'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/message/system/ClientboundSystemSubscribed.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.message.system; 2 | 3 | import xyz.r2turntrue.chzzk4j.session.ChzzkSessionSubscriptionType; 4 | 5 | public class ClientboundSystemSubscribed { 6 | private String eventType; 7 | private String channelId; 8 | 9 | public ChzzkSessionSubscriptionType getEventType() { 10 | return ChzzkSessionSubscriptionType.valueOf(eventType); 11 | } 12 | 13 | public String getChannelId() { 14 | return channelId; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return "ClientboundSystemSubscribed{" + 20 | "eventType='" + eventType + '\'' + 21 | ", channelId='" + channelId + '\'' + 22 | '}'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/message/system/ClientboundSystemUnsubscribed.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.message.system; 2 | 3 | import xyz.r2turntrue.chzzk4j.session.ChzzkSessionSubscriptionType; 4 | 5 | public class ClientboundSystemUnsubscribed { 6 | private String eventType; 7 | private String channelId; 8 | 9 | public ChzzkSessionSubscriptionType getEventType() { 10 | return ChzzkSessionSubscriptionType.valueOf(eventType); 11 | } 12 | 13 | public String getChannelId() { 14 | return channelId; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return "ClientboundSystemUnsubscribed{" + 20 | "eventType='" + eventType + '\'' + 21 | ", channelId='" + channelId + '\'' + 22 | '}'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/ChzzkChatBuilder.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 4 | 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | 8 | public class ChzzkChatBuilder { 9 | 10 | private String channelId; 11 | private ChzzkClient chzzk; 12 | private boolean autoReconnect = true; 13 | 14 | public ChzzkChatBuilder(ChzzkClient chzzk, String channelId) { 15 | this.chzzk = chzzk; 16 | this.channelId = channelId; 17 | } 18 | 19 | public ChzzkChatBuilder withAutoReconnect(boolean autoReconnect) { 20 | this.autoReconnect = autoReconnect; 21 | 22 | return this; 23 | } 24 | 25 | public ChzzkChat build() throws IOException { 26 | return new ChzzkChat(chzzk, channelId, autoReconnect); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/MissionDonationMessage.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | public class MissionDonationMessage extends DonationMessage { 4 | public int getDurationTime() { 5 | return extras.durationTime; 6 | } 7 | 8 | public String getMissionDonationId() { 9 | return extras.missionDonationId; 10 | } 11 | 12 | public String getMissionCreatedTime() { 13 | return extras.missionCreatedTime; 14 | } 15 | 16 | public String getMissionEndTime() { 17 | return extras.missionEndTime; 18 | } 19 | 20 | public String getMissionText() { 21 | return extras.missionText; 22 | } 23 | 24 | public String getMissionStatus() { 25 | return extras.status; 26 | } 27 | 28 | public boolean isMissionSucceed() { 29 | return extras.success; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/Installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | `chzzk4j`와 그 하위 의존성을 관리하기 위해 의존성 관리 도구를 사용하는 것을 추천합니다. 3 | 4 | ![](https://img.shields.io/maven-central/v/io.github.R2turnTrue/chzzk4j) 5 | 6 | {% tabs %} 7 | {% tab title="Gradle" %} 8 | ```groovy 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | implementation 'io.github.R2turnTrue:chzzk4j:<라이브러리 버전>' 15 | } 16 | ``` 17 | {% endtab %} 18 | {% tab title="Gradle - Kotlin DSL" %} 19 | ```kotlin 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | implementation("io.github.R2turnTrue:chzzk4j:<라이브러리 버전>") 26 | } 27 | ``` 28 | {% endtab %} 29 | {% tab title="Maven" %} 30 | ```xml 31 | 32 | io.github.R2turnTrue 33 | chzzk4j 34 | 라이브러리 버전 35 | 36 | ``` 37 | {% endtab %} 38 | {% endtabs %} -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/WsMessageServerboundSendChat.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | import java.sql.Timestamp; 4 | import java.time.LocalDate; 5 | import java.util.Date; 6 | 7 | class WsMessageServerboundSendChat extends WsMessageBase { 8 | static class Body { 9 | static class Extras { 10 | String chatType = "STREAMING"; 11 | String osType = "PC"; 12 | String streamingChannelId = ""; 13 | String emojis = ""; 14 | } 15 | 16 | String extras; 17 | String msg; 18 | long msgTime = System.currentTimeMillis(); 19 | int msgTypeCode = WsMessageTypes.ChatTypes.TEXT; 20 | } 21 | 22 | public WsMessageServerboundSendChat() { 23 | super(WsMessageTypes.Commands.SEND_CHAT); 24 | } 25 | 26 | Body bdy = new Body(); 27 | 28 | int tid = 3; 29 | String sid; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/event/ConnectionClosedEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat.event; 2 | 3 | public class ConnectionClosedEvent extends ChzzkEvent { 4 | private int code; 5 | private String reason; 6 | private boolean remote; 7 | private boolean shouldReconnect; 8 | 9 | public ConnectionClosedEvent(int code, String reason, boolean remote, boolean shouldReconnect) { 10 | this.code = code; 11 | this.reason = reason; 12 | this.remote = remote; 13 | this.shouldReconnect = shouldReconnect; 14 | } 15 | 16 | public int getCode() { 17 | return code; 18 | } 19 | 20 | public String getReason() { 21 | return reason; 22 | } 23 | 24 | public boolean isRemote() { 25 | return remote; 26 | } 27 | 28 | public boolean isShouldReconnect() { 29 | return shouldReconnect; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/ChzzkFollowingStatusResponse.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types; 2 | 3 | import xyz.r2turntrue.chzzk4j.types.channel.ChzzkPartialChannel; 4 | 5 | import java.util.Objects; 6 | 7 | public class ChzzkFollowingStatusResponse { 8 | public ChzzkPartialChannel channel; 9 | 10 | private ChzzkFollowingStatusResponse() {} 11 | 12 | @Override 13 | public String toString() { 14 | return "ChzzkFollowingStatusResponse{" + 15 | "channel=" + channel + 16 | '}'; 17 | } 18 | 19 | @Override 20 | public boolean equals(Object o) { 21 | if (this == o) return true; 22 | if (o == null || getClass() != o.getClass()) return false; 23 | ChzzkFollowingStatusResponse that = (ChzzkFollowingStatusResponse) o; 24 | return Objects.equals(channel, that.channel); 25 | } 26 | 27 | @Override 28 | public int hashCode() { 29 | return Objects.hashCode(channel); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/ChzzkSessionSubscriptionType.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session; 2 | 3 | public enum ChzzkSessionSubscriptionType { 4 | CHAT("/open/v1/sessions/events/subscribe/chat", 5 | "/open/v1/sessions/events/unsubscribe/chat"), 6 | DONATION("/open/v1/sessions/events/subscribe/donation", 7 | "/open/v1/sessions/events/unsubscribe/donation"), 8 | CHANNEL_SUBSCRIBE("/open/v1/sessions/events/subscribe/subscription", 9 | "/open/v1/sessions/events/unsubscribe/subscription"); 10 | 11 | private ChzzkSessionSubscriptionType(final String subscribeUrl, final String unsubscribeUrl) { 12 | this.subscribeUrl = subscribeUrl; 13 | this.unsubscribeUrl = unsubscribeUrl; 14 | } 15 | 16 | private final String subscribeUrl; 17 | private final String unsubscribeUrl; 18 | 19 | public String getSubscribeUrl() { 20 | return subscribeUrl; 21 | } 22 | 23 | public String getUnsubscribeUrl() { 24 | return unsubscribeUrl; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/ChzzkSessionBuilder.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session; 2 | 3 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 4 | 5 | import java.io.IOException; 6 | 7 | public class ChzzkSessionBuilder { 8 | 9 | private ChzzkClient chzzk; 10 | private boolean autoRecreate = true; 11 | 12 | public ChzzkSessionBuilder(ChzzkClient chzzk) { 13 | this.chzzk = chzzk; 14 | } 15 | 16 | public ChzzkSessionBuilder withAutoRecreate(boolean autoRecreate) { 17 | this.autoRecreate = autoRecreate; 18 | return this; 19 | } 20 | 21 | public ChzzkClientSession buildClientSession() throws IOException { 22 | return new ChzzkClientSession(chzzk, autoRecreate); 23 | } 24 | 25 | public ChzzkUserSession buildUserSession() throws IOException { 26 | if (!chzzk.isLoggedIn()) { 27 | throw new IllegalStateException("Should be logged in to build an user session!"); 28 | } 29 | 30 | return new ChzzkUserSession(chzzk, autoRecreate); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/recommendation/ChzzkRecommendationChannel.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel.recommendation; 2 | 3 | import xyz.r2turntrue.chzzk4j.types.channel.ChzzkPartialChannel; 4 | 5 | import java.util.Objects; 6 | 7 | public class ChzzkRecommendationChannel { 8 | private ChzzkPartialChannel channel; 9 | 10 | public ChzzkPartialChannel getChannel() { 11 | return channel; 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return "ChzzkRecommendationChannel{" + 17 | "channel=" + channel + 18 | '}'; 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) return true; 24 | if (o == null || getClass() != o.getClass()) return false; 25 | ChzzkRecommendationChannel that = (ChzzkRecommendationChannel) o; 26 | return Objects.equals(channel, that.channel); 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hashCode(channel); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024-2024 R2turnTrue 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/recommendation/ChzzkRecommendationChannels.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel.recommendation; 2 | 3 | import java.util.Arrays; 4 | import java.util.Objects; 5 | 6 | public class ChzzkRecommendationChannels { 7 | private ChzzkRecommendationChannel[] recommendationChannels; 8 | 9 | public ChzzkRecommendationChannel[] getChannels() { 10 | return recommendationChannels; 11 | } 12 | 13 | @Override 14 | public String toString() { 15 | return "ChzzkRecommendationChannels{" + 16 | "recommendationChannels=" + Arrays.toString(recommendationChannels) + 17 | '}'; 18 | } 19 | 20 | @Override 21 | public boolean equals(Object o) { 22 | if (this == o) return true; 23 | if (o == null || getClass() != o.getClass()) return false; 24 | ChzzkRecommendationChannels that = (ChzzkRecommendationChannels) o; 25 | return Objects.deepEquals(recommendationChannels, that.recommendationChannels); 26 | } 27 | 28 | @Override 29 | public int hashCode() { 30 | return Arrays.hashCode(recommendationChannels); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/Login.md: -------------------------------------------------------------------------------- 1 | # Login 2 | 이 페이지에서는 `ChzzkClient`를 로그인하는 방법에 대해 설명합니다. 3 | 4 | {% hint style="warning" %} 5 | 현재 chzzk4j는 치지직의 비공개 API에서 OpenAPI로 마이그레이션 중에 있습니다. 6 | 추후 이 인증 방식은 삭제되거나 deprecated될 수도 있으니, 되도록이면 사용하지 않거나 [두 인증 방식을 동시에](LoginBoth.md) 사용해주세요. 7 | {% endhint %} 8 | 9 | ## `NID_AUT` / `NID_SES` 쿠키를 이용한 방법 10 | 네이버의 인증 쿠키를 이용해 인증하는 방법입니다. 11 | ```java 12 | ChzzkLegacyLoginAdapter adapter = new ChzzkLegacyLoginAdapter("NID_AUT", "NID_SES"); 13 | 14 | ChzzkClient client = new ChzzkClientBuilder("API_CLIENT_ID", "API_SECRET") 15 | .withLoginAdapter(adapter) 16 | .build(); 17 | 18 | client.loginAsync().join(); 19 | ``` 20 | 21 | ## WebDriver를 이용하는 방법 22 | WebDriver를 이용해 `NID_AUT`와 `NID_SES` 쿠키를 자동으로 받을 수 있습니다. 23 | 사용하기 위해서는 먼저 ChromeDriver를 설치해야 합니다. 24 | ```java 25 | // ChromeDriver가 설치된 위치 (driver/chromedriver.exe) 26 | // 환경변수 PATH에 포함된 디렉터리에 설치되어 있다면 필요하지 않습니다. 27 | System.setProperty("webdriver.chrome.driver", "driver/chromedriver.exe"); 28 | 29 | var adapter = new NaverAutologinAdapter("네이버 ID", "네이버 비밀번호"); 30 | 31 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 32 | .withDebugMode() 33 | .withLoginAdapter(adapter) 34 | .build(); 35 | 36 | client.loginAsync().join(); 37 | ``` -------------------------------------------------------------------------------- /docs/SessionConnect.md: -------------------------------------------------------------------------------- 1 | # SessionConnect 2 | 이 페이지에서는 OpenAPI를 이용해 `ChzzkSession` 을 생성하고 연결하는 방법에 대해 설명합니다. 3 | 4 | {% hint style="info" %} 5 | 해당 API는 [API 키](GettingStarted.md), 그리고 [OpenAPI를 통한 인증](LoginOauth.md)를 필요로 합니다! 6 | {% endhint %} 7 | 8 | ## 세션 생성 / 연결하기 9 | ```java 10 | // 먼저 OpenAPI 인증을 완료한 클라이언트를 준비합니다. 11 | ChzzkOauthLoginAdapter adapter = new ChzzkOauthLoginAdapter("localhost", 포트); 12 | 13 | ChzzkClient client = new ChzzkClientBuilder("API_CLIENT_ID", "API_SECRET") 14 | .withDebugMode() 15 | .withLoginAdapter(adapter) 16 | .build(); 17 | 18 | client.loginAsync().join(); 19 | 20 | // ... 21 | 22 | ChzzkUserSession session = new ChzzkSessionBuilder(client) 23 | // chzzk4j는 서버측에서 세션의 연결을 끊었을 때 자동으로 세션을 다시 생성합니다. 24 | // 기본적으로는 이 기능이 활성화되어 있으나, 만약 비활성화하길 원한다면 Builder에서 withAutoRecreate를 false로 설정할 수 있습니다. 25 | //.withAutoRecreate(false) 26 | .buildUserSession(); 27 | // 원한다면 Client Session을 이용할 수도 있습니다만.. 28 | // 현재로서는 되도록이면 User Session을 활용해야 합니다. 29 | //.buildClientSession(); 30 | 31 | session.on(SessionConnectedEvent.class, (event) -> { 32 | System.out.println("Connected!"); 33 | }); 34 | 35 | session.createAndConnectAsync().join(); 36 | ``` -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/WsMessageTypes.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | class WsMessageTypes { 4 | static class Commands { 5 | public static final int PING = 0; 6 | public static final int PONG = 10000; 7 | public static final int CONNECT = 100; 8 | public static final int CONNECTED = 10100; 9 | public static final int REQUEST_RECENT_CHAT = 5101; 10 | public static final int RECENT_CHAT = 15101; 11 | public static final int EVENT = 93006; 12 | public static final int CHAT = 93101; 13 | public static final int DONATION = 93102; 14 | public static final int KICK = 94005; 15 | public static final int BLOCK = 94006; 16 | public static final int BLIND = 94008; 17 | public static final int NOTICE = 94010; 18 | public static final int PENALTY = 94015; 19 | public static final int SEND_CHAT = 3101; 20 | } 21 | 22 | static class ChatTypes { 23 | public static final int TEXT = 1, 24 | IMAGE = 2, // ? 25 | STICKER = 3, // ? 26 | VIDEO = 4, // ? 27 | RICH = 5, // ? 28 | DONATION = 10, 29 | SUBSCRIPTION = 11, 30 | SYSTEM_MESSAGE = 30; // ? 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/SessionEvent.md: -------------------------------------------------------------------------------- 1 | # SessionEvent 2 | 이 페이지에서는 OpenAPI를 이용해 `ChzzkSession` 에 이벤트를 구독하는 방법에 대해 설명합니다. 3 | 4 | {% hint style="info" %} 5 | 해당 API는 [API 키](GettingStarted.md), 그리고 [OpenAPI를 통한 인증](LoginOauth.md)를 필요로 합니다! 6 | {% endhint %} 7 | 8 | ## 이벤트 구독하기 9 | ```java 10 | ChzzkUserSession session = // 세션 생성 코드 11 | 12 | // 이벤트를 구독하기 전까지도 이벤트 리스너는 등록할 수 있습니다. 13 | session.on(SessionChatMessageEvent.class, (event) -> { 14 | var msg = event.getMessage(); 15 | System.out.printf("[CHAT] %s: %s [at %s]%n", msg.getProfile().getNickname(), msg.getContent(), msg.getMessageTime()); 16 | }); 17 | 18 | session.on(SessionDonationEvent.class, (event) -> { 19 | var msg = event.getMessage(); 20 | System.out.printf("[DONATION] %s: %s [%s]%n", msg.getDonatorNickname(), msg.getDonationText(), msg.getDonationType()); 21 | }); 22 | 23 | // 이벤트에 구독하는 시점은 크게 상관이 있지는 않습니다. 24 | session.subscribeAsync(ChzzkSessionSubscriptionType.CHAT).join(); 25 | 26 | // 다만 세션이 연결되는 도중에 이벤트가 구독되는 경우에는 문제가 발생할 수도 있으니, 이 점에 대해서는 주의 바랍니다. 27 | session.createAndConnectAsync().join(); 28 | 29 | // 세션이 연결된 이후에 이벤트에 구독해도 됩니다. 30 | session.subscribeAsync(ChzzkSessionSubscriptionType.DONATION).join(); 31 | ``` 32 | 33 | ## Q. 연결이 끊어지고 다시 세션이 연결되는 과정에서 이벤트를 또 구독해줘야 하나요? 34 | 35 | 아니요, 끊어졌던 세션이 다시 생성되었을 때 chzzk4j가 다시 자동으로 이벤트를 구독합니다. -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/auth/ChzzkLegacyLoginAdapter.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.auth; 2 | 3 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 4 | 5 | import java.util.Objects; 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | public class ChzzkLegacyLoginAdapter implements ChzzkLoginAdapter { 9 | 10 | public String nidSes; 11 | public String nidAut; 12 | 13 | public ChzzkLegacyLoginAdapter(String nidAut, String nidSes) { 14 | this.nidSes = nidSes; 15 | this.nidAut = nidAut; 16 | } 17 | 18 | @Override 19 | public CompletableFuture authorize(ChzzkClient client) { 20 | return CompletableFuture.supplyAsync(() -> new ChzzkLoginResult( 21 | nidAut, 22 | nidSes, 23 | null, 24 | null, 25 | -1 26 | )); 27 | } 28 | 29 | @Override 30 | public boolean equals(Object o) { 31 | if (this == o) return true; 32 | if (o == null || getClass() != o.getClass()) return false; 33 | ChzzkLegacyLoginAdapter that = (ChzzkLegacyLoginAdapter) o; 34 | return Objects.equals(nidSes, that.nidSes) && Objects.equals(nidAut, that.nidAut); 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return Objects.hash(nidSes, nidAut); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 이 페이지에서는 [설치](Installation.md) 이후 `chzzk4j`를 시작하는 내용에 대해 다룹니다. 3 | 4 | ## API 키 발급 5 | {% hint style="warning" %} 6 | 현재 chzzk4j는 치지직의 비공개 API에서 OpenAPI로 마이그레이션 중에 있습니다. 7 | 되도록이면 공식 API 키를 이용해주세요. 8 | {% endhint %} 9 | 10 | {% hint style="info" %} 11 | 공식 API 키 없이도 `chzzk4j`를 사용할 수 있습니다만, 몇몇 기능에 제한이 생길 수도 있습니다. 12 | {% endhint %} 13 | 14 | `chzzk4j`의 모든 기능을 활용하기 위해서는 치지직의 공식 API 키를 발급받아야 합니다. 15 | 1. [치지직 Developers](https://developers.chzzk.naver.com) 페이지에 로그인하고 내 서비스 버튼을 클릭합니다.
16 | ![](../image/doc_install/1.png) 17 | 2. 애플리케이션 목록에서 '애플리케이션 등록'을 클릭해서 관련 정보를 입력하고, 애플리케이션을 생성합니다. 18 | - 인증이 필요하지 않다면 로그인 리디렉션 URL은 `http://localhost:8080`으로 설정합니다. 19 | - 애플리케이션에 필요한 API Scope를 선택합니다. 20 | 3. 클라이언트 ID와 Secret을 복사합니다.
21 | ![](../image/doc_install/2.png) 22 | 23 | ## ChzzkClient 생성 24 | `ChzzkClientBuilder`를 통해 `ChzzkClient`를 생성합니다. 25 | 26 | {% tabs %} 27 | {% tab title="With API Key" %} 28 | ```java 29 | ChzzkClient client = new ChzzkClientBuilder("API_CLIENT_ID", "API_SECRET") 30 | // .withDebugMode() // 디버그 로그를 확인하고 싶다면 활성화할 수 있습니다. 31 | .build(); 32 | ``` 33 | {% endtab %} 34 | {% tab title="Without API Key" %} 35 | ```java 36 | ChzzkClient client = new ChzzkClientBuilder() 37 | // .withDebugMode() // 디버그 로그를 확인하고 싶다면 활성화할 수 있습니다. 38 | .build(); 39 | ``` 40 | {% endtab %} 41 | {% endtabs %} -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/emoticon/ChzzkChannelEmoticonData.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel.emoticon; 2 | 3 | import java.util.Objects; 4 | 5 | public class ChzzkChannelEmoticonData { 6 | private String emojiId; 7 | private String imageUrl; 8 | 9 | private ChzzkChannelEmoticonData() {} 10 | 11 | /** 12 | * Get the emoticon's id. 13 | */ 14 | public String getEmoticonId() { 15 | return emojiId; 16 | } 17 | 18 | /** 19 | * Get url of the emoticon's image. 20 | */ 21 | public String getEmoticonImageUrl() { 22 | return imageUrl; 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return "ChzzkChannelEmoticonData{" + 28 | "emojiId='" + emojiId + '\'' + 29 | ", imageUrl='" + imageUrl + '\'' + 30 | '}'; 31 | } 32 | 33 | @Override 34 | public boolean equals(Object o) { 35 | if (this == o) return true; 36 | if (o == null || getClass() != o.getClass()) return false; 37 | ChzzkChannelEmoticonData that = (ChzzkChannelEmoticonData) o; 38 | return Objects.equals(emojiId, that.emojiId) && Objects.equals(imageUrl, that.imageUrl); 39 | } 40 | 41 | @Override 42 | public int hashCode() { 43 | return Objects.hash(emojiId, imageUrl); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/WsMessageClientboundChat.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import java.lang.reflect.InvocationTargetException; 6 | import java.util.Date; 7 | 8 | class WsMessageClientboundChat extends WsMessageBase { 9 | public WsMessageClientboundChat() { 10 | super(WsMessageTypes.Commands.CHAT); 11 | } 12 | 13 | static class Chat { 14 | public String uid; 15 | public String msg; 16 | public int msgTypeCode; 17 | public long ctime; 18 | public String extras; 19 | public String profile; 20 | public String msgStatusType; 21 | 22 | public T toChatMessage(Class clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException { 23 | var msg = (T) clazz.getConstructors()[0].newInstance(); 24 | 25 | msg.rawJson = new Gson().toJson(this); 26 | 27 | msg.content = this.msg; 28 | msg.msgTypeCode = msgTypeCode; 29 | msg.createTime = new Date(ctime); 30 | msg.extras = new Gson().fromJson(extras, ChatMessage.Extras.class); 31 | msg.profile = new Gson().fromJson(profile, ChatMessage.Profile.class); 32 | msg.userId = uid; 33 | return msg; 34 | } 35 | } 36 | 37 | public Chat[] bdy; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/ChzzkChannelPersonalData.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel; 2 | 3 | import java.util.Objects; 4 | 5 | public class ChzzkChannelPersonalData { 6 | private ChzzkChannelFollowingData following; 7 | private boolean privateUserBlock; 8 | 9 | private ChzzkChannelPersonalData() {} 10 | 11 | /** 12 | * Get following status of the logged user about the channel. 13 | */ 14 | public ChzzkChannelFollowingData getFollowing() { 15 | return following; 16 | } 17 | 18 | public boolean isPrivateUserBlock() { 19 | return privateUserBlock; 20 | } 21 | 22 | @Override 23 | public boolean equals(Object o) { 24 | if (this == o) return true; 25 | if (o == null || getClass() != o.getClass()) return false; 26 | ChzzkChannelPersonalData that = (ChzzkChannelPersonalData) o; 27 | return privateUserBlock == that.privateUserBlock && Objects.equals(following, that.following); 28 | } 29 | 30 | @Override 31 | public int hashCode() { 32 | return Objects.hash(following, privateUserBlock); 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "ChzzkChannelPersonalData{" + 38 | "following=" + following + 39 | ", privateUserBlock=" + privateUserBlock + 40 | '}'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/ChzzkChannelFollower.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel; 2 | 3 | import java.util.Objects; 4 | 5 | public class ChzzkChannelFollower { 6 | private String channelId; 7 | private String channelName; 8 | private String createdDate; 9 | 10 | public String getChannelId() { 11 | return channelId; 12 | } 13 | 14 | public String getChannelName() { 15 | return channelName; 16 | } 17 | 18 | public String getCreatedDate() { 19 | return createdDate; 20 | } 21 | 22 | @Override 23 | public boolean equals(Object o) { 24 | if (this == o) return true; 25 | if (o == null || getClass() != o.getClass()) return false; 26 | ChzzkChannelFollower that = (ChzzkChannelFollower) o; 27 | return Objects.equals(channelId, that.channelId) && Objects.equals(channelName, that.channelName) && Objects.equals(createdDate, that.createdDate); 28 | } 29 | 30 | @Override 31 | public int hashCode() { 32 | return Objects.hash(channelId, channelName, createdDate); 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "ChzzkChannelFollower{" + 38 | "channelId='" + channelId + '\'' + 39 | ", channelName='" + channelName + '\'' + 40 | ", createdDate='" + createdDate + '\'' + 41 | '}'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/util/Chrome.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.util; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.openqa.selenium.JavascriptExecutor; 5 | import org.openqa.selenium.WebDriver; 6 | import org.openqa.selenium.chrome.ChromeDriver; 7 | import org.openqa.selenium.chrome.ChromeOptions; 8 | 9 | import java.util.concurrent.TimeUnit; 10 | 11 | public class Chrome { 12 | 13 | /** 14 | * Explicitly sets the properties for the chrome driver. 15 | */ 16 | public static void setDriverProperty(@NotNull String path) { 17 | System.setProperty("webdriver.chrome.driver", path); 18 | } 19 | 20 | /** 21 | * Get chrome web driver. 22 | */ 23 | public static WebDriver getDriver() { 24 | try { 25 | ChromeOptions options = new ChromeOptions(); 26 | options.addArguments("--headless"); 27 | options.addArguments("--disable-gpu"); 28 | options.addArguments("--window-size=1920,1080"); 29 | WebDriver driver = new ChromeDriver(options); 30 | driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); 31 | return driver; 32 | } catch (Exception e) { 33 | throw new RuntimeException(e); 34 | } 35 | } 36 | 37 | public static JavascriptExecutor getDriverAsJavascriptExecutor() { 38 | return (JavascriptExecutor) getDriver(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/auth/ChzzkSimpleUserLoginAdapter.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.auth; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 5 | 6 | import java.util.Objects; 7 | import java.util.concurrent.CompletableFuture; 8 | 9 | public class ChzzkSimpleUserLoginAdapter implements ChzzkLoginAdapter { 10 | 11 | public String accessToken; 12 | public String refreshToken; 13 | 14 | public ChzzkSimpleUserLoginAdapter(String accessToken, @Nullable String refreshToken) { 15 | this.accessToken = accessToken; 16 | this.refreshToken = refreshToken; 17 | } 18 | 19 | @Override 20 | public CompletableFuture authorize(ChzzkClient client) { 21 | return CompletableFuture.supplyAsync(() -> new ChzzkLoginResult( 22 | null, 23 | null, 24 | accessToken, 25 | refreshToken, 26 | -1 27 | )); 28 | } 29 | 30 | @Override 31 | public boolean equals(Object o) { 32 | if (this == o) return true; 33 | if (o == null || getClass() != o.getClass()) return false; 34 | ChzzkSimpleUserLoginAdapter that = (ChzzkSimpleUserLoginAdapter) o; 35 | return Objects.equals(accessToken, that.accessToken) && Objects.equals(refreshToken, that.refreshToken); 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | return Objects.hash(accessToken, refreshToken); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/BothLoginTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Assertions; 2 | import org.junit.jupiter.api.Test; 3 | import xyz.r2turntrue.chzzk4j.ChzzkClientBuilder; 4 | import xyz.r2turntrue.chzzk4j.auth.ChzzkOauthLoginAdapter; 5 | import xyz.r2turntrue.chzzk4j.naver.NaverAutologinAdapter; 6 | 7 | import java.util.Arrays; 8 | 9 | public class BothLoginTest extends ChzzkTestBase { 10 | 11 | String naverId; 12 | String naverPw; 13 | 14 | public BothLoginTest() { 15 | naverId = properties.getProperty("NAVER_ID"); 16 | naverPw = properties.getProperty("NAVER_PW"); 17 | } 18 | 19 | @Test 20 | public void testBothLogin() { 21 | Assertions.assertDoesNotThrow(() -> { 22 | var oauthAdapter = new ChzzkOauthLoginAdapter(5000); 23 | var naverAdapter = new NaverAutologinAdapter(naverId, naverPw); 24 | 25 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 26 | .withDebugMode() 27 | .withLoginAdapter(oauthAdapter) 28 | .withLoginAdapter(naverAdapter) 29 | .build(); 30 | 31 | System.out.println(oauthAdapter.getAccountInterlockUrl(apiClientId, false)); 32 | 33 | client.loginAsync().join(); 34 | 35 | client.refreshTokenAsync().join(); 36 | 37 | System.out.println(client.fetchLoggedUser()); 38 | System.out.println(Arrays.toString(client.fetchFollowingChannels().get())); 39 | }); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/ChzzkClientBuilder.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j; 2 | 3 | import xyz.r2turntrue.chzzk4j.auth.ChzzkLoginAdapter; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * Class for creating instances of {@link ChzzkClient}. 10 | */ 11 | public class ChzzkClientBuilder { 12 | boolean isAnonymous = false; 13 | boolean isDebug = false; 14 | String nidAuth; 15 | String nidSession; 16 | 17 | String apiClientId = null; 18 | String apiSecret = null; 19 | 20 | List loginAdapters = new ArrayList<>(); 21 | 22 | /** 23 | * Creates a new {@link ChzzkClientBuilder} with API Key. 24 | */ 25 | public ChzzkClientBuilder(String apiClientId, String apiSecret) { 26 | this.isAnonymous = true; 27 | this.apiClientId = apiClientId; 28 | this.apiSecret = apiSecret; 29 | } 30 | 31 | /** 32 | * Creates a new {@link ChzzkClientBuilder} without API Key. 33 | */ 34 | public ChzzkClientBuilder() { 35 | this.isAnonymous = true; 36 | } 37 | 38 | public ChzzkClientBuilder withDebugMode() { 39 | isDebug = true; 40 | 41 | return this; 42 | } 43 | 44 | public ChzzkClientBuilder withLoginAdapter(ChzzkLoginAdapter adapter) { 45 | isAnonymous = false; 46 | loginAdapters.add(adapter); 47 | 48 | return this; 49 | } 50 | 51 | public ChzzkClient build() { 52 | ChzzkClient chzzk = new ChzzkClient(this); 53 | chzzk.isDebug = this.isDebug; 54 | return chzzk; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/WsMessageClientboundRecentChat.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import java.lang.reflect.InvocationTargetException; 6 | import java.util.Date; 7 | 8 | class WsMessageClientboundRecentChat extends WsMessageBase { 9 | static class Body { 10 | static class RecentChat { 11 | public String userId; 12 | public String content; 13 | public int messageTypeCode; 14 | public long createTime; 15 | public String extras; 16 | public String profile; 17 | 18 | public ChatMessage toChatMessage(Class clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException { 19 | var msg = (ChatMessage) clazz.getConstructors()[0].newInstance(); 20 | msg.rawJson = new Gson().toJson(this); 21 | msg.content = content; 22 | msg.msgTypeCode = messageTypeCode; 23 | msg.createTime = new Date(createTime); 24 | msg.extras = new Gson().fromJson(extras, ChatMessage.Extras.class); 25 | msg.profile = new Gson().fromJson(profile, ChatMessage.Profile.class); 26 | msg.userId = userId; 27 | return msg; 28 | } 29 | } 30 | 31 | public RecentChat[] messageList; 32 | public int userCount; 33 | } 34 | 35 | public WsMessageClientboundRecentChat() { 36 | super(WsMessageTypes.Commands.RECENT_CHAT); 37 | } 38 | 39 | public Body bdy; 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/ChzzkChannelFollowerResponse.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel; 2 | 3 | import java.util.Arrays; 4 | import java.util.Objects; 5 | 6 | public class ChzzkChannelFollowerResponse { 7 | private int page; 8 | private int totalCount; 9 | private int totalPages; 10 | private ChzzkChannelFollower[] data; 11 | 12 | public int getPage() { 13 | return page; 14 | } 15 | 16 | public int getTotalCount() { 17 | return totalCount; 18 | } 19 | 20 | public int getTotalPages() { 21 | return totalPages; 22 | } 23 | 24 | public ChzzkChannelFollower[] getFollowers() { 25 | return data; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | return "ChzzkChannelFollowerResponse{" + 31 | "page=" + page + 32 | ", totalCount=" + totalCount + 33 | ", totalPages=" + totalPages + 34 | ", data=" + Arrays.toString(data) + 35 | '}'; 36 | } 37 | 38 | @Override 39 | public boolean equals(Object o) { 40 | if (this == o) return true; 41 | if (o == null || getClass() != o.getClass()) return false; 42 | ChzzkChannelFollowerResponse that = (ChzzkChannelFollowerResponse) o; 43 | return page == that.page && totalCount == that.totalCount && totalPages == that.totalPages && Objects.deepEquals(data, that.data); 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | return Objects.hash(page, totalCount, totalPages, Arrays.hashCode(data)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/ChzzkChannelSubscriberResponse.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel; 2 | 3 | import java.util.Arrays; 4 | import java.util.Objects; 5 | 6 | public class ChzzkChannelSubscriberResponse { 7 | private int page; 8 | private int totalCount; 9 | private int totalPages; 10 | private ChzzkChannelSubscriber[] data; 11 | 12 | public int getPage() { 13 | return page; 14 | } 15 | 16 | public int getTotalCount() { 17 | return totalCount; 18 | } 19 | 20 | public int getTotalPages() { 21 | return totalPages; 22 | } 23 | 24 | public ChzzkChannelSubscriber[] getFollowers() { 25 | return data; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | return "ChzzkChannelSubscriberResponse{" + 31 | "page=" + page + 32 | ", totalCount=" + totalCount + 33 | ", totalPages=" + totalPages + 34 | ", data=" + Arrays.toString(data) + 35 | '}'; 36 | } 37 | 38 | @Override 39 | public boolean equals(Object o) { 40 | if (this == o) return true; 41 | if (o == null || getClass() != o.getClass()) return false; 42 | ChzzkChannelSubscriberResponse that = (ChzzkChannelSubscriberResponse) o; 43 | return page == that.page && totalCount == that.totalCount && totalPages == that.totalPages && Objects.deepEquals(data, that.data); 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | return Objects.hash(page, totalCount, totalPages, Arrays.hashCode(data)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/ChzzkChannelFollowingData.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel; 2 | 3 | import java.util.Objects; 4 | 5 | public class ChzzkChannelFollowingData { 6 | private boolean following; 7 | private boolean notification; 8 | private String followDate; 9 | 10 | private ChzzkChannelFollowingData() {} 11 | 12 | /** 13 | * Get is me following the channel. 14 | */ 15 | public boolean isFollowing() { 16 | return following; 17 | } 18 | 19 | /** 20 | * Get is me enabled the channel notification. 21 | */ 22 | public boolean isEnabledNotification() { 23 | return notification; 24 | } 25 | 26 | /** 27 | * Get when me followed the channel in yyyy-mm-dd HH:mm:ss format. 28 | */ 29 | public String getFollowDate() { 30 | return followDate; 31 | } 32 | 33 | @Override 34 | public String toString() { 35 | return "ChzzkChannelFollowingData{" + 36 | "following=" + following + 37 | ", notification=" + notification + 38 | ", followDate='" + followDate + '\'' + 39 | '}'; 40 | } 41 | 42 | @Override 43 | public boolean equals(Object o) { 44 | if (this == o) return true; 45 | if (o == null || getClass() != o.getClass()) return false; 46 | ChzzkChannelFollowingData that = (ChzzkChannelFollowingData) o; 47 | return following == that.following && notification == that.notification && Objects.equals(followDate, that.followDate); 48 | } 49 | 50 | @Override 51 | public int hashCode() { 52 | return Objects.hash(following, notification, followDate); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/LiveApiTest.java: -------------------------------------------------------------------------------- 1 | import org.jetbrains.annotations.NotNull; 2 | import org.junit.jupiter.api.Assertions; 3 | import org.junit.jupiter.api.Test; 4 | import xyz.r2turntrue.chzzk4j.types.channel.live.ChzzkLiveDetail; 5 | import xyz.r2turntrue.chzzk4j.types.channel.live.ChzzkLiveStatus; 6 | 7 | import java.io.IOException; 8 | import java.util.concurrent.atomic.AtomicReference; 9 | 10 | public class LiveApiTest extends ChzzkTestBase { 11 | 12 | private final @NotNull String CHANNEL_TO_TEST = "8a59b34b46271960c1bf172bb0fac758"; 13 | private final @NotNull String INVALID_CHANNEL_TO_TEST = "INVALID"; 14 | 15 | @Test 16 | public void testChzzkLiveStatusGet() { 17 | AtomicReference status = new AtomicReference<>(); 18 | Assertions.assertDoesNotThrow(() -> { 19 | status.set(chzzk.fetchLiveStatus(CHANNEL_TO_TEST).get()); 20 | }); 21 | System.out.println(status); 22 | } 23 | 24 | @Test 25 | public void testInvalidChzzkLiveStatusGet() { 26 | Assertions.assertThrowsExactly(IOException.class, () -> { 27 | chzzk.fetchLiveStatus(INVALID_CHANNEL_TO_TEST); 28 | }); 29 | } 30 | 31 | @Test 32 | public void testChzzkLiveDetailGet() { 33 | AtomicReference detail = new AtomicReference<>(); 34 | Assertions.assertDoesNotThrow(() -> { 35 | detail.set(chzzk.fetchLiveDetail(CHANNEL_TO_TEST).get()); 36 | }); 37 | System.out.println(detail); 38 | } 39 | 40 | @Test 41 | public void testInvalidChzzkLiveDetailGet() { 42 | Assertions.assertThrowsExactly(IOException.class, () -> { 43 | chzzk.fetchLiveDetail(INVALID_CHANNEL_TO_TEST); 44 | }); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/util/HttpUtils.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.util; 2 | 3 | import com.sun.net.httpserver.HttpExchange; 4 | 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | import java.net.URLDecoder; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | public class HttpUtils { 13 | // https://stackoverflow.com/questions/11640025/how-to-obtain-the-query-string-in-a-get-with-java-httpserver-httpexchange 14 | public static Map queryToMap(String query) { 15 | if (query == null) { 16 | return null; 17 | } 18 | Map result = new HashMap<>(); 19 | for (String param : query.split("&")) { 20 | String[] entry = param.split("="); 21 | if (entry.length > 1) { 22 | result.put( 23 | URLDecoder.decode(entry[0], StandardCharsets.UTF_8), 24 | URLDecoder.decode(entry[1], StandardCharsets.UTF_8) 25 | ); 26 | } else { 27 | result.put( 28 | URLDecoder.decode(entry[0], StandardCharsets.UTF_8), 29 | "" 30 | ); 31 | } 32 | } 33 | return result; 34 | } 35 | 36 | // https://velog.io/@jeong_lululala/java-http-server 37 | public static void sendContent(HttpExchange exchange, String content, int rCode) throws IOException, IOException { 38 | byte[] bytes = content.getBytes(); 39 | 40 | exchange.sendResponseHeaders(rCode, bytes.length); 41 | 42 | OutputStream outputStream = exchange.getResponseBody(); 43 | outputStream.write(bytes); 44 | outputStream.flush(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/OauthLoginTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Assertions; 2 | import org.junit.jupiter.api.Test; 3 | import xyz.r2turntrue.chzzk4j.ChzzkClientBuilder; 4 | import xyz.r2turntrue.chzzk4j.auth.ChzzkOauthLoginAdapter; 5 | import xyz.r2turntrue.chzzk4j.naver.NaverAutologinAdapter; 6 | 7 | public class OauthLoginTest extends ChzzkTestBase { 8 | @Test 9 | public void testOauthLogin() { 10 | Assertions.assertDoesNotThrow(() -> { 11 | var adapter = new ChzzkOauthLoginAdapter(5000); 12 | 13 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 14 | .withDebugMode() 15 | .withLoginAdapter(adapter) 16 | .build(); 17 | 18 | System.out.println(adapter.getAccountInterlockUrl(apiClientId, false)); 19 | 20 | client.loginAsync().join(); 21 | 22 | System.out.println(client.fetchLoggedUser()); 23 | }); 24 | } 25 | 26 | @Test 27 | public void testOauthRefresh() { 28 | Assertions.assertDoesNotThrow(() -> { 29 | var adapter = new ChzzkOauthLoginAdapter(5000); 30 | 31 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 32 | .withDebugMode() 33 | .withLoginAdapter(adapter) 34 | .build(); 35 | 36 | System.out.println(adapter.getAccountInterlockUrl(apiClientId, false)); 37 | 38 | client.loginAsync().join(); 39 | 40 | System.out.println(client.fetchLoggedUser()); 41 | System.out.println(client.getLoginResult()); 42 | 43 | client.refreshTokenAsync().join(); 44 | 45 | System.out.println(client.fetchLoggedUser()); 46 | System.out.println(client.getLoginResult()); 47 | }); 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/ChzzkRestrictedChannel.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types; 2 | 3 | import java.util.Objects; 4 | 5 | public class ChzzkRestrictedChannel { 6 | private String restrictedChannelId; 7 | private String restrictedChannelName; 8 | private String createdDate; 9 | private String releaseDate; 10 | 11 | public String getRestrictedChannelId() { 12 | return restrictedChannelId; 13 | } 14 | 15 | public String getRestrictedChannelName() { 16 | return restrictedChannelName; 17 | } 18 | 19 | public String getCreatedDate() { 20 | return createdDate; 21 | } 22 | 23 | public String getReleaseDate() { 24 | return releaseDate; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return "ChzzkRestrictedChannel{" + 30 | "restrictedChannelId='" + restrictedChannelId + '\'' + 31 | ", restrictedChannelName='" + restrictedChannelName + '\'' + 32 | ", createdDate='" + createdDate + '\'' + 33 | ", releaseDate='" + releaseDate + '\'' + 34 | '}'; 35 | } 36 | 37 | @Override 38 | public boolean equals(Object o) { 39 | if (this == o) return true; 40 | if (o == null || getClass() != o.getClass()) return false; 41 | ChzzkRestrictedChannel that = (ChzzkRestrictedChannel) o; 42 | return Objects.equals(restrictedChannelId, that.restrictedChannelId) && Objects.equals(restrictedChannelName, that.restrictedChannelName) && Objects.equals(createdDate, that.createdDate) && Objects.equals(releaseDate, that.releaseDate); 43 | } 44 | 45 | @Override 46 | public int hashCode() { 47 | return Objects.hash(restrictedChannelId, restrictedChannelName, createdDate, releaseDate); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/BroadcastSettings.md: -------------------------------------------------------------------------------- 1 | # BroadcastSettings 2 | 이 페이지에서는 방송 / 채팅 설정을 가져오거나 변경하는 방법에 대해 설명합니다. 3 | 4 | {% hint style="info" %} 5 | 해당 API는 [API 키](GettingStarted.md), 그리고 [OpenAPI를 통한 인증](LoginOauth.md)를 필요로 합니다! 6 | {% endhint %} 7 | 8 | ## 방송 설정 가져오기 9 | ```java 10 | ChzzkLiveSettings live = client.fetchLiveSettings().join(); 11 | System.out.println(live); 12 | ``` 13 | 14 | ## 방송 설정 변경하기 15 | ```java 16 | // 우선 기존 방송 설정을 가져옵니다. 17 | ChzzkLiveSettings live = client.fetchLiveSettings().join(); 18 | // 혹은 새 라이브 설정을 생성합니다. 19 | //ChzzkLiveSettings live = new ChzzkLiveSettings(); 20 | 21 | // 방송 제목 변경하기 22 | live.setDefaultLiveTitle("안녕, 세상!"); 23 | 24 | // 카테고리 변경하기 25 | // 우선 방송 카테고리를 검색한 이후 변경합니다. 26 | ChzzkLiveCategory[] categories = client.searchCategories("마인크래프트", 1); 27 | live.setCategory(categories[0]); 28 | 29 | // 태그 변경하기 30 | live.getTags().clear(); 31 | live.getTags().add("프로그래밍"); 32 | 33 | // 변경된 방송 설정을 서버에 전송합니다. 34 | client.modifyLiveSettings(live).join(); 35 | ``` 36 | 37 | ## 채팅 설정 가져오기 38 | ```java 39 | ChzzkChatSettings settings = client.fetchChatSettings().join(); 40 | System.out.println(settings); 41 | ``` 42 | 43 | ## 채팅 설정 변경하기 44 | ```java 45 | ChzzkChatSettings settings = client.fetchChatSettings().join(); 46 | 47 | // 본인인증 여부 설정 조건을 변경합니다. 48 | settings.setChatAvailableCondition(ChzzkChatSettings.ChatAvailableCondition.REAL_NAME); 49 | 50 | // 채팅 참여 범위 설정 조건을 변경합니다. 51 | settings.setChatAvailableGroup(ChzzkChatSettings.ChatAvailableGroup.FOLLOWER); 52 | // (위 ChatAvailableGroup이 FOLLOWER일 경우) 최소 팔로잉 기간을 설정합니다. 53 | settings.setMinFollowerMinute(ChzzkChatSettings.MinFollowerMinute.M_60); 54 | // (위 ChatAvailableGroup이 FOLLOWER일 경우) 구독자는 최소 팔로잉 기간 조건 대상에서 제외/허용 할지 여부를 설정합니다. 55 | settings.setAllowSubscriberInFollowerMode(false); 56 | 57 | // 변경된 방송 설정을 서버에 전송합니다. 58 | client.modifyChatSettings(settings).join(); 59 | ``` -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/ChzzkChannel.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel; 2 | 3 | import java.util.Objects; 4 | 5 | public class ChzzkChannel extends ChzzkPartialChannel { 6 | private String channelDescription; 7 | private int followerCount; 8 | private boolean openLive; 9 | 10 | private ChzzkChannel() { 11 | super(); 12 | } 13 | 14 | /** 15 | * Get description of the channel. 16 | */ 17 | public String getChannelDescription() { 18 | return channelDescription; 19 | } 20 | 21 | /** 22 | * Get the count of the channel's followers. 23 | */ 24 | public int getFollowerCount() { 25 | return followerCount; 26 | } 27 | 28 | /** 29 | * Get is the channel broadcasting. 30 | */ 31 | public boolean isBroadcasting() { 32 | return openLive; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "ChzzkChannel{" + 38 | "parent=" + super.toString() + 39 | ", channelDescription='" + channelDescription + '\'' + 40 | ", followerCount=" + followerCount + 41 | ", openLive=" + openLive + 42 | '}'; 43 | } 44 | 45 | @Override 46 | public boolean equals(Object o) { 47 | if (this == o) return true; 48 | if (o == null || getClass() != o.getClass()) return false; 49 | if (!super.equals(o)) return false; 50 | ChzzkChannel that = (ChzzkChannel) o; 51 | return followerCount == that.followerCount && openLive == that.openLive && Objects.equals(channelDescription, that.channelDescription); 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return Objects.hash(super.hashCode(), channelDescription, followerCount, openLive); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/ChzzkChannelSubscriber.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel; 2 | 3 | import java.util.Objects; 4 | 5 | public class ChzzkChannelSubscriber { 6 | public enum SortBy { 7 | RECENT, 8 | LONGER 9 | } 10 | 11 | private String channelId; 12 | private String channelName; 13 | private int month; 14 | private int tierNo; 15 | private String createdDate; 16 | 17 | @Override 18 | public String toString() { 19 | return "ChzzkChannelSubscriber{" + 20 | "channelId='" + channelId + '\'' + 21 | ", channelName='" + channelName + '\'' + 22 | ", month=" + month + 23 | ", tierNo=" + tierNo + 24 | ", createdDate='" + createdDate + '\'' + 25 | '}'; 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (this == o) return true; 31 | if (o == null || getClass() != o.getClass()) return false; 32 | ChzzkChannelSubscriber that = (ChzzkChannelSubscriber) o; 33 | return month == that.month && tierNo == that.tierNo && Objects.equals(channelId, that.channelId) && Objects.equals(channelName, that.channelName) && Objects.equals(createdDate, that.createdDate); 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | return Objects.hash(channelId, channelName, month, tierNo, createdDate); 39 | } 40 | 41 | public String getChannelId() { 42 | return channelId; 43 | } 44 | 45 | public String getChannelName() { 46 | return channelName; 47 | } 48 | 49 | public int getMonth() { 50 | return month; 51 | } 52 | 53 | public int getTierNo() { 54 | return tierNo; 55 | } 56 | 57 | public String getCreatedDate() { 58 | return createdDate; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/ChzzkChannelManager.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel; 2 | 3 | import java.util.Objects; 4 | 5 | public class ChzzkChannelManager { 6 | public enum Role { 7 | STREAMING_CHANNEL_OWNER, 8 | STREAMING_CHANNEL_MANAGER, 9 | STREAMING_CHAT_MANAGER, 10 | STREAMING_SETTLEMENT_MANAGER 11 | } 12 | 13 | private String managerChannelId; 14 | private String managerChannelName; 15 | private Role role; 16 | private String createdDate; 17 | 18 | public String getManagerChannelId() { 19 | return managerChannelId; 20 | } 21 | 22 | public String getManagerChannelName() { 23 | return managerChannelName; 24 | } 25 | 26 | public Role getRole() { 27 | return role; 28 | } 29 | 30 | public String getCreatedDate() { 31 | return createdDate; 32 | } 33 | 34 | @Override 35 | public boolean equals(Object o) { 36 | if (this == o) return true; 37 | if (o == null || getClass() != o.getClass()) return false; 38 | ChzzkChannelManager that = (ChzzkChannelManager) o; 39 | return Objects.equals(managerChannelId, that.managerChannelId) && Objects.equals(managerChannelName, that.managerChannelName) && role == that.role && Objects.equals(createdDate, that.createdDate); 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | return Objects.hash(managerChannelId, managerChannelName, role, createdDate); 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return "ChzzkChannelManager{" + 50 | "managerChannelId='" + managerChannelId + '\'' + 51 | ", managerChannelName='" + managerChannelName + '\'' + 52 | ", role=" + role + 53 | ", createdDate='" + createdDate + '\'' + 54 | '}'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/ChzzkRestrictedChannelResponse.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types; 2 | 3 | import java.util.Arrays; 4 | import java.util.Objects; 5 | 6 | public class ChzzkRestrictedChannelResponse { 7 | public class Pagination { 8 | private String next; 9 | 10 | @Override 11 | public boolean equals(Object o) { 12 | if (this == o) return true; 13 | if (o == null || getClass() != o.getClass()) return false; 14 | Pagination that = (Pagination) o; 15 | return Objects.equals(next, that.next); 16 | } 17 | 18 | @Override 19 | public int hashCode() { 20 | return Objects.hashCode(next); 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return "Pagination{" + 26 | "next='" + next + '\'' + 27 | '}'; 28 | } 29 | } 30 | 31 | private ChzzkRestrictedChannel[] data; 32 | private Pagination page; 33 | 34 | public ChzzkRestrictedChannel[] getRestrictedChannels() { 35 | return data; 36 | } 37 | 38 | public String getNextPageToken() { 39 | return page.next; 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return "ChzzkRestrictedChannelResponse{" + 45 | "data=" + Arrays.toString(data) + 46 | ", page=" + page + 47 | '}'; 48 | } 49 | 50 | @Override 51 | public boolean equals(Object o) { 52 | if (this == o) return true; 53 | if (o == null || getClass() != o.getClass()) return false; 54 | ChzzkRestrictedChannelResponse that = (ChzzkRestrictedChannelResponse) o; 55 | return Objects.deepEquals(data, that.data) && Objects.equals(page, that.page); 56 | } 57 | 58 | @Override 59 | public int hashCode() { 60 | return Objects.hash(Arrays.hashCode(data), page); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/ChatTest.java: -------------------------------------------------------------------------------- 1 | import com.google.gson.Gson; 2 | import org.junit.jupiter.api.Test; 3 | import xyz.r2turntrue.chzzk4j.chat.*; 4 | import xyz.r2turntrue.chzzk4j.chat.event.*; 5 | import xyz.r2turntrue.chzzk4j.util.RawApiUtils; 6 | 7 | import java.io.IOException; 8 | import java.util.concurrent.ExecutionException; 9 | 10 | public class ChatTest extends ChzzkTestBase { 11 | @Test 12 | void testingChat() throws IOException, InterruptedException, ExecutionException { 13 | ChzzkChat chat = new ChzzkChatBuilder(chzzk, 14 | "36ddb9bb4f17593b60f1b63cec86611d") 15 | .withAutoReconnect(true) 16 | .build(); 17 | 18 | chat.on(ConnectEvent.class, (evt) -> { 19 | System.out.println("WebSocket connected! :)"); 20 | }); 21 | 22 | chat.on(ChatMessageEvent.class, (evt) -> { 23 | ChatMessage msg = evt.getMessage(); 24 | 25 | System.out.println(msg); 26 | 27 | if (msg.getProfile() == null) { 28 | System.out.println("[Chat] 익명: " + msg.getContent()); 29 | return; 30 | } 31 | 32 | System.out.println("[Chat] " + msg.getProfile().getNickname() + ": " + msg.getContent()); 33 | }); 34 | 35 | 36 | chat.on(NormalDonationEvent.class, (evt) -> { 37 | DonationMessage msg = evt.getMessage(); 38 | 39 | System.out.println(msg); 40 | 41 | if (msg.getProfile() == null) { 42 | System.out.println("[Donation] 익명: " + msg.getContent() + " - " + msg.getPayAmount()); 43 | return; 44 | } 45 | 46 | System.out.println("[Donation] " + msg.getProfile().getNickname() + ": " + msg.getContent() + " - " + msg.getPayAmount()); 47 | }); 48 | 49 | chat.connectBlocking(); 50 | 51 | //chat.requestRecentChat(50); 52 | Thread.sleep(100000000); 53 | chat.closeBlocking(); 54 | } 55 | } -------------------------------------------------------------------------------- /src/test/java/NaverLoginTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Assertions; 2 | import org.junit.jupiter.api.Test; 3 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 4 | import xyz.r2turntrue.chzzk4j.ChzzkClientBuilder; 5 | import xyz.r2turntrue.chzzk4j.exception.NotLoggedInException; 6 | import xyz.r2turntrue.chzzk4j.naver.NaverAutologinAdapter; 7 | import xyz.r2turntrue.chzzk4j.util.Chrome; 8 | 9 | import java.io.IOException; 10 | import java.util.concurrent.CompletionException; 11 | 12 | public class NaverLoginTest extends ChzzkTestBase { 13 | 14 | String naverId; 15 | String naverPw; 16 | 17 | public NaverLoginTest() { 18 | naverId = properties.getProperty("NAVER_ID"); 19 | naverPw = properties.getProperty("NAVER_PW"); 20 | } 21 | 22 | @Test 23 | public void testNaverLogin() { 24 | Assertions.assertDoesNotThrow(() -> { 25 | var adapter = new NaverAutologinAdapter( 26 | naverId, 27 | naverPw 28 | ); 29 | 30 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 31 | .withDebugMode() 32 | .withLoginAdapter(adapter) 33 | .build(); 34 | 35 | client.loginAsync().get(); 36 | 37 | System.out.println(client.fetchLoggedUser()); 38 | }); 39 | } 40 | 41 | @Test 42 | public void testNaverLoginFailed() { 43 | Assertions.assertThrowsExactly(CompletionException.class, () -> { 44 | Chrome.setDriverProperty(""); 45 | var adapter = new NaverAutologinAdapter( 46 | naverId, 47 | naverPw 48 | ); 49 | 50 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 51 | .withDebugMode() 52 | .withLoginAdapter(adapter) 53 | .build(); 54 | 55 | client.loginAsync().join(); 56 | }); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/live/ChzzkLiveChannel.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel.live; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.Objects; 6 | 7 | public class ChzzkLiveChannel { 8 | 9 | private String channelId; 10 | private String channelName; 11 | private String channelImageUrl; 12 | private boolean verifiedMark; 13 | 14 | /** 15 | * Get id of the live channel. 16 | */ 17 | public @NotNull String getId() { 18 | return channelId; 19 | } 20 | 21 | /** 22 | * Get name of the live channel. 23 | */ 24 | public @NotNull String getName() { 25 | return channelName; 26 | } 27 | 28 | /** 29 | * Get image url of the live channel. 30 | */ 31 | public @NotNull String getImageUrl() { 32 | return channelImageUrl; 33 | } 34 | 35 | /** 36 | * Get verified mark status of the live channel. 37 | */ 38 | public boolean hasVerifiedMark() { 39 | return verifiedMark; 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return "ChzzkLiveChannelImpl{" + 45 | "channelId='" + channelId + '\'' + 46 | ", channelName='" + channelName + '\'' + 47 | ", channelImageUrl='" + channelImageUrl + '\'' + 48 | ", verifiedMark=" + verifiedMark + 49 | '}'; 50 | } 51 | 52 | @Override 53 | public boolean equals(Object o) { 54 | if (this == o) return true; 55 | if (o == null || getClass() != o.getClass()) return false; 56 | ChzzkLiveChannel that = (ChzzkLiveChannel) o; 57 | return verifiedMark == that.verifiedMark && Objects.equals(channelId, that.channelId) && Objects.equals(channelName, that.channelName) && Objects.equals(channelImageUrl, that.channelImageUrl); 58 | } 59 | 60 | @Override 61 | public int hashCode() { 62 | return Objects.hash(channelId, channelName, channelImageUrl, verifiedMark); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/message/SessionNewSubscriberMessage.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.message; 2 | 3 | import java.util.Objects; 4 | 5 | public class SessionNewSubscriberMessage { 6 | private String channelId; 7 | private String subscriberChannelId; 8 | private String subscriberNickname; 9 | private int tierNo; 10 | private String tierName; 11 | private int month; 12 | 13 | public String getChannelId() { 14 | return channelId; 15 | } 16 | 17 | public String getSubscriberChannelId() { 18 | return subscriberChannelId; 19 | } 20 | 21 | public String getSubscriberNickname() { 22 | return subscriberNickname; 23 | } 24 | 25 | public int getTierNo() { 26 | return tierNo; 27 | } 28 | 29 | public String getTierName() { 30 | return tierName; 31 | } 32 | 33 | public int getMonth() { 34 | return month; 35 | } 36 | 37 | @Override 38 | public boolean equals(Object o) { 39 | if (this == o) return true; 40 | if (o == null || getClass() != o.getClass()) return false; 41 | SessionNewSubscriberMessage that = (SessionNewSubscriberMessage) o; 42 | return tierNo == that.tierNo && month == that.month && Objects.equals(channelId, that.channelId) && Objects.equals(subscriberChannelId, that.subscriberChannelId) && Objects.equals(subscriberNickname, that.subscriberNickname) && Objects.equals(tierName, that.tierName); 43 | } 44 | 45 | @Override 46 | public int hashCode() { 47 | return Objects.hash(channelId, subscriberChannelId, subscriberNickname, tierNo, tierName, month); 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return "SessionSubscriptionMessage{" + 53 | "channelId='" + channelId + '\'' + 54 | ", subscriberChannelId='" + subscriberChannelId + '\'' + 55 | ", subscriberNickname='" + subscriberNickname + '\'' + 56 | ", tierNo=" + tierNo + 57 | ", tierName='" + tierName + '\'' + 58 | ", month=" + month + 59 | '}'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/ChzzkChannelRules.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel; 2 | 3 | import java.util.Objects; 4 | 5 | public class ChzzkChannelRules { 6 | private boolean agree; 7 | private String channelId; 8 | private String rule; 9 | private String updatedDate; 10 | private boolean serviceAgree; 11 | 12 | private ChzzkChannelRules() {} 13 | 14 | /** 15 | * Get the user is agreed to the rules of channel. 16 | */ 17 | public boolean isAgree() { 18 | return agree; 19 | } 20 | 21 | /** 22 | * Get the id of channel. 23 | */ 24 | public String getChannelId() { 25 | return channelId; 26 | } 27 | 28 | /** 29 | * Get the rule string of channel. 30 | */ 31 | public String getRule() { 32 | return rule; 33 | } 34 | 35 | /** 36 | * Get when the rule updated in yyyy-mm-dd HH:mm:ss format. 37 | */ 38 | public String getUpdatedDate() { 39 | return updatedDate; 40 | } 41 | 42 | /** 43 | * Get the user is agreed to the rules of channel. 44 | */ 45 | public boolean isServiceAgree() { 46 | return serviceAgree; 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "ChzzkChannelRules{" + 52 | "agree=" + agree + 53 | ", channelId='" + channelId + '\'' + 54 | ", rule='" + rule + '\'' + 55 | ", updatedDate='" + updatedDate + '\'' + 56 | ", serviceAgree=" + serviceAgree + 57 | '}'; 58 | } 59 | 60 | @Override 61 | public boolean equals(Object o) { 62 | if (this == o) return true; 63 | if (o == null || getClass() != o.getClass()) return false; 64 | ChzzkChannelRules that = (ChzzkChannelRules) o; 65 | return agree == that.agree && serviceAgree == that.serviceAgree && Objects.equals(channelId, that.channelId) && Objects.equals(rule, that.rule) && Objects.equals(updatedDate, that.updatedDate); 66 | } 67 | 68 | @Override 69 | public int hashCode() { 70 | return Objects.hash(agree, channelId, rule, updatedDate, serviceAgree); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /docs/ChatMessage.md: -------------------------------------------------------------------------------- 1 | # 채팅 이벤트 2 | 이 페이지에서는 채팅의 메시지, 후원을 비롯한 이벤트 핸들링에 대해 설명합니다. 3 | 4 | {% hint style="warning" %} 5 | 현재 chzzk4j는 치지직의 비공개 API에서 OpenAPI로 마이그레이션 중에 있습니다. 6 | 되도록이면 공식 API를 활용한 채팅을 이용해주세요. 7 | {% endhint %} 8 | 9 | ## 일반 메시지 받기 10 | {% hint style="warning" %} 11 | 익명 프로필의 경우 `ChatMessage`의 프로필이 `null`일 수 있습니다! 12 | {% endhint %} 13 | 14 | ```java 15 | chat.on(ChatMessageEvent.class, (evt) -> { 16 | ChatMessage msg = evt.getMessage(); 17 | 18 | if (msg.getProfile() == null) { 19 | System.out.println("[Chat] 익명: " + msg.getContent()); 20 | return; 21 | } 22 | 23 | System.out.println("[Chat] " + msg.getProfile().getNickname() + ": " + msg.getContent()); 24 | }); 25 | ``` 26 | 27 | ## 일반 후원 메시지 받기 28 | ```java 29 | chat.on(NormalDonationEvent.class, (evt) -> { 30 | DonationMessage msg = evt.getMessage(); 31 | 32 | if (msg.getProfile() == null) { 33 | System.out.println("[Donation] 익명: " + msg.getContent() + ": " + msg.getContent() + " [" + msg.getPayAmount() + "원]"); 34 | return; 35 | } 36 | 37 | System.out.println("[Donation] " + msg.getProfile().getNickname() + ": " + msg.getContent() + " [" + msg.getPayAmount() + "원]"); 38 | }); 39 | ``` 40 | 41 | ## 구독 메시지 받기 42 | ```java 43 | chat.on(SubscriptionMessageEvent.class, (evt) -> { 44 | SubscriptionMessage msg = evt.getMessage(); 45 | 46 | if (msg.getProfile() == null) { 47 | System.out.println("[Subscription] 익명: " + msg.getContent() + ": [" + msg.getSubscriptionMonth() + "개월 " + msg.getSubscriptionTierName() + "]"); 48 | return; 49 | } 50 | 51 | System.out.println("[Subscription] " + msg.getProfile().getNickname() + ": [" + msg.getSubscriptionMonth() + "개월 " + msg.getSubscriptionTierName() + "]"); 52 | }); 53 | ``` 54 | 55 | ## 미션 후원 메시지 받기 56 | ```java 57 | chat.on(MissionDonationEvent.class, (evt) -> { 58 | MissionDonationMessage msg = evt.getMessage(); 59 | 60 | if (msg.getProfile() == null) { 61 | System.out.println("[Mission] 익명: " + msg.getMissionText() + ": [" + msg.getPayAmount() + "원]"); 62 | return; 63 | } 64 | 65 | System.out.println("[Mission] 익명: " + msg.getMissionText() + ": [" + msg.getPayAmount() + "원]"); 66 | }); 67 | ``` -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/live/ChzzkLiveCategory.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel.live; 2 | 3 | import java.util.Objects; 4 | 5 | public class ChzzkLiveCategory { 6 | public static class SearchResponse { 7 | private ChzzkLiveCategory[] data; 8 | 9 | public ChzzkLiveCategory[] getData() { 10 | return data; 11 | } 12 | } 13 | 14 | public enum Type { 15 | GAME, 16 | SPORTS, 17 | ETC 18 | } 19 | 20 | private String categoryType; 21 | private String categoryId; 22 | private String categoryValue; 23 | private String posterImageUrl; 24 | 25 | public Type getCategoryType() { 26 | return Type.valueOf(categoryType); 27 | } 28 | 29 | public void setCategoryType(Type categoryType) { 30 | this.categoryType = categoryType.toString(); 31 | } 32 | 33 | public String getCategoryId() { 34 | return categoryId; 35 | } 36 | 37 | public void setCategoryId(String categoryId) { 38 | this.categoryId = categoryId; 39 | } 40 | 41 | public String getCategoryValue() { 42 | return categoryValue; 43 | } 44 | 45 | public String getPosterImageUrl() { 46 | return posterImageUrl; 47 | } 48 | 49 | @Override 50 | public boolean equals(Object o) { 51 | if (this == o) return true; 52 | if (o == null || getClass() != o.getClass()) return false; 53 | ChzzkLiveCategory that = (ChzzkLiveCategory) o; 54 | return categoryType == that.categoryType && Objects.equals(categoryId, that.categoryId) && Objects.equals(categoryValue, that.categoryValue) && Objects.equals(posterImageUrl, that.posterImageUrl); 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | return Objects.hash(categoryType, categoryId, categoryValue, posterImageUrl); 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return "ChzzkLiveCategory{" + 65 | "categoryType=" + categoryType + 66 | ", categoryId='" + categoryId + '\'' + 67 | ", categoryValue='" + categoryValue + '\'' + 68 | ", posterImageUrl='" + posterImageUrl + '\'' + 69 | '}'; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/emoticon/ChzzkChannelEmotePackData.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel.emoticon; 2 | 3 | import java.util.List; 4 | import java.util.Objects; 5 | 6 | public class ChzzkChannelEmotePackData { 7 | private String emojiPackId; 8 | private String emojiPackName; 9 | private String emojiPackImageUrl; 10 | private boolean emojiPackLocked; 11 | private List emojis; 12 | 13 | private ChzzkChannelEmotePackData() {} 14 | 15 | /** 16 | * Get the pack's id. 17 | */ 18 | public String getPackId() { 19 | return emojiPackId; 20 | } 21 | 22 | /** 23 | * Get the name of the pack. 24 | */ 25 | public String getPackName() { 26 | return emojiPackName; 27 | } 28 | 29 | /** 30 | * Get url of the pack's image. 31 | */ 32 | public String getPackImageUrl() { 33 | return emojiPackImageUrl; 34 | } 35 | 36 | /** 37 | * Get the emoticons data of the pack. 38 | */ 39 | 40 | public List getEmojis() { 41 | return emojis; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return "ChzzkChannelEmotePackData{" + 47 | "emojiPackId='" + emojiPackId + '\'' + 48 | ", emojiPackName='" + emojiPackName + '\'' + 49 | ", emojiPackImageUrl='" + emojiPackImageUrl + '\'' + 50 | ", emojiPackLocked=" + emojiPackLocked + 51 | ", emojis=" + emojis + 52 | '}'; 53 | } 54 | 55 | @Override 56 | public boolean equals(Object o) { 57 | if (this == o) return true; 58 | if (o == null || getClass() != o.getClass()) return false; 59 | ChzzkChannelEmotePackData that = (ChzzkChannelEmotePackData) o; 60 | return emojiPackLocked == that.emojiPackLocked && Objects.equals(emojiPackId, that.emojiPackId) && Objects.equals(emojiPackName, that.emojiPackName) && Objects.equals(emojiPackImageUrl, that.emojiPackImageUrl) && Objects.equals(emojis, that.emojis); 61 | } 62 | 63 | @Override 64 | public int hashCode() { 65 | return Objects.hash(emojiPackId, emojiPackName, emojiPackImageUrl, emojiPackLocked, emojis); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/live/ChzzkLiveSettings.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel.live; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Objects; 6 | 7 | public class ChzzkLiveSettings { 8 | public static class ModifyRequest { 9 | private String defaultLiveTitle; 10 | private String categoryType; 11 | private String categoryId; 12 | private String[] tags; 13 | 14 | public ModifyRequest(ChzzkLiveSettings settings) { 15 | this.defaultLiveTitle = settings.defaultLiveTitle; 16 | this.categoryId = settings.category.getCategoryId(); 17 | this.categoryType = settings.category.getCategoryType().toString(); 18 | this.tags = settings.tags.toArray(new String[0]); 19 | } 20 | } 21 | 22 | private String defaultLiveTitle; 23 | private ChzzkLiveCategory category; 24 | private final List tags = new ArrayList<>(); 25 | 26 | public String getDefaultLiveTitle() { 27 | return defaultLiveTitle; 28 | } 29 | 30 | public void setDefaultLiveTitle(String defaultLiveTitle) { 31 | this.defaultLiveTitle = defaultLiveTitle; 32 | } 33 | 34 | public ChzzkLiveCategory getCategory() { 35 | return category; 36 | } 37 | 38 | public void setCategory(ChzzkLiveCategory category) { 39 | this.category = category; 40 | } 41 | 42 | public List getTags() { 43 | return tags; 44 | } 45 | 46 | @Override 47 | public boolean equals(Object o) { 48 | if (this == o) return true; 49 | if (o == null || getClass() != o.getClass()) return false; 50 | ChzzkLiveSettings that = (ChzzkLiveSettings) o; 51 | return Objects.equals(defaultLiveTitle, that.defaultLiveTitle) && Objects.equals(category, that.category) && Objects.equals(tags, that.tags); 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return Objects.hash(defaultLiveTitle, category, tags); 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "ChzzkLiveSettings{" + 62 | "defaultLiveTitle='" + defaultLiveTitle + '\'' + 63 | ", category=" + category + 64 | ", tags=" + tags + 65 | '}'; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/ChzzkTestBase.java: -------------------------------------------------------------------------------- 1 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 2 | import xyz.r2turntrue.chzzk4j.ChzzkClientBuilder; 3 | import xyz.r2turntrue.chzzk4j.auth.ChzzkLegacyLoginAdapter; 4 | import xyz.r2turntrue.chzzk4j.auth.ChzzkLoginAdapter; 5 | import xyz.r2turntrue.chzzk4j.naver.NaverAutologinAdapter; 6 | 7 | import java.io.FileInputStream; 8 | import java.io.IOException; 9 | import java.util.Properties; 10 | 11 | public class ChzzkTestBase { 12 | Properties properties = new Properties(); 13 | String currentUserId = ""; 14 | ChzzkClient chzzk; 15 | ChzzkClient loginChzzk; 16 | 17 | String apiClientId; 18 | String apiSecret; 19 | 20 | public ChzzkTestBase() { 21 | this(false); 22 | } 23 | 24 | public ChzzkTestBase(boolean naverLogin) { 25 | try { 26 | properties.load(new FileInputStream("env.properties")); 27 | } catch (IOException e) { 28 | throw new RuntimeException(e); 29 | } 30 | 31 | System.out.println("Setup..."); 32 | 33 | currentUserId = properties.getProperty("CURRENT_USER_ID"); 34 | 35 | apiClientId = properties.getProperty("API_CLIENT_ID"); 36 | apiSecret = properties.getProperty("API_SECRET"); 37 | 38 | /* 39 | ChzzkLegacyLoginAdapter adapter = new ChzzkLegacyLoginAdapter( 40 | properties.getProperty("NID_AUT"), 41 | properties.getProperty("NID_SES") 42 | ); 43 | */ 44 | 45 | ChzzkLoginAdapter adapter; 46 | 47 | if (naverLogin) { 48 | var naverId = properties.getProperty("NAVER_ID"); 49 | var naverPw = properties.getProperty("NAVER_PW"); 50 | adapter = new NaverAutologinAdapter( 51 | naverId, 52 | naverPw 53 | ); 54 | } else { 55 | adapter = new ChzzkLegacyLoginAdapter( 56 | properties.getProperty("NID_AUT"), 57 | properties.getProperty("NID_SES") 58 | ); 59 | } 60 | 61 | chzzk = new ChzzkClientBuilder(apiClientId, apiSecret) 62 | .withDebugMode() 63 | .build(); 64 | loginChzzk = new ChzzkClientBuilder(apiClientId, apiSecret) 65 | .withDebugMode() 66 | .withLoginAdapter(adapter) 67 | .build(); 68 | loginChzzk.loginAsync().join(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/gradle-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created 6 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle 7 | 8 | name: Gradle Package 9 | 10 | on: 11 | release: 12 | types: [published] 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | packages: write 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Build Properties 25 | env: 26 | GPG_KEY: ${{ secrets.GPG_KEY }} 27 | GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} 28 | GPG_KEY_PASSWORD: ${{ secrets.GPG_KEY_PASSWORD }} 29 | NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} 30 | NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} 31 | run: | 32 | echo "$GPG_KEY" | base64 -d > secring.gpg 33 | echo -e "signing.keyId=$GPG_KEY_ID" >> publish.properties 34 | echo -e "signing.password=$GPG_KEY_PASSWORD" >> publish.properties 35 | echo -e "signing.secretKeyRingFile=secring.gpg" >> publish.properties 36 | echo -e "nexusUsername=$NEXUS_USERNAME" >> publish.properties 37 | echo -e "nexusPassword=$NEXUS_PASSWORD" >> publish.properties 38 | - name: Import Key 39 | continue-on-error: true 40 | run: | 41 | export GPG_TTY=$(tty) 42 | gpg --import secring.gpg 43 | - name: Set up JDK 21 44 | uses: actions/setup-java@v4 45 | with: 46 | java-version: '21' 47 | distribution: 'temurin' 48 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 49 | settings-path: ${{ github.workspace }} # location for the settings.xml file 50 | 51 | - name: Setup Gradle 52 | uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 53 | 54 | - name: Build with Gradle 55 | run: ./gradlew build -x test 56 | 57 | # The USERNAME and TOKEN need to correspond to the credentials environment variables used in 58 | # the publishing section of your build.gradle 59 | - name: Publish 60 | run: ./gradlew publishToSonatype 61 | -------------------------------------------------------------------------------- /docs/LoginOauth.md: -------------------------------------------------------------------------------- 1 | # LoginOauth 2 | 이 페이지에서는 OpenAPI를 이용해 `ChzzkClient`를 로그인하는 방법에 대해 설명합니다. 3 | 4 | {% hint style="warning" %} 5 | 현재 치지직의 OpenAPI는 아직 완성되지 않은 상태로 보입니다. 6 | 따라서 몇몇 API의 경우는 아직 OpenAPI 인증 방식을 지원하지 않으므로, 되도록이면 [두 인증 방식 동시](LoginBoth.md)에 사용해주세요. 7 | {% endhint %} 8 | 9 | ## 직접 인증 토큰을 기입하는 방식 10 | 치지직의 인증 토큰을 직접 기입해 인증하는 방법입니다. 11 | ```java 12 | ChzzkSimpleUserLoginAdapter adapter = new ChzzkSimpleUserLoginAdapter("Access Token", "Refresh Token"); 13 | 14 | // 혹은 Refresh Token을 null로 둘 수도 있습니다. 15 | ChzzkSimpleUserLoginAdapter adapter = new ChzzkSimpleUserLoginAdapter("Access Token", null); 16 | 17 | ChzzkClient client = new ChzzkClientBuilder("API_CLIENT_ID", "API_SECRET") 18 | .withLoginAdapter(adapter) 19 | .build(); 20 | 21 | client.loginAsync().join(); 22 | ``` 23 | 24 | ## 직접 인증 코드를 기입하는 방식 25 | 치지직의 Account Interlock 과정 이후 Redirect되는 URL의 쿼리에 함께 들어오는 `code` 값을 이용해 인증을 진행합니다. 26 | ```java 27 | ChzzkOauthCodeLoginAdapter adapter = new ChzzkOauthCodeLoginAdapter("code 값", "인증시 사용했던 state 값"); 28 | 29 | ChzzkClient client = new ChzzkClientBuilder("API_CLIENT_ID", "API_SECRET") 30 | .withLoginAdapter(adapter) 31 | .build(); 32 | 33 | client.loginAsync().join(); 34 | ``` 35 | 36 | ## 임시로 HTTP 서버를 열어 인증 토큰을 얻는 방식 37 | 38 | {% hint style="danger" %} 39 | 해당 기능은 애플리케이션 개발 과정의 편의를 위해 만들어진 기능으로, 프로덕션 환경에 최적화되지 않았습니다! 40 | 동시에 각각 다른 `ChzzkClient`의 인증 과정을 진행한다면 포트 문제로 오류가 발생합니다. 41 | 프로덕션 환경에서는 되도록이면 위의 '인증 코드를 기입하는 방식' 혹은 '인증 토큰을 직접 기입하는 방식'을 사용해주세요. 42 | {% endhint %} 43 | 44 | {% hint style="danger" %} 45 | 해당 기능을 사용하기 위해서는 우선 **치지직 개발자 센터**에서 클라이언트의 Redirect URL을 `http://localhost:8080/oauth_callback` 으로 수정해주세요! 46 | {% endhint %} 47 | 48 | 잠시 동안 HTTP 서버를 열어 인증 토큰을 얻는 방식입니다. 49 | 50 | ```java 51 | // 기본적으로는 8080 포트에 인증 서버가 열립니다. 52 | var adapter = new ChzzkOauthLoginAdapter(); 53 | // 혹은 HTTP 서버의 포트를 직접 기입해줄 수도 있습니다. 54 | var adapter = new ChzzkOauthLoginAdapter(포트); 55 | 56 | // Redirect URL의 Host가 다를 경우 이를 설정해줄 수도 있습니다. 57 | var adapter = new ChzzkOauthLoginAdapter("localhost", 포트); 58 | 59 | var client = new ChzzkClientBuilder("API_CLIENT_ID", "API_SECRET") 60 | .withDebugMode() 61 | .withLoginAdapter(adapter) 62 | .build(); 63 | 64 | // 사용자는 이 URL에 접속해 로그인할 수 있습니다. 65 | System.out.println(adapter.getAccountInterlockUrl("API_CLIENT_ID", false)); 66 | 67 | // loginAsync가 실행되었을 때 사용자가 로그인하기 전까지 HTTP 서버가 가동됩니다. 68 | client.loginAsync().join(); 69 | ``` 70 | 71 | ## 액세스 토큰 갱신하기 72 | `ChzzkClient#refreshTokenAsync` 를 통해 `ChzzkClient`의 액세스 토큰을 갱신할 수 있습니다. 73 | 74 | ```java 75 | client.refreshTokenAsync().join(); 76 | ``` -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/message/SessionDonationMessage.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.message; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | import java.util.Map; 6 | import java.util.Objects; 7 | 8 | public class SessionDonationMessage { 9 | public enum DonationType { 10 | CHAT, 11 | VIDEO 12 | } 13 | 14 | private String donationType; 15 | private String channelId; 16 | private String donatorChannelId; 17 | private String donatorNickname; 18 | private int payAmount; 19 | private String donationText; 20 | public Map emojis; 21 | 22 | public DonationType getDonationType() { 23 | return DonationType.valueOf(donationType); 24 | } 25 | 26 | public String getReceivedChannelId() { 27 | return channelId; 28 | } 29 | 30 | public String getDonatorChannelId() { 31 | return donatorChannelId; 32 | } 33 | 34 | public String getDonatorNickname() { 35 | return donatorNickname; 36 | } 37 | 38 | public int getPayAmount() { 39 | return payAmount; 40 | } 41 | 42 | public String getDonationText() { 43 | return donationText; 44 | } 45 | 46 | @Nullable 47 | public String getEmojiImgUrl(String emojiId) { 48 | return emojis.get(emojiId); 49 | } 50 | 51 | public Map getEmojis() { 52 | return emojis; 53 | } 54 | 55 | @Override 56 | public boolean equals(Object o) { 57 | if (this == o) return true; 58 | if (o == null || getClass() != o.getClass()) return false; 59 | SessionDonationMessage that = (SessionDonationMessage) o; 60 | return payAmount == that.payAmount && Objects.equals(donationType, that.donationType) && Objects.equals(channelId, that.channelId) && Objects.equals(donatorChannelId, that.donatorChannelId) && Objects.equals(donatorNickname, that.donatorNickname) && Objects.equals(donationText, that.donationText) && Objects.equals(emojis, that.emojis); 61 | } 62 | 63 | @Override 64 | public int hashCode() { 65 | return Objects.hash(donationType, channelId, donatorChannelId, donatorNickname, payAmount, donationText, emojis); 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return "SessionDonationMessage{" + 71 | "donationType='" + donationType + '\'' + 72 | ", channelId='" + channelId + '\'' + 73 | ", donatorChannelId='" + donatorChannelId + '\'' + 74 | ", donatorNickname='" + donatorNickname + '\'' + 75 | ", payAmount=" + payAmount + 76 | ", donationText='" + donationText + '\'' + 77 | ", emojis=" + emojis + 78 | '}'; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/auth/ChzzkLoginResult.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.auth; 2 | 3 | import java.util.Objects; 4 | 5 | public class ChzzkLoginResult { 6 | 7 | String legacy_NID_AUT; 8 | String legacy_NID_SES; 9 | String accessToken; 10 | String refreshToken; 11 | int tokenExpiresIn; 12 | 13 | public String legacy_NID_AUT() { 14 | return legacy_NID_AUT; 15 | } 16 | 17 | public String legacy_NID_SES() { 18 | return legacy_NID_SES; 19 | } 20 | 21 | public String accessToken() { 22 | return accessToken; 23 | } 24 | 25 | public String refreshToken() { 26 | return refreshToken; 27 | } 28 | 29 | public int tokenExpiresIn() { 30 | return tokenExpiresIn; 31 | } 32 | 33 | public ChzzkLoginResult(String legacy_NID_AUT, String legacy_NID_SES, String accessToken, String refreshToken, int tokenExpiresIn) { 34 | this.legacy_NID_AUT = legacy_NID_AUT; 35 | this.legacy_NID_SES = legacy_NID_SES; 36 | this.accessToken = accessToken; 37 | this.refreshToken = refreshToken; 38 | this.tokenExpiresIn = tokenExpiresIn; 39 | } 40 | 41 | public void _setLegacy_NID_AUT(String legacy_NID_AUT) { 42 | this.legacy_NID_AUT = legacy_NID_AUT; 43 | } 44 | 45 | public void _setLegacy_NID_SES(String legacy_NID_SES) { 46 | this.legacy_NID_SES = legacy_NID_SES; 47 | } 48 | 49 | public void _setAccessToken(String accessToken) { 50 | this.accessToken = accessToken; 51 | } 52 | 53 | public void _setRefreshToken(String refreshToken) { 54 | this.refreshToken = refreshToken; 55 | } 56 | 57 | public void _setTokenExpiresIn(int tokenExpiresIn) { 58 | this.tokenExpiresIn = tokenExpiresIn; 59 | } 60 | 61 | @Override 62 | public boolean equals(Object o) { 63 | if (this == o) return true; 64 | if (o == null || getClass() != o.getClass()) return false; 65 | ChzzkLoginResult that = (ChzzkLoginResult) o; 66 | return tokenExpiresIn == that.tokenExpiresIn && Objects.equals(legacy_NID_AUT, that.legacy_NID_AUT) && Objects.equals(legacy_NID_SES, that.legacy_NID_SES) && Objects.equals(accessToken, that.accessToken) && Objects.equals(refreshToken, that.refreshToken); 67 | } 68 | 69 | @Override 70 | public int hashCode() { 71 | return Objects.hash(legacy_NID_AUT, legacy_NID_SES, accessToken, refreshToken, tokenExpiresIn); 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return "ChzzkLoginResult{" + 77 | "legacy_NID_AUT='" + legacy_NID_AUT + '\'' + 78 | ", legacy_NID_SES='" + legacy_NID_SES + '\'' + 79 | ", accessToken='" + accessToken + '\'' + 80 | ", refreshToken='" + refreshToken + '\'' + 81 | ", tokenExpiresIn=" + tokenExpiresIn + 82 | '}'; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/auth/ChzzkOauthCodeLoginAdapter.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.auth; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonElement; 5 | import org.jetbrains.annotations.Nullable; 6 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 7 | import xyz.r2turntrue.chzzk4j.auth.oauth.TokenRequestBody; 8 | import xyz.r2turntrue.chzzk4j.auth.oauth.TokenResponseBody; 9 | import xyz.r2turntrue.chzzk4j.util.RawApiUtils; 10 | 11 | import java.io.IOException; 12 | import java.util.Objects; 13 | import java.util.concurrent.CompletableFuture; 14 | 15 | public class ChzzkOauthCodeLoginAdapter implements ChzzkLoginAdapter { 16 | 17 | public String code; 18 | public String state; 19 | 20 | public ChzzkOauthCodeLoginAdapter(String code, String state) { 21 | this.code = code; 22 | this.state = state; // 어따 쓰는건진 모르겠는데 /auth/v1/token에서 받네요? 23 | } 24 | 25 | @Override 26 | public CompletableFuture authorize(ChzzkClient client) { 27 | return CompletableFuture.supplyAsync(() -> { 28 | var gson = new Gson(); 29 | 30 | JsonElement resp = null; 31 | try { 32 | resp = RawApiUtils.getContentJson(client.getHttpClient(), RawApiUtils.httpPostRequest(ChzzkClient.OPENAPI_URL + "/auth/v1/token", 33 | gson.toJson(new TokenRequestBody( 34 | "authorization_code", 35 | client.getApiClientId(), 36 | client.getApiSecret(), 37 | code, 38 | state 39 | ))).build(), client.isDebug); 40 | 41 | var respBody = gson.fromJson(resp, TokenResponseBody.class); 42 | 43 | if (client.isDebug) { 44 | System.out.println("AccToken: " + respBody.accessToken()); 45 | System.out.println("RefToken: " + respBody.refreshToken()); 46 | System.out.println("ExpiresIn: " + respBody.expiresIn()); 47 | } 48 | 49 | return new ChzzkLoginResult( 50 | null, 51 | null, 52 | respBody.accessToken(), 53 | respBody.refreshToken(), 54 | respBody.expiresIn() 55 | ); 56 | } catch (IOException e) { 57 | throw new RuntimeException(e); 58 | } 59 | }); 60 | } 61 | 62 | @Override 63 | public boolean equals(Object o) { 64 | if (this == o) return true; 65 | if (o == null || getClass() != o.getClass()) return false; 66 | ChzzkOauthCodeLoginAdapter that = (ChzzkOauthCodeLoginAdapter) o; 67 | return Objects.equals(code, that.code); 68 | } 69 | 70 | @Override 71 | public int hashCode() { 72 | return Objects.hashCode(code); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/util/RawApiUtils.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.util; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import com.google.gson.JsonParser; 6 | import okhttp3.*; 7 | 8 | import java.io.IOException; 9 | 10 | public class RawApiUtils { 11 | public static Request.Builder httpGetRequest(String url) { 12 | return new Request.Builder() 13 | .url(url) 14 | .get(); 15 | } 16 | 17 | public static Request.Builder httpGetRequest(String url, String body) { 18 | return new Request.Builder() 19 | .url(url) 20 | .get(); 21 | } 22 | 23 | public static Request.Builder httpPostRequest(String url, String body) { 24 | return new Request.Builder() 25 | .url(url) 26 | .post(RequestBody.create(body, MediaType.parse("application/json; charset=utf-8"))); 27 | } 28 | 29 | public static Request.Builder httpDeleteRequest(String url, String body) { 30 | return new Request.Builder() 31 | .url(url) 32 | .delete(RequestBody.create(body, MediaType.parse("application/json; charset=utf-8"))); 33 | } 34 | 35 | public static Request.Builder httpPutRequest(String url, String body) { 36 | return new Request.Builder() 37 | .url(url) 38 | .put(RequestBody.create(body, MediaType.parse("application/json; charset=utf-8"))); 39 | } 40 | 41 | public static Request.Builder httpPatchRequest(String url, String body) { 42 | return new Request.Builder() 43 | .url(url) 44 | .patch(RequestBody.create(body, MediaType.parse("application/json; charset=utf-8"))); 45 | } 46 | 47 | public static JsonObject getRawJson(OkHttpClient httpClient, Request request, boolean isDebug) throws IOException { 48 | Response response = httpClient.newCall(request).execute(); 49 | 50 | if (response.isSuccessful()) { 51 | ResponseBody body = response.body(); 52 | assert body != null; 53 | String bodyString = body.string(); 54 | //\System.out.println("BD: " + bodyString); 55 | 56 | return JsonParser.parseString(bodyString).getAsJsonObject(); 57 | } else { 58 | System.err.println(response); 59 | if (isDebug) { 60 | ResponseBody body = response.body(); 61 | if (body != null) { 62 | String bodyString = body.string(); 63 | System.out.println("BD: " + bodyString); 64 | } 65 | } 66 | 67 | if (response.code() == 403 || response.code() == 401) { 68 | throw new IllegalStateException("Unauthorized! Check the client is authorized or the authentication scopes!"); 69 | } 70 | 71 | throw new IOException("Response was not successful! Try to use debugMode to check response body!"); 72 | } 73 | } 74 | 75 | public static JsonElement getContentJson(OkHttpClient httpClient, Request request, boolean isDebug) throws IOException { 76 | return getRawJson(httpClient, request, isDebug).get("content"); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/ChzzkPartialChannel.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 5 | import xyz.r2turntrue.chzzk4j.exception.NotExistsException; 6 | import xyz.r2turntrue.chzzk4j.types.channel.emoticon.ChzzkChannelEmotePackData; 7 | 8 | import java.io.IOException; 9 | import java.util.Objects; 10 | import java.util.concurrent.CompletableFuture; 11 | 12 | public class ChzzkPartialChannel { 13 | private String channelId; 14 | private String channelName; 15 | private String channelImageUrl; 16 | private boolean verifiedMark; 17 | private ChzzkChannelPersonalData personalData; 18 | private ChzzkChannelEmotePackData emotePackData; 19 | 20 | ChzzkPartialChannel() {} 21 | 22 | /** 23 | * Get this channel's {@link ChzzkChannelRules}. 24 | * 25 | * @return {@link CompletableFuture} of the channel 26 | * @throws IOException if the request to API failed 27 | * @throws NotExistsException if the channel doesn't exists or the rules of the channel doesn't available 28 | */ 29 | public CompletableFuture getRules(ChzzkClient chzzk) throws IOException, NotExistsException { 30 | return chzzk.fetchChannelChatRules(channelId); 31 | } 32 | 33 | /** 34 | * Get the channel's id. 35 | */ 36 | public String getChannelId() { 37 | return channelId; 38 | } 39 | 40 | /** 41 | * Get the name of the channel. 42 | */ 43 | public String getChannelName() { 44 | return channelName; 45 | } 46 | 47 | /** 48 | * Get url of the channel's image. 49 | */ 50 | @Nullable 51 | public String getChannelImageUrl() { 52 | return channelImageUrl; 53 | } 54 | 55 | /** 56 | * Get is the channel verified. 57 | */ 58 | public boolean isVerifiedMark() { 59 | return verifiedMark; 60 | } 61 | 62 | /** 63 | * Get personal data of logged user about the channel. 64 | * If not logged in, returns null. 65 | */ 66 | @Nullable 67 | public ChzzkChannelPersonalData getPersonalData() { 68 | return personalData; 69 | } 70 | 71 | /** 72 | * Get the emoticon pack data of the channel. 73 | */ 74 | @Nullable 75 | public ChzzkChannelEmotePackData getEmotePackData() { 76 | return emotePackData; 77 | } 78 | @Override 79 | public String toString() { 80 | return "ChzzkPartialChannel{" + 81 | "channelId='" + channelId + '\'' + 82 | ", channelName='" + channelName + '\'' + 83 | ", channelImageUrl='" + channelImageUrl + '\'' + 84 | ", verifiedMark=" + verifiedMark + 85 | ", personalData=" + personalData + 86 | '}'; 87 | } 88 | 89 | @Override 90 | public boolean equals(Object o) { 91 | if (this == o) return true; 92 | if (o == null || getClass() != o.getClass()) return false; 93 | ChzzkPartialChannel that = (ChzzkPartialChannel) o; 94 | return verifiedMark == that.verifiedMark && Objects.equals(channelId, that.channelId) && Objects.equals(channelName, that.channelName) && Objects.equals(channelImageUrl, that.channelImageUrl) && Objects.equals(personalData, that.personalData) && Objects.equals(emotePackData, that.emotePackData); 95 | } 96 | 97 | @Override 98 | public int hashCode() { 99 | return Objects.hash(channelId, channelName, channelImageUrl, verifiedMark, personalData, emotePackData); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/ChzzkUser.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | import java.util.Arrays; 6 | import java.util.Objects; 7 | 8 | public class ChzzkUser { 9 | boolean hasProfile; 10 | String userIdHash; 11 | String nickname; 12 | String profileImageUrl; 13 | Object[] penalties; // unknown 14 | boolean officialNotiAgree; 15 | String officialNotiAgreeUpdatedDate; 16 | boolean verifiedMark; 17 | boolean loggedIn; 18 | 19 | public ChzzkUser() {} 20 | 21 | 22 | /** 23 | * Get the user has profile. 24 | */ 25 | public boolean isHasProfile() { 26 | return hasProfile; 27 | } 28 | 29 | /** 30 | * Get the user's id. 31 | */ 32 | public String getUserId() { 33 | return userIdHash; 34 | } 35 | 36 | /** 37 | * Get the nickname of the user. 38 | */ 39 | public String getNickname() { 40 | return nickname; 41 | } 42 | 43 | /** 44 | * Get url of the user's profile image. 45 | */ 46 | public String getProfileImageUrl() { 47 | return profileImageUrl; 48 | } 49 | 50 | /** 51 | * Get user agreed to official notification. 52 | */ 53 | public boolean isOfficialNotiAgree() { 54 | return officialNotiAgree; 55 | } 56 | 57 | /** 58 | * Get when user agreed to official notification in ISO-8601 format. 59 | */ 60 | @Nullable 61 | public String getOfficialNotiAgreeUpdatedDate() { 62 | return officialNotiAgreeUpdatedDate; 63 | } 64 | 65 | /** 66 | * Get user has verified mark. 67 | */ 68 | public boolean isVerifiedMark() { 69 | return verifiedMark; 70 | } 71 | 72 | public void _setUserId(String userId) { 73 | userIdHash = userId; 74 | } 75 | 76 | public void _setHasProfile(boolean value) { 77 | hasProfile = value; 78 | } 79 | 80 | public void _setNickname(String nickname) { 81 | this.nickname = nickname; 82 | } 83 | 84 | public void _setLoggedIn(boolean loggedIn) { 85 | this.loggedIn = loggedIn; 86 | } 87 | 88 | @Override 89 | public String toString() { 90 | return "ChzzkUser{" + 91 | "hasProfile=" + hasProfile + 92 | ", userIdHash='" + userIdHash + '\'' + 93 | ", nickname='" + nickname + '\'' + 94 | ", profileImageUrl='" + profileImageUrl + '\'' + 95 | ", penalties=" + Arrays.toString(penalties) + 96 | ", officialNotiAgree=" + officialNotiAgree + 97 | ", officialNotiAgreeUpdatedDate='" + officialNotiAgreeUpdatedDate + '\'' + 98 | ", verifiedMark=" + verifiedMark + 99 | ", loggedIn=" + loggedIn + 100 | '}'; 101 | } 102 | 103 | @Override 104 | public boolean equals(Object o) { 105 | if (this == o) return true; 106 | if (o == null || getClass() != o.getClass()) return false; 107 | ChzzkUser chzzkUser = (ChzzkUser) o; 108 | return hasProfile == chzzkUser.hasProfile && officialNotiAgree == chzzkUser.officialNotiAgree && verifiedMark == chzzkUser.verifiedMark && loggedIn == chzzkUser.loggedIn && Objects.equals(userIdHash, chzzkUser.userIdHash) && Objects.equals(nickname, chzzkUser.nickname) && Objects.equals(profileImageUrl, chzzkUser.profileImageUrl) && Objects.deepEquals(penalties, chzzkUser.penalties) && Objects.equals(officialNotiAgreeUpdatedDate, chzzkUser.officialNotiAgreeUpdatedDate); 109 | } 110 | 111 | @Override 112 | public int hashCode() { 113 | return Objects.hash(hasProfile, userIdHash, nickname, profileImageUrl, Arrays.hashCode(penalties), officialNotiAgree, officialNotiAgreeUpdatedDate, verifiedMark, loggedIn); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/naver/NaverAutologinAdapter.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.naver; 2 | 3 | import com.google.common.collect.Maps; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.openqa.selenium.By; 6 | import org.openqa.selenium.JavascriptExecutor; 7 | import org.openqa.selenium.WebDriver; 8 | import org.openqa.selenium.WebElement; 9 | import org.openqa.selenium.support.ui.WebDriverWait; 10 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 11 | import xyz.r2turntrue.chzzk4j.auth.ChzzkLoginAdapter; 12 | import xyz.r2turntrue.chzzk4j.auth.ChzzkLoginResult; 13 | import xyz.r2turntrue.chzzk4j.util.Chrome; 14 | 15 | import java.time.Duration; 16 | import java.util.Map; 17 | import java.util.concurrent.CompletableFuture; 18 | 19 | public class NaverAutologinAdapter implements ChzzkLoginAdapter { 20 | 21 | private final @NotNull String id; 22 | private final @NotNull String password; 23 | private final @NotNull Map cookies; 24 | 25 | /** 26 | * Log in to Naver using ID and password. 27 | * @param id naver id 28 | * @param password password of the naver id 29 | */ 30 | public NaverAutologinAdapter(@NotNull String id, @NotNull String password) { 31 | this.id = id; 32 | this.password = password; 33 | this.cookies = Maps.newConcurrentMap(); 34 | } 35 | 36 | @Override 37 | public CompletableFuture authorize(ChzzkClient client) { 38 | return CompletableFuture.supplyAsync(() -> { 39 | WebDriver driver = Chrome.getDriver(); 40 | driver.get("https://nid.naver.com/nidlogin.login"); 41 | try { 42 | // Write id and pw fields 43 | if (driver instanceof JavascriptExecutor js) { 44 | js.executeScript(String.format("document.getElementById('id').value='%s';", id)); 45 | js.executeScript(String.format("document.getElementById('pw').value='%s';", password)); 46 | } 47 | 48 | // Click login button 49 | WebElement loginBtn = driver.findElement(By.id("log.login")); 50 | loginBtn.click(); 51 | 52 | // Wait until the specific cookies are available 53 | WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); 54 | wait.until(driver1 -> driver1.manage().getCookieNamed("NID_AUT") != null); 55 | 56 | // Find cookies 57 | cookies.clear(); 58 | for (NaverAutologinAdapter.Cookie key : NaverAutologinAdapter.Cookie.values()) { 59 | org.openqa.selenium.Cookie cookie = driver.manage().getCookieNamed(key.toString()); 60 | if (cookie != null) { 61 | cookies.put(key, cookie.getValue()); 62 | } 63 | } 64 | 65 | if (client.isDebug) System.out.println("NID_AUT: " + getCookie(Cookie.NID_AUT)); 66 | } catch (Exception e) { 67 | e.printStackTrace(); 68 | } finally { 69 | driver.quit(); 70 | } 71 | 72 | return new ChzzkLoginResult( 73 | getCookie(Cookie.NID_AUT), 74 | getCookie(Cookie.NID_SES), 75 | null, 76 | null, 77 | -1); 78 | }); 79 | } 80 | 81 | public @NotNull String getId() { 82 | return id; 83 | } 84 | 85 | public @NotNull String getPassword() { 86 | return password; 87 | } 88 | 89 | /** 90 | * If logged into Naver, it returns the cookie value. 91 | * If not logged in, it returns an empty string. 92 | * @param key {@link NaverAutologinAdapter.Cookie} 93 | */ 94 | public @NotNull String getCookie(@NotNull NaverAutologinAdapter.Cookie key) { 95 | return cookies.getOrDefault(key, ""); 96 | } 97 | 98 | public enum Cookie { 99 | NID_AUT, NID_SES 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/ChzzkChatSettings.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types; 2 | 3 | import java.util.Objects; 4 | 5 | public class ChzzkChatSettings { 6 | public enum ChatAvailableCondition { 7 | NONE, 8 | REAL_NAME 9 | } 10 | 11 | public enum ChatAvailableGroup { 12 | ALL, 13 | FOLLOWER, 14 | MANAGER, 15 | SUBSCRIBER 16 | } 17 | 18 | public enum MinFollowerMinute { 19 | M_0, 20 | M_5, 21 | M_10, 22 | M_30, 23 | M_60, 24 | M_1440, 25 | M_10080, 26 | M_43200 27 | } 28 | 29 | public enum ChatSlowModeSec { 30 | S_0, 31 | S_3, 32 | S_5, 33 | S_10, 34 | S_30, 35 | S_60, 36 | S_120, 37 | S_300 38 | } 39 | 40 | private String chatAvailableCondition; 41 | private String chatAvailableGroup; 42 | private int minFollowerMinute; 43 | private boolean allowSubscriberInFollowerMode; 44 | private int chatSlowModeSec; 45 | private boolean chatEmojiMode; 46 | 47 | public ChatAvailableCondition getChatAvailableCondition() { 48 | return ChatAvailableCondition.valueOf(chatAvailableCondition); 49 | } 50 | 51 | public ChatAvailableGroup getChatAvailableGroup() { 52 | return ChatAvailableGroup.valueOf(chatAvailableGroup); 53 | } 54 | 55 | public MinFollowerMinute getMinFollowerMinute() { 56 | return MinFollowerMinute.valueOf("M_" + minFollowerMinute); 57 | } 58 | 59 | public boolean isAllowSubscriberInFollowerMode() { 60 | return allowSubscriberInFollowerMode; 61 | } 62 | 63 | public int getChatSlowModeSec() { 64 | return chatSlowModeSec; 65 | } 66 | 67 | public boolean isChatEmojiMode() { 68 | return chatEmojiMode; 69 | } 70 | 71 | public void setChatAvailableCondition(ChatAvailableCondition chatAvailableCondition) { 72 | this.chatAvailableCondition = chatAvailableCondition.toString(); 73 | } 74 | 75 | public void setChatAvailableGroup(ChatAvailableGroup chatAvailableGroup) { 76 | this.chatAvailableGroup = chatAvailableGroup.toString(); 77 | } 78 | 79 | public void setMinFollowerMinute(MinFollowerMinute minFollowerMinute) { 80 | this.minFollowerMinute = Integer.parseInt(minFollowerMinute.toString().replace("M_", "")); 81 | } 82 | 83 | public void setAllowSubscriberInFollowerMode(boolean allowSubscriberInFollowerMode) { 84 | this.allowSubscriberInFollowerMode = allowSubscriberInFollowerMode; 85 | } 86 | 87 | public void setChatEmojiMode(boolean chatEmojiMode) { 88 | this.chatEmojiMode = chatEmojiMode; 89 | } 90 | 91 | public void setChatSlowModeSec(ChatSlowModeSec sec) { 92 | this.chatSlowModeSec = Integer.parseInt(sec.toString().replace("S_", "")); 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | return "ChzzkChatSettings{" + 98 | "chatAvailableCondition='" + chatAvailableCondition + '\'' + 99 | ", chatAvailableGroup='" + chatAvailableGroup + '\'' + 100 | ", minFollowerMinute=" + minFollowerMinute + 101 | ", allowSubscriberInFollowerMode=" + allowSubscriberInFollowerMode + 102 | '}'; 103 | } 104 | 105 | @Override 106 | public boolean equals(Object o) { 107 | if (this == o) return true; 108 | if (o == null || getClass() != o.getClass()) return false; 109 | ChzzkChatSettings that = (ChzzkChatSettings) o; 110 | return minFollowerMinute == that.minFollowerMinute && allowSubscriberInFollowerMode == that.allowSubscriberInFollowerMode && Objects.equals(chatAvailableCondition, that.chatAvailableCondition) && Objects.equals(chatAvailableGroup, that.chatAvailableGroup); 111 | } 112 | 113 | @Override 114 | public int hashCode() { 115 | return Objects.hash(chatAvailableCondition, chatAvailableGroup, minFollowerMinute, allowSubscriberInFollowerMode); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/SessionApiTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Test; 2 | import xyz.r2turntrue.chzzk4j.ChzzkClientBuilder; 3 | import xyz.r2turntrue.chzzk4j.auth.ChzzkOauthLoginAdapter; 4 | import xyz.r2turntrue.chzzk4j.exception.NotLoggedInException; 5 | import xyz.r2turntrue.chzzk4j.session.ChzzkSessionBuilder; 6 | import xyz.r2turntrue.chzzk4j.session.ChzzkSessionSubscriptionType; 7 | import xyz.r2turntrue.chzzk4j.session.event.*; 8 | 9 | import java.io.IOException; 10 | import java.util.UUID; 11 | 12 | public class SessionApiTest extends ChzzkTestBase { 13 | 14 | @Test 15 | public void sessionTest() throws IOException, InterruptedException, NotLoggedInException { 16 | var adapter = new ChzzkOauthLoginAdapter(5000); 17 | 18 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 19 | .withDebugMode() 20 | .withLoginAdapter(adapter) 21 | .build(); 22 | 23 | System.out.println(adapter.getAccountInterlockUrl(apiClientId, false)); 24 | 25 | client.loginAsync().join(); 26 | 27 | System.out.println(client.fetchLoggedUser()); 28 | 29 | var session = new ChzzkSessionBuilder(client) 30 | .buildUserSession(); 31 | 32 | session.subscribeAsync(ChzzkSessionSubscriptionType.CHAT).join(); 33 | session.subscribeAsync(ChzzkSessionSubscriptionType.DONATION).join(); 34 | session.subscribeAsync(ChzzkSessionSubscriptionType.CHANNEL_SUBSCRIBE).join(); 35 | 36 | session.on(SessionConnectedEvent.class, (event) -> { 37 | System.out.println("Connected!"); 38 | }); 39 | 40 | session.on(SessionDisconnectedEvent.class, (event) -> { 41 | System.out.println("Disconnected :("); 42 | }); 43 | 44 | session.on(SessionChatMessageEvent.class, (event) -> { 45 | var msg = event.getMessage(); 46 | System.out.printf("[CHAT] %s: %s [at %s]%n", msg.getProfile().getNickname(), msg.getContent(), msg.getMessageTime()); 47 | }); 48 | 49 | session.on(SessionDonationEvent.class, (event) -> { 50 | var msg = event.getMessage(); 51 | System.out.printf("[DONATION] %s: %s [%s]%n", msg.getDonatorNickname(), msg.getDonationText(), msg.getDonationType()); 52 | }); 53 | 54 | session.on(SessionNewSubscriberEvent.class, (event) -> { 55 | var msg = event.getMessage(); 56 | System.out.printf("[SUBSCRIBE] %s just subscribed!: %s", msg.getSubscriberNickname(), msg.toString()); 57 | }); 58 | 59 | session.on(SessionRecreateEvent.class, (event) -> { 60 | System.out.println("Recreating the session..."); 61 | }); 62 | 63 | session.on(SessionSubscribedEvent.class, (event) -> { 64 | System.out.println("Yeah I subscribed to: " + event.getEventType()); 65 | }); 66 | 67 | session.on(SessionUnsubscribedEvent.class, (event) -> { 68 | System.out.println("Yeah I unsubscribed to: " + event.getEventType()); 69 | }); 70 | 71 | session.createAndConnectAsync().join(); 72 | 73 | Thread.sleep(Long.MAX_VALUE); 74 | } 75 | 76 | @Test 77 | public void sessionAutoRecreateTest() throws IOException, NotLoggedInException, InterruptedException { 78 | var adapter = new ChzzkOauthLoginAdapter(5000); 79 | 80 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 81 | .withDebugMode() 82 | .withLoginAdapter(adapter) 83 | .build(); 84 | 85 | System.out.println(adapter.getAccountInterlockUrl(apiClientId, false)); 86 | 87 | client.loginAsync().join(); 88 | 89 | System.out.println(client.fetchLoggedUser()); 90 | 91 | var session = new ChzzkSessionBuilder(client) 92 | .withAutoRecreate(true) 93 | .buildUserSession(); 94 | 95 | session.on(SessionConnectedEvent.class, (event) -> { 96 | System.out.println("Connected!"); 97 | }); 98 | 99 | session.on(SessionDisconnectedEvent.class, (event) -> { 100 | System.out.println("Disconnected :("); 101 | }); 102 | 103 | session.createAndConnectAsync().join(); 104 | 105 | Thread.sleep(1000); 106 | 107 | session.disconnectAsync(false).join(); 108 | 109 | Thread.sleep(Long.MAX_VALUE); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/ChannelApiTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Test; 2 | import xyz.r2turntrue.chzzk4j.exception.ChannelNotExistsException; 3 | import xyz.r2turntrue.chzzk4j.exception.NoAccessTokenOnlySupported; 4 | import xyz.r2turntrue.chzzk4j.exception.NotExistsException; 5 | import xyz.r2turntrue.chzzk4j.exception.NotLoggedInException; 6 | import xyz.r2turntrue.chzzk4j.types.ChzzkUser; 7 | import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannel; 8 | 9 | import java.io.IOException; 10 | import java.util.concurrent.ExecutionException; 11 | import java.util.concurrent.atomic.AtomicReference; 12 | 13 | import org.junit.jupiter.api.Assertions; 14 | import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannelFollower; 15 | import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannelFollowingData; 16 | import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannelRules; 17 | import xyz.r2turntrue.chzzk4j.types.channel.recommendation.ChzzkRecommendationChannels; 18 | 19 | // 8e7a6f0a0b1f0612afee1a673e94027d - 레고칠칠 20 | // c2186ca6edb3a663f137b15ed7346fac - 리얼진짜우왁굳 21 | // 두 채널 모두 팔로우한 뒤 테스트 진행해주세요. 22 | // 23 | // 22bd842599735ae19e454983280f611e - ENCHANT 24 | // 위 채널은 팔로우 해제 후 테스트 진행해주세요. 25 | public class ChannelApiTest extends ChzzkTestBase { 26 | public final String FOLLOWED_CHANNEL_1 = "8e7a6f0a0b1f0612afee1a673e94027d"; // 레고칠칠 27 | public final String FOLLOWED_CHANNEL_2 = "c2186ca6edb3a663f137b15ed7346fac"; // 리얼진짜우왁굳 28 | public final String UNFOLLOWED_CHANNEL = "22bd842599735ae19e454983280f611e"; // ENCHANT 29 | 30 | public ChannelApiTest() { 31 | super(true); 32 | } 33 | 34 | @Test 35 | void gettingNormalChannelInfo() throws IOException { 36 | AtomicReference channel = new AtomicReference<>(); 37 | Assertions.assertDoesNotThrow(() -> 38 | channel.set(chzzk.fetchChannel(FOLLOWED_CHANNEL_2).get())); 39 | 40 | System.out.println(channel); 41 | } 42 | 43 | @Test 44 | void gettingInvalidChannelInfo() throws IOException { 45 | Assertions.assertThrowsExactly(ChannelNotExistsException.class, () -> { 46 | chzzk.fetchChannel("invalidchannelid"); 47 | }); 48 | } 49 | 50 | @Test 51 | void gettingNormalChannelRules() throws IOException { 52 | AtomicReference rule = new AtomicReference<>(); 53 | Assertions.assertDoesNotThrow(() -> 54 | rule.set(chzzk.fetchChannelChatRules(FOLLOWED_CHANNEL_1).get())); 55 | 56 | System.out.println(rule); 57 | } 58 | 59 | @Test 60 | void gettingInvalidChannelRules() throws IOException { 61 | Assertions.assertThrowsExactly(NotExistsException.class, () -> { 62 | chzzk.fetchChannelChatRules("invalidchannel or no rule channel"); 63 | }); 64 | } 65 | 66 | @Test 67 | void gettingFollowStatusAnonymous() throws IOException { 68 | Assertions.assertThrowsExactly(NotLoggedInException.class, () -> 69 | chzzk.fetchFollowingStatus("FOLLOWED_CHANNEL_1")); 70 | } 71 | 72 | @Test 73 | void gettingFollowStatus() throws IOException { 74 | AtomicReference followingStatus = new AtomicReference<>(); 75 | Assertions.assertDoesNotThrow(() -> 76 | followingStatus.set(loginChzzk.fetchFollowingStatus(FOLLOWED_CHANNEL_1).get())); 77 | 78 | System.out.println(followingStatus); 79 | 80 | Assertions.assertEquals(followingStatus.get().isFollowing(), true); 81 | 82 | Assertions.assertDoesNotThrow(() -> 83 | followingStatus.set(loginChzzk.fetchFollowingStatus(UNFOLLOWED_CHANNEL).get())); 84 | 85 | System.out.println(followingStatus); 86 | 87 | Assertions.assertEquals(followingStatus.get().isFollowing(), false); 88 | } 89 | 90 | @Test 91 | void gettingUserInfo() throws IOException, NotLoggedInException, ExecutionException, InterruptedException { 92 | ChzzkUser currentUser = loginChzzk.fetchLoggedUser().get(); 93 | System.out.println(currentUser); 94 | Assertions.assertEquals(currentUser.getUserId(), currentUserId); 95 | } 96 | 97 | @Test 98 | void gettingRecommendationChannels() throws IOException, NotLoggedInException, NoAccessTokenOnlySupported, ExecutionException, InterruptedException { 99 | ChzzkRecommendationChannels channels = loginChzzk.fetchRecommendationChannels().get(); 100 | System.out.println(channels); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/session/message/SessionChatMessage.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.session.message; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | import java.time.Instant; 6 | import java.time.LocalDateTime; 7 | import java.util.Arrays; 8 | import java.util.Map; 9 | import java.util.Objects; 10 | import java.util.TimeZone; 11 | 12 | public class SessionChatMessage { 13 | public static class Profile { 14 | public static class Badge { 15 | private String imageUrl; 16 | 17 | public String getImageUrl() { 18 | return imageUrl; 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) return true; 24 | if (o == null || getClass() != o.getClass()) return false; 25 | Badge badge = (Badge) o; 26 | return Objects.equals(imageUrl, badge.imageUrl); 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hashCode(imageUrl); 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return "Badge{" + 37 | "imageUrl='" + imageUrl + '\'' + 38 | '}'; 39 | } 40 | } 41 | 42 | private String nickname; 43 | private boolean verifiedMark; 44 | private Badge[] badges; 45 | 46 | public String getNickname() { 47 | return nickname; 48 | } 49 | 50 | public boolean isVerifiedMark() { 51 | return verifiedMark; 52 | } 53 | 54 | public Badge[] getBadges() { 55 | return badges; 56 | } 57 | 58 | @Override 59 | public boolean equals(Object o) { 60 | if (this == o) return true; 61 | if (o == null || getClass() != o.getClass()) return false; 62 | Profile profile = (Profile) o; 63 | return verifiedMark == profile.verifiedMark && Objects.equals(nickname, profile.nickname) && Objects.deepEquals(badges, profile.badges); 64 | } 65 | 66 | @Override 67 | public int hashCode() { 68 | return Objects.hash(nickname, verifiedMark, Arrays.hashCode(badges)); 69 | } 70 | 71 | @Override 72 | public String toString() { 73 | return "Profile{" + 74 | "nickname='" + nickname + '\'' + 75 | ", verifiedMark=" + verifiedMark + 76 | ", badges=" + Arrays.toString(badges) + 77 | '}'; 78 | } 79 | } 80 | 81 | private String channelId; 82 | private String senderChannelId; 83 | 84 | private Profile profile; 85 | private String content; 86 | private Map emojis; 87 | private long messageTime; 88 | 89 | public String getReceivedChannelId() { 90 | return channelId; 91 | } 92 | 93 | public String getSenderChannelId() { 94 | return senderChannelId; 95 | } 96 | 97 | public Profile getProfile() { 98 | return profile; 99 | } 100 | 101 | public String getContent() { 102 | return content; 103 | } 104 | 105 | @Nullable 106 | public String getEmojiImgUrl(String emojiId) { 107 | return emojis.get(emojiId); 108 | } 109 | 110 | public Map getEmojis() { 111 | return emojis; 112 | } 113 | 114 | public LocalDateTime getMessageTime() { 115 | return LocalDateTime.ofInstant(Instant.ofEpochMilli(messageTime), 116 | TimeZone.getDefault().toZoneId()); 117 | } 118 | 119 | @Override 120 | public boolean equals(Object o) { 121 | if (this == o) return true; 122 | if (o == null || getClass() != o.getClass()) return false; 123 | SessionChatMessage that = (SessionChatMessage) o; 124 | return messageTime == that.messageTime && Objects.equals(channelId, that.channelId) && Objects.equals(senderChannelId, that.senderChannelId) && Objects.equals(profile, that.profile) && Objects.equals(content, that.content) && Objects.equals(emojis, that.emojis); 125 | } 126 | 127 | @Override 128 | public int hashCode() { 129 | return Objects.hash(channelId, senderChannelId, profile, content, emojis, messageTime); 130 | } 131 | 132 | @Override 133 | public String toString() { 134 | return "SessionChatMessage{" + 135 | "channelId='" + channelId + '\'' + 136 | ", senderChannelId='" + senderChannelId + '\'' + 137 | ", profile=" + profile + 138 | ", content='" + content + '\'' + 139 | ", emojis=" + emojis + 140 | ", messageTime=" + messageTime + 141 | '}'; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/live/ChzzkLiveDetail.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel.live; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.time.LocalDateTime; 6 | import java.time.ZoneId; 7 | import java.time.ZonedDateTime; 8 | import java.time.format.DateTimeFormatter; 9 | import java.util.Objects; 10 | import java.util.Optional; 11 | 12 | public class ChzzkLiveDetail extends ChzzkLiveStatus { 13 | 14 | private transient final @NotNull DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 15 | private final transient @NotNull ZoneId zoneId = ZoneId.of("Asia/Seoul"); 16 | 17 | private int liveId; 18 | private String liveImageUrl; 19 | private String defaultThumbnailImageUrl; 20 | private String openDate; 21 | private String closeDate; 22 | private ChzzkLiveChannel channel; 23 | 24 | /** 25 | * Get unique ID number of the live stream. 26 | */ 27 | public int getLiveId() { 28 | return liveId; 29 | } 30 | 31 | /** 32 | * Get URL of the automatically generated thumbnail image. 33 | * @param resolution Image {@link Resolution} 34 | */ 35 | public @NotNull String getLiveImageUrl(@NotNull Resolution resolution) { 36 | return liveImageUrl.replace("{type}", resolution.getRawAsString()); 37 | } 38 | 39 | /** 40 | * Get default thumbnail image URL. 41 | */ 42 | public @NotNull Optional getDefaultThumbnailImageUrl() { 43 | return Optional.ofNullable(defaultThumbnailImageUrl); 44 | } 45 | 46 | /** 47 | * Get start time of the live stream. 48 | */ 49 | public @NotNull Optional getOpenDate() { 50 | if (openDate == null) { 51 | return Optional.empty(); 52 | } 53 | ZonedDateTime date = LocalDateTime.parse(openDate, formatter).atZone(zoneId); 54 | return Optional.of(date); 55 | } 56 | 57 | /** 58 | * Get close time of the live stream. 59 | */ 60 | public @NotNull Optional getCloseDate() { 61 | if (closeDate == null) { 62 | return Optional.empty(); 63 | } 64 | ZonedDateTime date = LocalDateTime.parse(closeDate, formatter).atZone(zoneId); 65 | return Optional.of(date); 66 | } 67 | 68 | /** 69 | * Get live stream channel. 70 | */ 71 | public @NotNull ChzzkLiveChannel getLiveChannel() { 72 | return channel; 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | return "ChzzkLiveDetailImpl{" + 78 | "liveId=" + liveId + 79 | ", liveImageUrl='" + liveImageUrl + '\'' + 80 | ", defaultThumbnailImageUrl='" + defaultThumbnailImageUrl + '\'' + 81 | ", openDate='" + openDate + '\'' + 82 | ", closeDate='" + closeDate + '\'' + 83 | ", channel=" + channel + 84 | ", liveTitle='" + liveTitle + '\'' + 85 | ", status='" + status + '\'' + 86 | ", concurrentUserCount=" + concurrentUserCount + 87 | ", accumulateCount=" + accumulateCount + 88 | ", paidPromotion=" + paidPromotion + 89 | ", adult=" + adult + 90 | ", clipActive=" + clipActive + 91 | ", chatChannelId='" + chatChannelId + '\'' + 92 | ", tags=" + tags + 93 | ", categoryType='" + categoryType + '\'' + 94 | ", liveCategory='" + liveCategory + '\'' + 95 | ", liveCategoryValue='" + liveCategoryValue + '\'' + 96 | ", livePollingStatusJson='" + livePollingStatusJson + '\'' + 97 | ", faultStatus=" + faultStatus + 98 | ", userAdultStatus=" + userAdultStatus + 99 | ", chatActive=" + chatActive + 100 | ", chatAvailableGroup='" + chatAvailableGroup + '\'' + 101 | ", chatAvailableCondition='" + chatAvailableCondition + '\'' + 102 | ", minFollowerMinute=" + minFollowerMinute + 103 | ", chatDonationRankingExposure=" + chatDonationRankingExposure + 104 | '}'; 105 | } 106 | 107 | @Override 108 | public boolean equals(Object o) { 109 | if (this == o) return true; 110 | if (o == null || getClass() != o.getClass()) return false; 111 | ChzzkLiveDetail that = (ChzzkLiveDetail) o; 112 | return liveId == that.liveId && Objects.equals(formatter, that.formatter) && Objects.equals(zoneId, that.zoneId) && Objects.equals(liveImageUrl, that.liveImageUrl) && Objects.equals(defaultThumbnailImageUrl, that.defaultThumbnailImageUrl) && Objects.equals(openDate, that.openDate) && Objects.equals(closeDate, that.closeDate) && Objects.equals(channel, that.channel); 113 | } 114 | 115 | @Override 116 | public int hashCode() { 117 | return Objects.hash(formatter, zoneId, liveId, liveImageUrl, defaultThumbnailImageUrl, openDate, closeDate, channel); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](image/banner.png) 2 | # chzzk4j 3 | 4 | * **0.0.12 이하 버전을 사용하신다면, [과거 버전의 문서](https://github.com/R2turnTrue/chzzk4j/blob/2a5936c9570220957c3ef4f13462d12d4d19e4ff/README.md) 파일을 확인해주세요!** 5 | 6 | ![](https://img.shields.io/maven-central/v/io.github.R2turnTrue/chzzk4j) / 7 | [Example Minecraft Plugin](https://github.com/R2turnTrue/chzzk4j_demo) / 8 | [Discord Server](https://discord.gg/GgNXbzZeDk) 9 | 10 | Unofficial Java API library of CHZZK (치지직, the video streaming service of Naver) 11 | 12 | * This library is not completed. Please contribute a lot through Pull-Request! 13 | * Please feel free to create an issue if you have any problems! 14 | 15 | ## installation 16 | 17 | > [!Warning] 18 | > 0.1.1 이하의 경우, Group ID로 `io.github.R2turnTrue`를 사용해야 합니다! 대소문자를 구별하니 조심해주세요! 19 | 20 | ```kotlin 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | dependencies { 26 | implementation("io.github.r2turntrue:chzzk4j:0.1.3") 27 | } 28 | ``` 29 | 30 | ## usage 31 | Check at [our docs](https://r2turntrue.gitbook.io/chzzk4j)! 32 | 33 | ## examples 34 | 35 | ### minimal legacy chat example 36 | ```java 37 | ChzzkClient client = new ChzzkClientBuilder() // 레거시 챗은 비공개 API이기 때문에, API 키가 필요하지 않습니다. 38 | .build(); 39 | ChzzkChat chat = new ChzzkChatBuilder(client, 40 | "CHANNEL_ID") 41 | .build(); 42 | 43 | chat.on(ConnectEvent.class, (evt) -> { 44 | System.out.println("connected to chat :)"); 45 | }); 46 | 47 | chat.on(ChatMessageEvent.class, (evt) -> { 48 | ChatMessage msg = evt.getMessage(); 49 | 50 | if (msg.getProfile() == null) { 51 | System.out.println(String.format("익명: %s", msg.getContent())); 52 | return; 53 | } 54 | 55 | System.out.println( 56 | String.format("[Chat] %s: %s", 57 | msg.getProfile().getNickname(), 58 | msg.getContent())); 59 | }); 60 | 61 | chat.connectBlocking(); 62 | ``` 63 | 64 | ### session api example 65 | ```java 66 | var adapter = new ChzzkOauthLoginAdapter(5000); 67 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 68 | .withDebugMode() 69 | .withLoginAdapter(adapter) 70 | .build(); 71 | 72 | System.out.println(adapter.getAccountInterlockUrl(apiClientId, false)); // Show Login URL 73 | 74 | client.loginAsync().join(); // Wait for the user to login by the interlock URL 75 | 76 | var session = new ChzzkSessionBuilder(client) 77 | .buildUserSession(); 78 | 79 | session.subscribeAsync(ChzzkSessionSubscriptionType.CHAT).join(); 80 | 81 | session.on(SessionConnectedEvent.class, (event) -> { 82 | System.out.println("Connected!"); 83 | }); 84 | 85 | session.on(SessionDisconnectedEvent.class, (event) -> { 86 | System.out.println("Disconnected :("); 87 | }); 88 | 89 | session.on(SessionChatMessageEvent.class, (event) -> { 90 | var msg = event.getMessage(); 91 | System.out.printf("[CHAT] %s: %s [at %s]%n", msg.getProfile().getNickname(), msg.getContent(), msg.getMessageTime()); 92 | }); 93 | 94 | session.on(SessionRecreateEvent.class, (event) -> { 95 | System.out.println("Recreating the session..."); 96 | }); 97 | 98 | session.on(SessionSubscribedEvent.class, (event) -> { 99 | System.out.println("Yeah I subscribed to: " + event.getEventType()); 100 | }); 101 | 102 | session.on(SessionUnsubscribedEvent.class, (event) -> { 103 | System.out.println("Yeah I unsubscribed to: " + event.getEventType()); 104 | }); 105 | 106 | session.createAndConnectAsync().join(); 107 | ``` 108 | 109 | ### modify chat settings example 110 | ```java 111 | ChzzkChatSettings settings = client.fetchChatSettings().join(); 112 | 113 | // 본인인증 여부 설정 조건을 변경합니다. 114 | settings.setChatAvailableCondition(ChzzkChatSettings.ChatAvailableCondition.REAL_NAME); 115 | 116 | // 채팅 참여 범위 설정 조건을 변경합니다. 117 | settings.setChatAvailableGroup(ChzzkChatSettings.ChatAvailableGroup.FOLLOWER); 118 | // (위 ChatAvailableGroup이 FOLLOWER일 경우) 최소 팔로잉 기간을 설정합니다. 119 | settings.setMinFollowerMinute(ChzzkChatSettings.MinFollowerMinute.M_60); 120 | // (위 ChatAvailableGroup이 FOLLOWER일 경우) 구독자는 최소 팔로잉 기간 조건 대상에서 제외/허용 할지 여부를 설정합니다. 121 | settings.setAllowSubscriberInFollowerMode(false); 122 | 123 | // 변경된 방송 설정을 서버에 전송합니다. 124 | client.modifyChatSettings(settings).join(); 125 | ``` 126 | 127 | ## features 128 | 129 | - [x] get channel information & rules 130 | - [x] get current user's information 131 | - [x] get channel followed status 132 | - [x] async chat integration (read/send) 133 | - [x] get recommendation channels 134 | - [x] fix invalid json (chat) 135 | - [x] load emoji pack 136 | - [x] get live status 137 | - [x] get live detail 138 | 139 | ### need to implement 140 | 141 | - [ ] write javadocs of all methods/classes/etc.. 142 | - [ ] parse emoji from chat message 143 | - [ ] get following channels of user that logged in 144 | - [ ] get video information 145 | - [ ] get cheese ranking 146 | 147 | ### references 148 | 149 | - [kimcore/chzzk](https://github.com/kimcore/chzzk) 150 | - [Chzzk Official API - Session Test Results](https://gist.github.com/fi-xz/69ce1f35ca1b2318a2b410c0d5757e0f#file-main-kt) -------------------------------------------------------------------------------- /src/test/java/OpenChatApiTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Assertions; 2 | import org.junit.jupiter.api.Test; 3 | import xyz.r2turntrue.chzzk4j.ChzzkClientBuilder; 4 | import xyz.r2turntrue.chzzk4j.auth.ChzzkOauthLoginAdapter; 5 | import xyz.r2turntrue.chzzk4j.exception.NotLoggedInException; 6 | import xyz.r2turntrue.chzzk4j.types.ChzzkChatSettings; 7 | import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannelFollower; 8 | import xyz.r2turntrue.chzzk4j.types.channel.ChzzkChannelSubscriber; 9 | 10 | import java.util.Arrays; 11 | import java.util.UUID; 12 | 13 | public class OpenChatApiTest extends ChzzkTestBase { 14 | @Test 15 | void fetchFollowers() { 16 | Assertions.assertDoesNotThrow(() -> { 17 | var adapter = new ChzzkOauthLoginAdapter(5000); 18 | 19 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 20 | .withDebugMode() 21 | .withLoginAdapter(adapter) 22 | .build(); 23 | 24 | System.out.println(adapter.getAccountInterlockUrl(apiClientId, false)); 25 | client.loginAsync().get(); 26 | 27 | ChzzkChannelFollower[] followers = client.fetchChannelFollowers().get().getFollowers(); 28 | 29 | System.out.println(Arrays.toString(followers)); 30 | }); 31 | } 32 | 33 | @Test 34 | void fetchSubscribers() { 35 | Assertions.assertDoesNotThrow(() -> { 36 | var adapter = new ChzzkOauthLoginAdapter(5000); 37 | 38 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 39 | .withDebugMode() 40 | .withLoginAdapter(adapter) 41 | .build(); 42 | 43 | System.out.println(adapter.getAccountInterlockUrl(apiClientId, false)); 44 | client.loginAsync().get(); 45 | 46 | ChzzkChannelSubscriber[] subscribers = client.fetchChannelSubscribers().get().getFollowers(); 47 | 48 | System.out.println(Arrays.toString(subscribers)); 49 | }); 50 | } 51 | 52 | @Test 53 | public void testOpenChatApi() { 54 | Assertions.assertDoesNotThrow(() -> { 55 | var adapter = new ChzzkOauthLoginAdapter(5000); 56 | 57 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 58 | .withDebugMode() 59 | .withLoginAdapter(adapter) 60 | .build(); 61 | 62 | System.out.println(adapter.getAccountInterlockUrl(apiClientId, false)); 63 | 64 | client.loginAsync().join(); 65 | 66 | System.out.println(client.fetchLoggedUser()); 67 | 68 | try { 69 | System.out.println(Arrays.toString(client.searchCategories("마인크래프트").join())); 70 | 71 | var live = client.fetchLiveSettings().join(); 72 | System.out.println(live); 73 | 74 | live.setDefaultLiveTitle(UUID.randomUUID().toString()); 75 | 76 | client.modifyLiveSettings(live).join(); 77 | 78 | var newLive = client.fetchLiveSettings().join(); 79 | 80 | Assertions.assertEquals(live.getDefaultLiveTitle(), newLive.getDefaultLiveTitle()); 81 | 82 | var chatId = client.sendChatToLoggedInChannel("안녕, 세상! --> " + UUID.randomUUID()).join(); 83 | System.out.println(chatId); 84 | client.setAnnouncementOfLoggedInChannel("안녕, 세상! [공지] --> " + UUID.randomUUID()).join(); 85 | 86 | var oldSettings = client.fetchChatSettings().join(); 87 | System.out.println(oldSettings); 88 | 89 | var settings = client.fetchChatSettings().join(); 90 | 91 | settings.setChatAvailableCondition(ChzzkChatSettings.ChatAvailableCondition.REAL_NAME); 92 | settings.setChatAvailableGroup(ChzzkChatSettings.ChatAvailableGroup.FOLLOWER); 93 | settings.setAllowSubscriberInFollowerMode(false); 94 | settings.setMinFollowerMinute(ChzzkChatSettings.MinFollowerMinute.M_60); 95 | settings.setChatSlowModeSec(ChzzkChatSettings.ChatSlowModeSec.S_120); 96 | settings.setChatEmojiMode(true); 97 | 98 | client.modifyChatSettings(settings).join(); 99 | 100 | var newSettings = client.fetchChatSettings().join(); 101 | 102 | Assertions.assertNotEquals(oldSettings, settings); 103 | Assertions.assertEquals(settings, newSettings); 104 | 105 | client.modifyChatSettings(oldSettings).join(); 106 | } catch (NotLoggedInException e) { 107 | throw new RuntimeException(e); 108 | } 109 | }); 110 | } 111 | 112 | @Test 113 | public void testAddRestriction() { 114 | Assertions.assertDoesNotThrow(() -> { 115 | var adapter = new ChzzkOauthLoginAdapter(5000); 116 | 117 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 118 | .withDebugMode() 119 | .withLoginAdapter(adapter) 120 | .build(); 121 | 122 | System.out.println(adapter.getAccountInterlockUrl(apiClientId, false)); 123 | 124 | client.loginAsync().join(); 125 | 126 | System.out.println(client.fetchLoggedUser()); 127 | 128 | client.restrictChannel("5f9e076713488378d70f870fd4971e32").get(); 129 | }); 130 | } 131 | 132 | @Test 133 | public void testFetchRestrictions() { 134 | Assertions.assertDoesNotThrow(() -> { 135 | var adapter = new ChzzkOauthLoginAdapter(5000); 136 | 137 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 138 | .withDebugMode() 139 | .withLoginAdapter(adapter) 140 | .build(); 141 | 142 | System.out.println(adapter.getAccountInterlockUrl(apiClientId, false)); 143 | 144 | client.loginAsync().join(); 145 | 146 | System.out.println(client.fetchLoggedUser()); 147 | 148 | System.out.println(client.fetchRestrictedChannels().get()); 149 | }); 150 | } 151 | 152 | @Test 153 | public void testRemoveRestriction() { 154 | Assertions.assertDoesNotThrow(() -> { 155 | var adapter = new ChzzkOauthLoginAdapter(5000); 156 | 157 | var client = new ChzzkClientBuilder(apiClientId, apiSecret) 158 | .withDebugMode() 159 | .withLoginAdapter(adapter) 160 | .build(); 161 | 162 | System.out.println(adapter.getAccountInterlockUrl(apiClientId, false)); 163 | 164 | client.loginAsync().join(); 165 | 166 | System.out.println(client.fetchLoggedUser()); 167 | 168 | client.unrestrictChannel("5f9e076713488378d70f870fd4971e32").get(); 169 | }); 170 | } 171 | 172 | } -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/types/channel/live/ChzzkLiveStatus.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.types.channel.live; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.util.List; 6 | import java.util.Objects; 7 | import java.util.Optional; 8 | 9 | public class ChzzkLiveStatus { 10 | 11 | protected String liveTitle; 12 | protected String status; 13 | protected int concurrentUserCount; 14 | protected int accumulateCount; 15 | protected boolean paidPromotion; 16 | protected boolean adult; 17 | protected boolean clipActive; 18 | protected String chatChannelId; 19 | protected List tags; 20 | protected String categoryType; 21 | protected String liveCategory; 22 | protected String liveCategoryValue; 23 | protected String livePollingStatusJson; 24 | protected Object faultStatus; 25 | protected Object userAdultStatus; 26 | protected boolean chatActive; 27 | protected String chatAvailableGroup; 28 | protected String chatAvailableCondition; 29 | protected int minFollowerMinute; 30 | protected boolean chatDonationRankingExposure; 31 | 32 | /** 33 | * Get title of the live stream. 34 | */ 35 | public @NotNull String getTitle() { 36 | return liveTitle; 37 | } 38 | 39 | /** 40 | * Get live stream is opened. 41 | */ 42 | public boolean isOnline() { 43 | return status.equalsIgnoreCase("open"); 44 | } 45 | 46 | /** 47 | * Get current number of viewers watching the live stream. 48 | */ 49 | public int getUserCount() { 50 | return concurrentUserCount; 51 | } 52 | 53 | /** 54 | * Get cumulative count of viewers. 55 | */ 56 | public int getAccumulateUserCount() { 57 | return accumulateCount; 58 | } 59 | 60 | /** 61 | * Indicates the presence of paid promotions. 62 | */ 63 | public boolean hasPaidPromotion() { 64 | return paidPromotion; 65 | } 66 | 67 | /** 68 | * Indicates whether the channel is adult-only. 69 | */ 70 | public boolean isNSFW() { 71 | return adult; 72 | } 73 | 74 | /** 75 | * Indicates whether clips are enabled. 76 | */ 77 | public boolean isClipActive() { 78 | return clipActive; 79 | } 80 | 81 | /** 82 | * Get unique ID number of the chat room. 83 | */ 84 | public @NotNull String getChatChannelId() { 85 | return chatChannelId; 86 | } 87 | 88 | /** 89 | * Get tags of the live stream. 90 | */ 91 | public @NotNull List getTags() { 92 | return List.copyOf(tags); 93 | } 94 | 95 | /** 96 | * Get main category of the broadcast. 97 | * Typically, it returns "GAME" for game broadcasts, "ETC" for others, 98 | * and "null" if no category is set. 99 | */ 100 | public @NotNull Optional getCategoryType() { 101 | return Optional.ofNullable(categoryType); 102 | } 103 | 104 | /** 105 | * Get subcategory of the live stream. 106 | */ 107 | public @NotNull Optional getLiveCategory() { 108 | return Optional.ofNullable(liveCategory); 109 | } 110 | 111 | /** 112 | * Get display name of the subcategory. 113 | */ 114 | public @NotNull String getLiveCategoryValue() { 115 | return liveCategoryValue; 116 | } 117 | 118 | /** 119 | * Get chat activation state of the live stream. 120 | */ 121 | public boolean isChatActive() { 122 | return chatActive; 123 | } 124 | 125 | /** 126 | * Get group of viewers who are allowed to send chat messages. 127 | */ 128 | public @NotNull String getChatAvailableGroup() { 129 | return chatAvailableGroup; 130 | } 131 | 132 | /** 133 | * Get conditions that viewers must meet to be able to send chat messages. 134 | */ 135 | public @NotNull String getChatAvailableCondition() { 136 | return chatAvailableCondition; 137 | } 138 | 139 | /** 140 | * Get minimum follow time required to send chat messages. 141 | */ 142 | public int getMinFollowerMinute() { 143 | return minFollowerMinute; 144 | } 145 | 146 | /** 147 | * Indicates whether the chat donation ranking is displayed. 148 | */ 149 | public boolean isChatDonationRankingExposure() { 150 | return chatDonationRankingExposure; 151 | } 152 | 153 | @Override 154 | public String toString() { 155 | return "ChzzkLiveStatusImpl{" + 156 | "liveTitle='" + liveTitle + '\'' + 157 | ", status='" + status + '\'' + 158 | ", concurrentUserCount=" + concurrentUserCount + 159 | ", accumulateCount=" + accumulateCount + 160 | ", paidPromotion=" + paidPromotion + 161 | ", adult=" + adult + 162 | ", clipActive=" + clipActive + 163 | ", chatChannelId='" + chatChannelId + '\'' + 164 | ", tags=" + tags + 165 | ", categoryType='" + categoryType + '\'' + 166 | ", liveCategory='" + liveCategory + '\'' + 167 | ", liveCategoryValue='" + liveCategoryValue + '\'' + 168 | ", livePollingStatusJson='" + livePollingStatusJson + '\'' + 169 | ", faultStatus=" + faultStatus + 170 | ", userAdultStatus=" + userAdultStatus + 171 | ", chatActive=" + chatActive + 172 | ", chatAvailableGroup='" + chatAvailableGroup + '\'' + 173 | ", chatAvailableCondition='" + chatAvailableCondition + '\'' + 174 | ", minFollowerMinute=" + minFollowerMinute + 175 | ", chatDonationRankingExposure=" + chatDonationRankingExposure + 176 | '}'; 177 | } 178 | 179 | @Override 180 | public boolean equals(Object o) { 181 | if (this == o) return true; 182 | if (o == null || getClass() != o.getClass()) return false; 183 | ChzzkLiveStatus that = (ChzzkLiveStatus) o; 184 | return concurrentUserCount == that.concurrentUserCount && accumulateCount == that.accumulateCount && paidPromotion == that.paidPromotion && adult == that.adult && clipActive == that.clipActive && chatActive == that.chatActive && minFollowerMinute == that.minFollowerMinute && chatDonationRankingExposure == that.chatDonationRankingExposure && Objects.equals(liveTitle, that.liveTitle) && Objects.equals(status, that.status) && Objects.equals(chatChannelId, that.chatChannelId) && Objects.equals(tags, that.tags) && Objects.equals(categoryType, that.categoryType) && Objects.equals(liveCategory, that.liveCategory) && Objects.equals(liveCategoryValue, that.liveCategoryValue) && Objects.equals(livePollingStatusJson, that.livePollingStatusJson) && Objects.equals(faultStatus, that.faultStatus) && Objects.equals(userAdultStatus, that.userAdultStatus) && Objects.equals(chatAvailableGroup, that.chatAvailableGroup) && Objects.equals(chatAvailableCondition, that.chatAvailableCondition); 185 | } 186 | 187 | @Override 188 | public int hashCode() { 189 | return Objects.hash(liveTitle, status, concurrentUserCount, accumulateCount, paidPromotion, adult, clipActive, chatChannelId, tags, categoryType, liveCategory, liveCategoryValue, livePollingStatusJson, faultStatus, userAdultStatus, chatActive, chatAvailableGroup, chatAvailableCondition, minFollowerMinute, chatDonationRankingExposure); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/auth/ChzzkOauthLoginAdapter.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.auth; 2 | 3 | import com.google.gson.Gson; 4 | import com.sun.net.httpserver.HttpServer; 5 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 6 | import xyz.r2turntrue.chzzk4j.auth.oauth.TokenRequestBody; 7 | import xyz.r2turntrue.chzzk4j.auth.oauth.TokenResponseBody; 8 | import xyz.r2turntrue.chzzk4j.util.HttpUtils; 9 | import xyz.r2turntrue.chzzk4j.util.RawApiUtils; 10 | 11 | import java.io.IOException; 12 | import java.io.OutputStream; 13 | import java.net.InetSocketAddress; 14 | import java.net.URLEncoder; 15 | import java.nio.charset.StandardCharsets; 16 | import java.util.Arrays; 17 | import java.util.Map; 18 | import java.util.Objects; 19 | import java.util.concurrent.CompletableFuture; 20 | import java.util.concurrent.CountDownLatch; 21 | import java.util.concurrent.ExecutorService; 22 | import java.util.concurrent.Executors; 23 | 24 | public class ChzzkOauthLoginAdapter implements ChzzkLoginAdapter { 25 | 26 | private String redirectHost = "localhost"; 27 | private int port = 8080; 28 | private String succeedPage = "Authorized! Back to the app!"; 29 | private String failedPage = "Authorization Failed! Re-try it!"; 30 | 31 | private String state; 32 | private String code; 33 | 34 | private int expiresIn; 35 | 36 | private String accToken; 37 | private String refToken; 38 | 39 | public String getOauthState() { 40 | return state; 41 | } 42 | 43 | public ChzzkOauthLoginAdapter() { 44 | } 45 | 46 | public ChzzkOauthLoginAdapter(int port) { 47 | this.port = port; 48 | } 49 | 50 | public ChzzkOauthLoginAdapter(String redirectHost) { 51 | this.redirectHost = redirectHost; 52 | } 53 | 54 | public ChzzkOauthLoginAdapter(String redirectHost, int port) { 55 | this.redirectHost = redirectHost; 56 | this.port = port; 57 | } 58 | 59 | public ChzzkOauthLoginAdapter(String redirectHost, int port, String succeedPage, String failedPage) { 60 | this.redirectHost = redirectHost; 61 | this.port = port; 62 | this.succeedPage = succeedPage; 63 | this.failedPage = failedPage; 64 | } 65 | 66 | @Override 67 | public CompletableFuture authorize(ChzzkClient client) { 68 | return CompletableFuture.supplyAsync(() -> { 69 | InetSocketAddress address = new InetSocketAddress(port); 70 | HttpServer httpServer = null; 71 | try { 72 | httpServer = HttpServer.create(address, 0); 73 | } catch (IOException e) { 74 | throw new RuntimeException(e); 75 | } 76 | 77 | CountDownLatch latch = new CountDownLatch(1); 78 | 79 | HttpServer finalHttpServer = httpServer; 80 | httpServer.createContext("/oauth_callback", (exchange) -> { 81 | 82 | try { 83 | Map params = HttpUtils.queryToMap(exchange.getRequestURI().getQuery()); 84 | 85 | if (params.containsKey("code") && params.containsKey("state")) { 86 | state = params.get("state"); 87 | code = params.get("code"); 88 | 89 | if (client.isDebug) System.out.println("Received the code"); 90 | 91 | var gson = new Gson(); 92 | 93 | var resp = RawApiUtils.getContentJson(client.getHttpClient(), RawApiUtils.httpPostRequest(ChzzkClient.OPENAPI_URL + "/auth/v1/token", 94 | gson.toJson(new TokenRequestBody( 95 | "authorization_code", 96 | client.getApiClientId(), 97 | client.getApiSecret(), 98 | code, 99 | state 100 | ))).build(), client.isDebug); 101 | 102 | var respBody = gson.fromJson(resp, TokenResponseBody.class); 103 | 104 | if (client.isDebug) { 105 | System.out.println("AccToken: " + respBody.accessToken()); 106 | System.out.println("RefToken: " + respBody.refreshToken()); 107 | System.out.println("ExpiresIn: " + respBody.expiresIn()); 108 | } 109 | 110 | accToken = respBody.accessToken(); 111 | refToken = respBody.refreshToken(); 112 | expiresIn = respBody.expiresIn(); 113 | 114 | if (accToken == null || refToken == null) { 115 | throw new Exception("access token or refresh token is null"); 116 | } 117 | 118 | HttpUtils.sendContent(exchange, succeedPage, 200); 119 | 120 | finalHttpServer.stop(1); 121 | latch.countDown(); 122 | } else { 123 | HttpUtils.sendContent(exchange, failedPage, 403); 124 | } 125 | } catch(Exception e) { 126 | if(client.isDebug) e.printStackTrace(); 127 | HttpUtils.sendContent(exchange, e.getMessage(), 403); 128 | } 129 | }); 130 | 131 | httpServer.start(); 132 | 133 | try { 134 | latch.await(); 135 | } catch (InterruptedException e) { 136 | throw new RuntimeException(e); 137 | } 138 | 139 | return new ChzzkLoginResult( 140 | null, 141 | null, 142 | accToken, 143 | refToken, 144 | expiresIn 145 | ); 146 | }); 147 | } 148 | 149 | public String getAccountInterlockUrl(String clientId, boolean redirectToHttps) { 150 | return getAccountInterlockUrl(clientId, redirectToHttps, "dummy"); 151 | } 152 | 153 | public String getAccountInterlockUrl(String clientId, boolean redirectToHttps, String state) { 154 | return "https://chzzk.naver.com/account-interlock" + 155 | "?clientId=" + 156 | clientId + 157 | "&redirectUri=" + 158 | URLEncoder.encode( 159 | (redirectToHttps ? "https://" : "http://") + 160 | redirectHost + 161 | ":" + 162 | port + 163 | "/oauth_callback", 164 | StandardCharsets.UTF_8 165 | ) + 166 | "&state=" + 167 | URLEncoder.encode( 168 | state, 169 | StandardCharsets.UTF_8 170 | ); 171 | } 172 | 173 | @Override 174 | public boolean equals(Object o) { 175 | if (this == o) return true; 176 | if (o == null || getClass() != o.getClass()) return false; 177 | ChzzkOauthLoginAdapter that = (ChzzkOauthLoginAdapter) o; 178 | return port == that.port && Objects.equals(redirectHost, that.redirectHost) && Objects.equals(succeedPage, that.succeedPage) && Objects.equals(failedPage, that.failedPage) && Objects.equals(state, that.state) && Objects.equals(code, that.code) && Objects.equals(accToken, that.accToken) && Objects.equals(refToken, that.refToken); 179 | } 180 | 181 | @Override 182 | public int hashCode() { 183 | return Objects.hash(redirectHost, port, succeedPage, failedPage, state, code, accToken, refToken); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/ChzzkChat.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | import com.google.gson.JsonElement; 4 | import xyz.r2turntrue.chzzk4j.ChzzkClient; 5 | import xyz.r2turntrue.chzzk4j.chat.event.ChzzkEvent; 6 | import xyz.r2turntrue.chzzk4j.exception.NotLoggedInException; 7 | import xyz.r2turntrue.chzzk4j.types.ChzzkUser; 8 | import xyz.r2turntrue.chzzk4j.util.RawApiUtils; 9 | 10 | import java.io.IOException; 11 | import java.net.URI; 12 | import java.nio.channels.AlreadyConnectedException; 13 | import java.util.ArrayList; 14 | import java.util.HashMap; 15 | import java.util.concurrent.Callable; 16 | import java.util.concurrent.CompletableFuture; 17 | import java.util.concurrent.ExecutionException; 18 | import java.util.function.Consumer; 19 | 20 | public class ChzzkChat { 21 | boolean reconnecting; 22 | ChzzkClient chzzk; 23 | 24 | boolean isConnectedToWebsocket = false; 25 | private ChatWebsocketClient client; 26 | 27 | HashMap, ArrayList>> handlerMap = new HashMap<>(); 28 | 29 | String accessToken; 30 | String userId; 31 | String channelId; 32 | String chatId; 33 | 34 | boolean autoReconnect = false; 35 | 36 | public boolean isConnectedToChat() { 37 | return isConnectedToWebsocket; 38 | } 39 | 40 | public boolean shouldAutoReconnect() { 41 | return autoReconnect; 42 | } 43 | 44 | public String getChatId() { 45 | return chatId; 46 | } 47 | 48 | public String getChannelId() { 49 | return channelId; 50 | } 51 | 52 | ChzzkChat(ChzzkClient chzzk, String channelId, boolean autoReconnect) { 53 | this.chzzk = chzzk; 54 | this.channelId = channelId; 55 | this.autoReconnect = autoReconnect; 56 | } 57 | 58 | public void on(Class clazz, Consumer action) { 59 | if (!handlerMap.containsKey(clazz)) { 60 | handlerMap.put(clazz, new ArrayList<>()); 61 | } 62 | 63 | handlerMap.get(clazz).add(action); 64 | } 65 | 66 | public void emit(Class clazz, T obj) { 67 | if (handlerMap.containsKey(clazz)) { 68 | for (Consumer handler : handlerMap.get(clazz)) { 69 | @SuppressWarnings("unchecked") 70 | Consumer specificHandler = (Consumer) handler; 71 | specificHandler.accept(obj); 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * Connects to the chat. This method doesn't block. 78 | */ 79 | public CompletableFuture connectAsync() { 80 | return connectFromChannelId(channelId, autoReconnect); 81 | } 82 | 83 | /** 84 | * Connects to the chat. This method blocks. 85 | */ 86 | public void connectBlocking() throws ExecutionException, InterruptedException { 87 | connectFromChannelId(channelId, autoReconnect).get(); 88 | } 89 | 90 | /** 91 | * @deprecated current not to be used because of some bugs; will be fixed someday? 92 | * @param chatCount 93 | */ 94 | @Deprecated 95 | public void requestRecentChat(int chatCount) { 96 | if (!isConnectedToWebsocket) { 97 | throw new IllegalStateException("Connect to request recent chats!"); 98 | } 99 | 100 | client.requestRecentChat(chatCount); 101 | } 102 | 103 | public void sendChat(String content) { 104 | if (!isConnectedToWebsocket) { 105 | throw new IllegalStateException("Connect to send chat!"); 106 | } 107 | 108 | client.sendChat(content); 109 | } 110 | 111 | /** 112 | * Connect to chatting by the channel id 113 | * 114 | * @param channelId channel id to connect. 115 | * @param autoReconnect should reconnect automatically when disconnected by the server. 116 | * @throws IOException when failed to connect to the chat 117 | * @throws UnsupportedOperationException when failed to fetch chatChannelId! (Try to put NID_SES/NID_AUT when create {@link ChzzkClient}, because it's mostly caused by age restriction) 118 | */ 119 | private CompletableFuture connectFromChannelId(String channelId, boolean autoReconnect) { 120 | return CompletableFuture.runAsync(() -> { 121 | try { 122 | JsonElement chatIdRaw = RawApiUtils.getContentJson(chzzk.getHttpClient(), 123 | RawApiUtils.httpGetRequest(ChzzkClient.API_URL + "/service/v3/channels/" + channelId + "/live-detail").build(), chzzk.isDebug) 124 | .getAsJsonObject() 125 | .get("chatChannelId"); 126 | 127 | if (chatIdRaw.isJsonNull()) { 128 | throw new UnsupportedOperationException("Failed to fetch chatChannelId! (Try to put NID_SES/NID_AUT, because it's mostly caused by age restriction)"); 129 | } 130 | 131 | connectFromChatId(channelId, chatIdRaw.getAsString(), autoReconnect).get(); 132 | } catch (IOException | ExecutionException | InterruptedException e) { 133 | throw new RuntimeException(e); 134 | } 135 | }); 136 | } 137 | 138 | private CompletableFuture connectFromChatId(String channelId, String chatId, boolean autoReconnect) { 139 | return CompletableFuture.runAsync(() -> { 140 | try { 141 | if (isConnectedToWebsocket) { 142 | throw new AlreadyConnectedException(); 143 | } 144 | 145 | reconnecting = false; 146 | 147 | this.autoReconnect = autoReconnect; 148 | 149 | isConnectedToWebsocket = true; 150 | 151 | this.channelId = channelId; 152 | this.chatId = chatId; 153 | 154 | userId = ""; 155 | try { 156 | ChzzkUser user = chzzk.fetchLoggedUser().get(); 157 | userId = user.getUserId(); 158 | } catch (NotLoggedInException e) { 159 | } catch (ExecutionException e) { 160 | throw new RuntimeException(e); 161 | } 162 | 163 | String accessTokenUrl = ChzzkClient.GAME_API_URL + 164 | "/v1/chats/access-token?channelId=" + chatId + 165 | "&chatType=STREAMING"; 166 | accessToken = RawApiUtils.getContentJson( 167 | chzzk.getHttpClient(), 168 | RawApiUtils.httpGetRequest(accessTokenUrl).build(), 169 | chzzk.isDebug 170 | ).getAsJsonObject().get("accessToken").getAsString(); 171 | 172 | int serverId = 0; 173 | 174 | for (char i : channelId.toCharArray()) { 175 | serverId += Character.getNumericValue(i); 176 | } 177 | 178 | serverId = Math.abs(serverId) % 9 + 1; 179 | 180 | client = new ChatWebsocketClient(this, 181 | URI.create("wss://kr-ss" + serverId + ".chat.naver.com/chat")); 182 | 183 | client.connectBlocking(); 184 | } catch (IOException | InterruptedException e) { 185 | throw new RuntimeException(e); 186 | } 187 | }); 188 | } 189 | 190 | public CompletableFuture reconnectAsync() { 191 | return CompletableFuture.runAsync(() -> { 192 | try { 193 | if (client == null) { 194 | throw new IllegalStateException("Client not initalized to reconnect!"); 195 | } 196 | 197 | URI chatUri = client.getURI(); 198 | 199 | if (!client.isClosed() && !client.isClosing()) { 200 | try { 201 | client.closeBlocking(); 202 | } catch (InterruptedException e) { 203 | throw new RuntimeException(e); 204 | } 205 | } 206 | 207 | reconnecting = true; 208 | 209 | client.reconnectBlocking(); 210 | } catch (InterruptedException e) { 211 | throw new RuntimeException(e); 212 | } 213 | }); 214 | } 215 | 216 | public void reconnectSync() throws ExecutionException, InterruptedException { 217 | reconnectAsync().get(); 218 | } 219 | 220 | public CompletableFuture closeAsync() { 221 | return CompletableFuture.runAsync(() -> { 222 | try { 223 | if (!isConnectedToWebsocket) { 224 | throw new IllegalStateException("Not connected!"); 225 | } 226 | 227 | client.closeBlocking(); 228 | isConnectedToWebsocket = false; 229 | } catch (InterruptedException e) { 230 | throw new RuntimeException(e); 231 | } 232 | }); 233 | } 234 | 235 | public void closeBlocking() throws ExecutionException, InterruptedException { 236 | closeAsync().get(); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/main/java/xyz/r2turntrue/chzzk4j/chat/ChatMessage.java: -------------------------------------------------------------------------------- 1 | package xyz.r2turntrue.chzzk4j.chat; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | import java.util.Arrays; 6 | import java.util.Date; 7 | import java.util.Objects; 8 | 9 | public class ChatMessage { 10 | public enum OsType 11 | { 12 | PC, 13 | AOS, 14 | IOS 15 | } 16 | 17 | public static class Extras { 18 | // todo: emoji parsing implementation 19 | //public Emoji[] emojis; 20 | 21 | String donationType; 22 | String osType; 23 | 24 | int payAmount = -1; 25 | 26 | // Subscription 27 | int month = 0; 28 | String tierName = ""; 29 | 30 | // Mission 31 | int durationTime; 32 | String missionDonationId; 33 | String missionCreatedTime; 34 | String missionEndTime; 35 | String missionText; 36 | String status; 37 | boolean success; 38 | 39 | public OsType getOsType() { 40 | return OsType.valueOf(osType); 41 | } 42 | 43 | public int getPayAmount() { 44 | return payAmount; 45 | } 46 | 47 | //public int getSubscriptionMonth() { 48 | // return month; 49 | //} 50 | 51 | //public String getSubscriptionTierName() { 52 | // return tierName; 53 | //} 54 | 55 | @Override 56 | public String toString() { 57 | return "Extras{" + 58 | "osType='" + osType + '\'' + 59 | ", payAmount=" + payAmount + '\'' + 60 | ", month=" + month + '\'' + 61 | ", tierName='" + tierName + 62 | '}'; 63 | } 64 | } 65 | 66 | public static class Profile { 67 | String nickname; 68 | String profileImageUrl; 69 | String userRoleCode; 70 | boolean verifiedMark; 71 | 72 | ActivityBadge[] activityBadges; 73 | StreamingProperty streamingProperty; 74 | 75 | public static class StreamingProperty { 76 | Subscription subscription; 77 | 78 | public static class Subscription { 79 | int accmulativeMonth; 80 | int tier; 81 | PartialBadge badge; 82 | 83 | public int getAccmulativeMonth() { 84 | return accmulativeMonth; 85 | } 86 | 87 | public int getTier() { 88 | return tier; 89 | } 90 | 91 | public PartialBadge getBadge() { 92 | return badge; 93 | } 94 | 95 | @Override 96 | public boolean equals(Object o) { 97 | if (this == o) return true; 98 | if (o == null || getClass() != o.getClass()) return false; 99 | Subscription that = (Subscription) o; 100 | return accmulativeMonth == that.accmulativeMonth && tier == that.tier && Objects.equals(badge, that.badge); 101 | } 102 | 103 | @Override 104 | public int hashCode() { 105 | return Objects.hash(accmulativeMonth, tier, badge); 106 | } 107 | 108 | @Override 109 | public String toString() { 110 | return "Subscription{" + 111 | "accmulativeMonth=" + accmulativeMonth + 112 | ", tier=" + tier + 113 | ", badge=" + badge + 114 | '}'; 115 | } 116 | } 117 | 118 | @Override 119 | public String toString() { 120 | return "StreamingProperty{" + 121 | "subscription=" + subscription + 122 | '}'; 123 | } 124 | } 125 | 126 | public static class PartialBadge { 127 | String imageUrl; 128 | 129 | public String getImageUrl() { 130 | return imageUrl; 131 | } 132 | 133 | @Override 134 | public boolean equals(Object o) { 135 | if (this == o) return true; 136 | if (o == null || getClass() != o.getClass()) return false; 137 | PartialBadge that = (PartialBadge) o; 138 | return Objects.equals(imageUrl, that.imageUrl); 139 | } 140 | 141 | @Override 142 | public int hashCode() { 143 | return Objects.hashCode(imageUrl); 144 | } 145 | 146 | @Override 147 | public String toString() { 148 | return "PartialBadge{" + 149 | "imageUrl='" + imageUrl + '\'' + 150 | '}'; 151 | } 152 | } 153 | 154 | public static class ActivityBadge extends PartialBadge { 155 | int badgeNo; 156 | String badgeId; 157 | boolean activated; 158 | 159 | public int getBadgeNo() { 160 | return badgeNo; 161 | } 162 | 163 | public String getBadgeId() { 164 | return badgeId; 165 | } 166 | 167 | public boolean isActivated() { 168 | return activated; 169 | } 170 | 171 | @Override 172 | public boolean equals(Object o) { 173 | if (this == o) return true; 174 | if (o == null || getClass() != o.getClass()) return false; 175 | ActivityBadge that = (ActivityBadge) o; 176 | return badgeNo == that.badgeNo && activated == that.activated && Objects.equals(badgeId, that.badgeId) && Objects.equals(imageUrl, that.imageUrl); 177 | } 178 | 179 | @Override 180 | public int hashCode() { 181 | return Objects.hash(badgeNo, badgeId, imageUrl, activated); 182 | } 183 | 184 | @Override 185 | public String toString() { 186 | return "ActivityBadge{" + 187 | "badgeNo=" + badgeNo + 188 | ", badgeId='" + badgeId + '\'' + 189 | ", imageUrl='" + imageUrl + '\'' + 190 | ", activated=" + activated + 191 | '}'; 192 | } 193 | } 194 | 195 | public String getNickname() { 196 | return nickname; 197 | } 198 | 199 | public String getProfileImageUrl() { 200 | return profileImageUrl; 201 | } 202 | 203 | public String getUserRoleCode() { 204 | return userRoleCode; 205 | } 206 | 207 | public boolean isVerifiedMark() { 208 | return verifiedMark; 209 | } 210 | 211 | public ActivityBadge[] getActivityBadges() { 212 | return activityBadges; 213 | } 214 | 215 | @Nullable 216 | public StreamingProperty.Subscription getSubscription() { 217 | return streamingProperty.subscription; 218 | } 219 | 220 | public boolean hasSubscription() { 221 | return streamingProperty.subscription != null; 222 | } 223 | 224 | @Override 225 | public String toString() { 226 | return "Profile{" + 227 | "nickname='" + nickname + '\'' + 228 | ", profileImageUrl='" + profileImageUrl + '\'' + 229 | ", userRoleCode='" + userRoleCode + '\'' + 230 | ", verifiedMark=" + verifiedMark + 231 | ", activityBadges=" + Arrays.toString(activityBadges) + 232 | ", streamingProperty=" + streamingProperty + 233 | '}'; 234 | } 235 | } 236 | 237 | int msgTypeCode = 0; 238 | String userId; 239 | String content; 240 | Date createTime; 241 | Extras extras = new Extras(); 242 | Profile profile = new Profile(); 243 | 244 | String rawJson; 245 | 246 | public String getRawJson() { return rawJson; } 247 | 248 | public int getChatTypeCode() { 249 | return msgTypeCode; 250 | } 251 | 252 | public String getUserId() { 253 | return userId; 254 | } 255 | 256 | public String getContent() { 257 | return content; 258 | } 259 | 260 | public Date getCreateTime() { 261 | return createTime; 262 | } 263 | 264 | public Extras getExtras() { 265 | return extras; 266 | } 267 | 268 | /** 269 | * Returns profile of sender of the message. 270 | * @return nullable {@link Profile} 271 | */ 272 | @Nullable 273 | public Profile getProfile() { 274 | return profile; 275 | } 276 | 277 | public boolean hasProfile() { 278 | return profile != null; 279 | } 280 | 281 | @Override 282 | public String toString() { 283 | return "ChatMessage{" + 284 | "userId='" + userId + '\'' + 285 | ", msgTypeCode='" + msgTypeCode + '\'' + 286 | ", content='" + content + '\'' + 287 | ", createTime=" + createTime + 288 | ", extras=" + extras + 289 | ", profile=" + profile + 290 | '}'; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | --------------------------------------------------------------------------------