From 83ca1dcf689d835f867392256fc3626439ffce31 Mon Sep 17 00:00:00 2001 From: NetRiceCake Date: Wed, 26 Nov 2025 21:52:59 +0900 Subject: [PATCH] Many changes --- .gitignore | 4 +- .idea/vcs.xml | 6 - README.md | 67 +++++- dependency-reduced-pom.xml | 56 ----- ex.png | Bin 0 -> 39548 bytes pom.xml | 3 +- .../github/netricecake/KakaoTalkClient.java | 89 ------- .../java/com/github/netricecake/Main.java | 69 +++++- .../netricecake/{ => kakao}/KakaoApi.java | 74 ++++-- .../netricecake/kakao/KakaoDefaultValues.java | 9 - .../kakao/LocoSocketHandlerImpl.java | 89 +++++++ .../github/netricecake/kakao/TalkClient.java | 222 ++++++++++++++++++ .../github/netricecake/kakao/TalkHandler.java | 27 +++ .../netricecake/kakao/structs/ChatRoom.java | 20 ++ .../netricecake/kakao/structs/Member.java | 17 ++ .../netricecake/kakao/structs/Message.java | 29 +++ .../{network => loco}/LocoPacket.java | 21 +- .../netricecake/loco/LocoSocektHandler.java | 13 + .../github/netricecake/loco/LocoSocket.java | 139 +++++++++++ .../{network => loco}/codec/LocoCodec.java | 26 +- .../codec/SecureLayerCodec.java | 6 +- .../{ => loco}/crypto/CryptoManager.java | 9 +- .../loco/packet/inbound/ChatInfoIn.java | 22 ++ .../loco/packet/inbound/CheckInIn.java | 52 ++++ .../loco/packet/inbound/DelMemIn.java | 25 ++ .../loco/packet/inbound/GetConfIn.java | 24 ++ .../loco/packet/inbound/InfoLinkIn.java | 17 ++ .../loco/packet/inbound/LoginListIn.java | 65 +++++ .../loco/packet/inbound/MessageIn.java | 41 ++++ .../loco/packet/inbound/NewMemIn.java | 25 ++ .../loco/packet/inbound/PingIn.java | 4 + .../loco/packet/inbound/WriteIn.java | 17 ++ .../loco/packet/outbound/ChatInfoOut.java | 20 ++ .../loco/packet/outbound/CheckInOut.java | 45 ++++ .../packet/outbound/GetConfOut.java} | 18 +- .../loco/packet/outbound/InfoLinkOut.java | 23 ++ .../loco/packet/outbound/LoginListOut.java | 79 +++++++ .../loco/packet/outbound/MessageOut.java | 11 + .../loco/packet/outbound/PingOut.java | 11 + .../packet/outbound/WriteOut.java} | 21 +- .../netricecake/{ => loco}/util/BsonUtil.java | 2 +- .../netricecake/{ => loco}/util/ByteUtil.java | 2 +- .../netricecake/message/LocoRequest.java | 9 - .../netricecake/message/LocoResponse.java | 9 - .../message/request/CheckInRequest.java | 44 ---- .../message/request/LoginListRequest.java | 79 ------- .../message/request/MessageRequest.java | 16 -- .../message/request/PingRequest.java | 18 -- .../message/request/SetStatusRequest.java | 34 --- .../message/response/CheckInResponse.java | 54 ----- .../message/response/GetConfResponse.java | 26 -- .../message/response/LoginListResponse.java | 25 -- .../message/response/MessageResponse.java | 30 --- .../message/response/PingResponse.java | 23 -- .../message/response/SetStatusResponse.java | 24 -- .../network/LocoPacketHandler.java | 6 - .../netricecake/network/LocoSocket.java | 92 -------- src/main/resources/META-INF/MANIFEST.MF | 3 - test.png | Bin 3873 -> 0 bytes 59 files changed, 1269 insertions(+), 742 deletions(-) delete mode 100644 .idea/vcs.xml delete mode 100644 dependency-reduced-pom.xml create mode 100644 ex.png delete mode 100644 src/main/java/com/github/netricecake/KakaoTalkClient.java rename src/main/java/com/github/netricecake/{ => kakao}/KakaoApi.java (77%) delete mode 100644 src/main/java/com/github/netricecake/kakao/KakaoDefaultValues.java create mode 100644 src/main/java/com/github/netricecake/kakao/LocoSocketHandlerImpl.java create mode 100644 src/main/java/com/github/netricecake/kakao/TalkClient.java create mode 100644 src/main/java/com/github/netricecake/kakao/TalkHandler.java create mode 100644 src/main/java/com/github/netricecake/kakao/structs/ChatRoom.java create mode 100644 src/main/java/com/github/netricecake/kakao/structs/Member.java create mode 100644 src/main/java/com/github/netricecake/kakao/structs/Message.java rename src/main/java/com/github/netricecake/{network => loco}/LocoPacket.java (73%) create mode 100644 src/main/java/com/github/netricecake/loco/LocoSocektHandler.java create mode 100644 src/main/java/com/github/netricecake/loco/LocoSocket.java rename src/main/java/com/github/netricecake/{network => loco}/codec/LocoCodec.java (72%) rename src/main/java/com/github/netricecake/{network => loco}/codec/SecureLayerCodec.java (91%) rename src/main/java/com/github/netricecake/{ => loco}/crypto/CryptoManager.java (94%) create mode 100644 src/main/java/com/github/netricecake/loco/packet/inbound/ChatInfoIn.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/inbound/CheckInIn.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/inbound/DelMemIn.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/inbound/GetConfIn.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/inbound/InfoLinkIn.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/inbound/LoginListIn.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/inbound/MessageIn.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/inbound/NewMemIn.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/inbound/PingIn.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/inbound/WriteIn.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/outbound/ChatInfoOut.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/outbound/CheckInOut.java rename src/main/java/com/github/netricecake/{message/request/GetConfRequest.java => loco/packet/outbound/GetConfOut.java} (55%) create mode 100644 src/main/java/com/github/netricecake/loco/packet/outbound/InfoLinkOut.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/outbound/LoginListOut.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/outbound/MessageOut.java create mode 100644 src/main/java/com/github/netricecake/loco/packet/outbound/PingOut.java rename src/main/java/com/github/netricecake/{message/request/WriteRequest.java => loco/packet/outbound/WriteOut.java} (64%) rename src/main/java/com/github/netricecake/{ => loco}/util/BsonUtil.java (96%) rename src/main/java/com/github/netricecake/{ => loco}/util/ByteUtil.java (97%) delete mode 100644 src/main/java/com/github/netricecake/message/LocoRequest.java delete mode 100644 src/main/java/com/github/netricecake/message/LocoResponse.java delete mode 100644 src/main/java/com/github/netricecake/message/request/CheckInRequest.java delete mode 100644 src/main/java/com/github/netricecake/message/request/LoginListRequest.java delete mode 100644 src/main/java/com/github/netricecake/message/request/MessageRequest.java delete mode 100644 src/main/java/com/github/netricecake/message/request/PingRequest.java delete mode 100644 src/main/java/com/github/netricecake/message/request/SetStatusRequest.java delete mode 100644 src/main/java/com/github/netricecake/message/response/CheckInResponse.java delete mode 100644 src/main/java/com/github/netricecake/message/response/GetConfResponse.java delete mode 100644 src/main/java/com/github/netricecake/message/response/LoginListResponse.java delete mode 100644 src/main/java/com/github/netricecake/message/response/MessageResponse.java delete mode 100644 src/main/java/com/github/netricecake/message/response/PingResponse.java delete mode 100644 src/main/java/com/github/netricecake/message/response/SetStatusResponse.java delete mode 100644 src/main/java/com/github/netricecake/network/LocoPacketHandler.java delete mode 100644 src/main/java/com/github/netricecake/network/LocoSocket.java delete mode 100644 src/main/resources/META-INF/MANIFEST.MF delete mode 100644 test.png diff --git a/.gitignore b/.gitignore index af665ab..935d2bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ target/ !.mvn/wrapper/maven-wrapper.jar +.mvn !**/src/main/**/target/ !**/src/test/**/target/ +.kotlin ### IntelliJ IDEA ### .idea/modules.xml @@ -35,4 +37,4 @@ build/ .vscode/ ### Mac OS ### -.DS_Store +.DS_Store \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 97f1bc8..135000d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,66 @@ -# Loco wrapper +# loco-wrapper -테스트 해볼거면 본계정 쓰지마세요. 영정먹을 수 있음 +안드로이드 카카오톡 25.9.2 기반 비공식 카카오톡 클라이언트 (태블릿 서브 디바이스 로그인) -![test](./test.png) +현재 오픈 채팅방에서만 작동합니다. + +절대 본계정으로 돌려보지마세요. + +## Example + +![ex](./ex.png) + +Main.java 파일 참고 +```java +TalkClient client = new TalkClient(email, password, deviceName, deviceUuid, new TalkHandler() { + @Override + public void onMessage(Message msg) { + if (msg.getType() != 1) return; // 1이 그냥 채팅, 그냥 채팅만 받기 + if (msg.getMessage().equals("!send")) { + getTalkClient().sendMessage(msg.getChatRoom(), "test"); + } + else if (msg.getMessage().equals("!reply")) { // 답장 + int replyType = 26; // 답장 타입 + JsonObject extraObject = new JsonObject(); + extraObject.addProperty("src_logId", msg.getLogId()); + extraObject.addProperty("src_userId", msg.getAuthor().getId()); + extraObject.addProperty("src_message", msg.getMessage()); + extraObject.addProperty("src_type", msg.getType()); + extraObject.addProperty("src_linkId", msg.getChatRoom().getLinkId()); + getTalkClient().sendMessage(msg.getChatRoom(), replyType, "reply test", extraObject.toString()); + } + else if (msg.getMessage().equals("!mention")) { // 멘션 + JsonObject extraObject = new JsonObject(); + JsonArray mentionArray = new JsonArray(); + JsonObject mentionObject = new JsonObject(); + mentionObject.addProperty("user_id", msg.getAuthor().getId()); + JsonArray pos = new JsonArray(); + pos.add(1); + mentionObject.add("at", pos); + mentionObject.addProperty("len", msg.getAuthor().getName().length()); + mentionArray.add(mentionObject); + extraObject.add("mentions", mentionArray); + getTalkClient().sendMessage(msg.getChatRoom(), 1, "@" + msg.getAuthor().getName(), extraObject.toString()); + } + } + + @Override + public void onNewMember(ChatRoom room, Member member) { + getTalkClient().sendMessage(room, member.getName() + "님이 들어왔습니다."); + } + + @Override + public void onDelMember(ChatRoom room, Member member) { + getTalkClient().sendMessage(room, member.getName() + "님이 나갔습니다."); + } + }); +client.connect(); +``` + +## Usage + +첫 로그인시에 기기등록이 필요합니다. 콘솔창에 방법 나오니 따라하세요. + +로그인하면 로그인 정보(토큰 등)가 email_deviceName 폴더 안에 저장됩니다. 서버 연결이 안되면 삭제하고 시도하세요. + +**device uuid 무조건 바꾸시오.** diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml deleted file mode 100644 index 16def58..0000000 --- a/dependency-reduced-pom.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - 4.0.0 - com.github.netricecake - loco-wrapper - 1.0-SNAPSHOT - - - - maven-shade-plugin - 3.4.1 - - - package - - shade - - - - - com.github.netricecake.Main - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - - - - - - - com.squareup.okhttp3 - okhttp-bom - 5.2.0 - pom - import - - - - - 21 - 21 - UTF-8 - - diff --git a/ex.png b/ex.png new file mode 100644 index 0000000000000000000000000000000000000000..e822ddbef22fdd7996c001a7eaf47f58aeec685a GIT binary patch literal 39548 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%0As3gw`u|eWXGdE)D^g{rIuATITIXn&+QC95v# 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.projectlombok lombok 1.18.42 + provided org.mongodb @@ -88,4 +89,4 @@ - + \ No newline at end of file diff --git a/src/main/java/com/github/netricecake/KakaoTalkClient.java b/src/main/java/com/github/netricecake/KakaoTalkClient.java deleted file mode 100644 index d9d33b1..0000000 --- a/src/main/java/com/github/netricecake/KakaoTalkClient.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.github.netricecake; - -import com.github.netricecake.kakao.KakaoDefaultValues; -import com.github.netricecake.message.request.*; -import com.github.netricecake.message.response.CheckInResponse; -import com.github.netricecake.message.response.GetConfResponse; -import com.github.netricecake.message.response.MessageResponse; -import com.github.netricecake.network.LocoPacket; -import com.github.netricecake.network.LocoSocket; -import com.github.netricecake.util.BsonUtil; - -import java.io.IOException; -import java.security.InvalidParameterException; -import java.util.Map; -import java.util.concurrent.CompletableFuture; - -public class KakaoTalkClient { - - private final static String DEFAULT_MCCMNC = KakaoDefaultValues.MCCMNC; - - private KakaoApi.LoginData loginData; - private GetConfResponse bookingData; - private CheckInResponse checkInData; - - LocoSocket socket; - - public void login(String email, String password, String deviceName, String deviceUuid) throws Exception, IOException, InvalidParameterException, IllegalStateException { - try { - loginData = KakaoApi.loginRequest(email, password, deviceName, deviceUuid); - } catch (IllegalStateException e) { - Map.Entry registerInfo = KakaoApi.generatePasscode(email, password, deviceName, deviceUuid); - System.out.println("디바이스 등록이 필요합니다."); - System.out.println("카카오톡에서 " + registerInfo.getValue() + "초 안에 코드를 입력해주세요. 코드 : " + registerInfo.getKey()); - boolean registerResult = KakaoApi.registerDevice(email, password, deviceUuid); - if (!registerResult) throw new IllegalStateException("기기 등록 실패"); - System.out.println("기기 등록 성공"); - loginData = KakaoApi.loginRequest(email, password, deviceName, deviceUuid); - } - System.out.println("로그인 성공"); - - bookingData = KakaoApi.getBookingData(DEFAULT_MCCMNC, loginData.userId); - - LocoSocket checkInSocket = new LocoSocket(bookingData.getAddr(), bookingData.getPort()); - - try { - CheckInRequest checkInRequest = new CheckInRequest(); - checkInRequest.setUserId(loginData.userId); - byte[] body = checkInRequest.toBson(); - checkInSocket.connect(); - checkInSocket.write(new LocoPacket(1001, checkInRequest.getMethod(), body)); - checkInData = new CheckInResponse(); - checkInData.fromBson(checkInSocket.read().getBody()); - checkInSocket.close(); - } catch (Exception e) { - System.out.println("오류 : " + e.getMessage()); - } - - socket = new LocoSocket(checkInData.getHost(), checkInData.getPort()); - socket.connect(); - LoginListRequest req = new LoginListRequest(); - req.setDuuid(deviceUuid); - req.setOauthToken(loginData.accessToken); - socket.write(new LocoPacket(1002, "LOGINLIST", req.toBson())); - } - - public void test() throws Exception { - while(true) { - LocoPacket packet = socket.read(); - if (packet == null) continue; - if (!packet.getMethod().equals("MSG")) continue; - - socket.write(new LocoPacket(packet.getPacketId(), "MSG", new MessageRequest().toBson())); - MessageResponse res = new MessageResponse(); - res.fromBson(packet.getBody()); - if (res.getType() != 1) return; - if (res.getMessage().trim().equals("!!test")) { - WriteRequest req = new WriteRequest(); - req.setChatId(res.getChatId()); - req.setMessage("test!!"); - socket.write(new LocoPacket(1003, "WRITE", req.toBson())); - LocoPacket t = socket.read(); - System.out.println(BsonUtil.bsonToJson(t.getBody())); - System.exit(0); - } - - } - } - -} diff --git a/src/main/java/com/github/netricecake/Main.java b/src/main/java/com/github/netricecake/Main.java index 63b0821..5d7e8e6 100644 --- a/src/main/java/com/github/netricecake/Main.java +++ b/src/main/java/com/github/netricecake/Main.java @@ -1,22 +1,65 @@ package com.github.netricecake; -import com.github.netricecake.util.ByteUtil; - -import java.security.SecureRandom; +import com.github.netricecake.kakao.TalkClient; +import com.github.netricecake.kakao.TalkHandler; +import com.github.netricecake.kakao.structs.ChatRoom; +import com.github.netricecake.kakao.structs.Member; +import com.github.netricecake.kakao.structs.Message; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +//TIP 코드를 실행하려면 을(를) 누르거나 +// 에디터 여백에 있는 아이콘을 클릭하세요. public class Main { - static String EMAIL = ""; - static String PASSWORD = ""; - static String DEVICE_NAME = "SM-X930"; // 갤럭시 탭 s11 울트라 + static String email = "invalid@example.com"; + static String password = "example"; + static String deviceName = "SM-X930"; // 갤럭시 탭 s11 울트라, 지원되는 태블릿 모델명 넣으세요 + static String deviceUuid = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; // 64자 랜덤 hex-string public static void main(String[] args) throws Exception { - byte[] uuid = new byte[32]; - new SecureRandom().nextBytes(uuid); - String deviceUuid = ByteUtil.byteArrayToHexString(uuid); - KakaoTalkClient client = new KakaoTalkClient(); - client.login(EMAIL, PASSWORD, DEVICE_NAME, deviceUuid); - client.test(); - } + TalkClient client = new TalkClient(email, password, deviceName, deviceUuid, new TalkHandler() { + @Override + public void onMessage(Message msg) { + if (msg.getType() != 1) return; // 1이 그냥 채팅, 그냥 채팅만 받기 + if (msg.getMessage().equals("!send")) { + getTalkClient().sendMessage(msg.getChatRoom(), "test"); + } + else if (msg.getMessage().equals("!reply")) { // 답장 + int replyType = 26; // 답장 타입 + JsonObject extraObject = new JsonObject(); + extraObject.addProperty("src_logId", msg.getLogId()); + extraObject.addProperty("src_userId", msg.getAuthor().getId()); + extraObject.addProperty("src_message", msg.getMessage()); + extraObject.addProperty("src_type", msg.getType()); + extraObject.addProperty("src_linkId", msg.getChatRoom().getLinkId()); + getTalkClient().sendMessage(msg.getChatRoom(), replyType, "reply test", extraObject.toString()); + } + else if (msg.getMessage().equals("!mention")) { // 멘션 + JsonObject extraObject = new JsonObject(); + JsonArray mentionArray = new JsonArray(); + JsonObject mentionObject = new JsonObject(); + mentionObject.addProperty("user_id", msg.getAuthor().getId()); + JsonArray pos = new JsonArray(); + pos.add(1); + mentionObject.add("at", pos); + mentionObject.addProperty("len", msg.getAuthor().getName().length()); + mentionArray.add(mentionObject); + extraObject.add("mentions", mentionArray); + getTalkClient().sendMessage(msg.getChatRoom(), 1, "@" + msg.getAuthor().getName(), extraObject.toString()); + } + } + @Override + public void onNewMember(ChatRoom room, Member member) { + getTalkClient().sendMessage(room, member.getName() + "님이 들어왔습니다."); + } + + @Override + public void onDelMember(ChatRoom room, Member member) { + getTalkClient().sendMessage(room, member.getName() + "님이 나갔습니다."); + } + }); + client.connect(); + } } diff --git a/src/main/java/com/github/netricecake/KakaoApi.java b/src/main/java/com/github/netricecake/kakao/KakaoApi.java similarity index 77% rename from src/main/java/com/github/netricecake/KakaoApi.java rename to src/main/java/com/github/netricecake/kakao/KakaoApi.java index f6d088e..bf036f7 100644 --- a/src/main/java/com/github/netricecake/KakaoApi.java +++ b/src/main/java/com/github/netricecake/kakao/KakaoApi.java @@ -1,8 +1,8 @@ -package com.github.netricecake; +package com.github.netricecake.kakao; -import com.github.netricecake.message.request.GetConfRequest; -import com.github.netricecake.message.response.GetConfResponse; -import com.github.netricecake.util.ByteUtil; +import com.github.netricecake.loco.packet.inbound.GetConfIn; +import com.github.netricecake.loco.packet.outbound.GetConfOut; +import com.github.netricecake.loco.util.ByteUtil; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -10,11 +10,9 @@ import okhttp3.*; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; -import javax.security.auth.login.AccountException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.charset.StandardCharsets; import java.security.InvalidParameterException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -22,6 +20,17 @@ import java.util.Map; public class KakaoApi { + + public final static String AGENT = "android"; + public final static String VERSION = "25.9.2"; + public final static String OS_VERSION = "13"; + public final static String API_LEVEL = "33"; + public final static String LANGUAGE = "ko"; + + public final static String PROTOCOL_VERSION = "1"; + public final static int NETWORK_TYPE = 0; // 0 : WIFI, 3: Cellular + public final static String MCCMNC = "45006"; // 앞자리 세자리(한국) 450 고정, 뒤에 두자리 SKT: 05 KT: 08 LGU+: 06 + public final static String ALLOW_LIST_URL = "https://katalk.kakao.com/android/account/allowlist.json"; public final static String LOGIN_URL = "https://katalk.kakao.com/android/account/login.json"; public final static String PASSCODE_GENERATE_URL = "https://katalk.kakao.com/android/account/passcodeLogin/generate"; @@ -30,11 +39,6 @@ public class KakaoApi { public final static String BOOKING_URL = "booking-loco.kakao.com"; public final static int BOOKING_PORT = 443; - public final static String AGENT = "android"; - public final static String VERSION = "25.9.2"; - public final static String OS_VERSION = "13"; - public final static String API_LEVEL = "33"; - public final static String LANGUAGE = "ko"; public final static String AUTH_USER_AGENT = String.format("KT/%s An/%s %s", VERSION, OS_VERSION, LANGUAGE); public final static String AUTH_HEADER_AGENT = String.format("%s/%s/%s", AGENT, VERSION, LANGUAGE); @@ -69,10 +73,10 @@ public class KakaoApi { if (status == 0) { LoginData data = new LoginData(); - data.userId = jsonObject.get("userId").getAsInt(); + data.userId = jsonObject.get("userId").getAsLong(); data.countryIso = jsonObject.get("countryIso").getAsString(); data.countryCode = jsonObject.get("countryCode").getAsString(); - data.accountId = jsonObject.get("accountId").getAsInt(); + data.accountId = jsonObject.get("accountId").getAsLong(); data.accessToken = jsonObject.get("access_token").getAsString(); data.refreshToken = jsonObject.get("refresh_token").getAsString(); data.tokenType = jsonObject.get("token_type").getAsString(); @@ -133,11 +137,11 @@ public class KakaoApi { return false; } - public static GetConfResponse getBookingData(String MCCMNC, int userId) { + public static GetConfIn getBookingData(long userId) { byte[] id = ByteUtil.intToByteArrayLE(1000); byte[] method = new byte[11]; System.arraycopy("GETCONF".getBytes(), 0, method, 0, 7); - byte[] b = new GetConfRequest(MCCMNC, AGENT, userId).toBson(); + byte[] b = new GetConfOut(MCCMNC, AGENT, userId).toBson(); byte[] m = ByteUtil.concatBytes(id, new byte[2], method, new byte[1], ByteUtil.intToByteArrayLE(b.length), b); try { SSLSocketFactory socketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); @@ -154,7 +158,7 @@ public class KakaoApi { reader.read(res); int len = ByteUtil.byteArrayToIntLE(ByteUtil.sliceBytes(res, 18, 4)); - GetConfResponse r = new GetConfResponse(); + GetConfIn r = new GetConfIn(); r.fromBson(ByteUtil.sliceBytes(res, 22, len)); socket.close(); @@ -201,10 +205,10 @@ public class KakaoApi { } public static class LoginData { - public int userId; + public long userId; public String countryIso; public String countryCode; - public int accountId; + public long accountId; public String accessToken; public String refreshToken; public String tokenType; @@ -213,6 +217,40 @@ public class KakaoApi { public String mainDeviceAgentName; public String mainDeviceAppVersion; public String recipe; + + public void fromJson(String json) { + JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject(); + userId = jsonObject.get("userId").getAsLong(); + countryIso = jsonObject.get("countryIso").getAsString(); + countryCode = jsonObject.get("countryCode").getAsString(); + accountId = jsonObject.get("accountId").getAsLong(); + accessToken = jsonObject.get("access_token").getAsString(); + refreshToken = jsonObject.get("refresh_token").getAsString(); + tokenType = jsonObject.get("token_type").getAsString(); + autoLoginAccountId = jsonObject.get("autoLoginAccountId").getAsString(); + displayAccountId = jsonObject.get("displayAccountId").getAsString(); + mainDeviceAgentName = jsonObject.get("mainDeviceAgentName").getAsString(); + mainDeviceAppVersion = jsonObject.get("mainDeviceAppVersion").getAsString(); + recipe = jsonObject.get("recipe").getAsString(); + } + + public String toJson() { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("userId", userId); + jsonObject.addProperty("countryIso", countryIso); + jsonObject.addProperty("countryCode", countryCode); + jsonObject.addProperty("accountId", accountId); + jsonObject.addProperty("access_token", accessToken); + jsonObject.addProperty("refresh_token", refreshToken); + jsonObject.addProperty("token_type", tokenType); + jsonObject.addProperty("autoLoginAccountId", autoLoginAccountId); + jsonObject.addProperty("displayAccountId", displayAccountId); + jsonObject.addProperty("mainDeviceAgentName", mainDeviceAgentName); + jsonObject.addProperty("mainDeviceAppVersion", mainDeviceAppVersion); + jsonObject.addProperty("recipe", recipe); + + return gson.toJson(jsonObject); + } } } diff --git a/src/main/java/com/github/netricecake/kakao/KakaoDefaultValues.java b/src/main/java/com/github/netricecake/kakao/KakaoDefaultValues.java deleted file mode 100644 index 783d2ea..0000000 --- a/src/main/java/com/github/netricecake/kakao/KakaoDefaultValues.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.netricecake.kakao; - -public class KakaoDefaultValues { - - public final static String MCCMNC = "45006"; - public final static int ntype = 0; - public final static String os = "android"; - -} diff --git a/src/main/java/com/github/netricecake/kakao/LocoSocketHandlerImpl.java b/src/main/java/com/github/netricecake/kakao/LocoSocketHandlerImpl.java new file mode 100644 index 0000000..724758b --- /dev/null +++ b/src/main/java/com/github/netricecake/kakao/LocoSocketHandlerImpl.java @@ -0,0 +1,89 @@ +package com.github.netricecake.kakao; + +import com.github.netricecake.kakao.structs.ChatRoom; +import com.github.netricecake.kakao.structs.Member; +import com.github.netricecake.kakao.structs.Message; +import com.github.netricecake.loco.LocoPacket; +import com.github.netricecake.loco.LocoSocektHandler; +import com.github.netricecake.loco.packet.inbound.*; +import com.github.netricecake.loco.packet.outbound.ChatInfoOut; +import com.github.netricecake.loco.packet.outbound.InfoLinkOut; +import com.github.netricecake.loco.packet.outbound.MessageOut; +import lombok.Getter; + +public class LocoSocketHandlerImpl implements LocoSocektHandler { + + @Getter + private TalkClient client; + + public LocoSocketHandlerImpl(TalkClient client) { + this.client = client; + } + + @Override + public void onPacket(LocoPacket packet) { + if (packet.getMethod().equals("MSG")) { + client.getSocket().write(new LocoPacket(packet.getPacketId(), "MSG", new MessageOut().toBson())); + + MessageIn in = new MessageIn(); + in.fromBson(packet.getBody()); + checkRoom(in.getChatId()); + + ChatRoom room = client.getChatRooms().get(in.getChatId()); + if (!room.getType().equals("OM")) return; + Message msg = new Message(in.getLogId(), room, new Member(in.getAuthorId(), in.getAuthorNickname()), in.getType(), in.getMessage(), in.getAttachment()); + client.getTalkHandler().onMessage(msg); + } else if (packet.getMethod().equals("NEWMEM")) { + NewMemIn in = new NewMemIn(); + in.fromBson(packet.getBody()); + checkRoom(in.getChatId()); + + ChatRoom room = client.getChatRooms().get(in.getChatId()); + if (!room.getType().equals("OM")) return; + client.getTalkHandler().onNewMember(room, new Member(in.getUserId(), in.getNickname())); + } else if (packet.getMethod().equals("DELMEM")) { + DelMemIn in = new DelMemIn(); + in.fromBson(packet.getBody()); + checkRoom(in.getChatId()); + + ChatRoom room = client.getChatRooms().get(in.getChatId()); + if (!room.getType().equals("OM")) return; + client.getTalkHandler().onDelMember(room, new Member(in.getUserId(), in.getNickname())); + } + } + + private void checkRoom(long chatId) { + if (!client.getChatRooms().containsKey(chatId)) { + ChatInfoOut co = new ChatInfoOut(chatId); + ChatInfoIn ci = new ChatInfoIn(); + ci.fromBson(client.getSocket().writeAndRead(new LocoPacket("CHATINFO", co.toBson())).getBody()); + ChatRoom room = new ChatRoom(); + room.setChatId(chatId); + room.setType(ci.getType()); + room.setLinkId(ci.getLinkId()); + if (ci.getType().equals("OM")) { + InfoLinkOut lo = new InfoLinkOut(ci.getLinkId()); + InfoLinkIn li = new InfoLinkIn(); + li.fromBson(client.getSocket().writeAndRead(new LocoPacket("INFOLINK", lo.toBson())).getBody()); + room.setName(li.getName()); + } + + client.getChatRooms().put(chatId, room); + } + } + + @Override + public void onConnect() { + + } + + @Override + public void onDisconnect() { + + } + + @Override + public void onError(Exception e) { + + } +} diff --git a/src/main/java/com/github/netricecake/kakao/TalkClient.java b/src/main/java/com/github/netricecake/kakao/TalkClient.java new file mode 100644 index 0000000..d78f530 --- /dev/null +++ b/src/main/java/com/github/netricecake/kakao/TalkClient.java @@ -0,0 +1,222 @@ +package com.github.netricecake.kakao; + +import com.github.netricecake.kakao.structs.ChatRoom; +import com.github.netricecake.loco.LocoPacket; +import com.github.netricecake.loco.LocoSocektHandler; +import com.github.netricecake.loco.LocoSocket; +import com.github.netricecake.loco.packet.inbound.CheckInIn; +import com.github.netricecake.loco.packet.inbound.GetConfIn; +import com.github.netricecake.loco.packet.inbound.LoginListIn; +import com.github.netricecake.loco.packet.inbound.WriteIn; +import com.github.netricecake.loco.packet.outbound.CheckInOut; +import com.github.netricecake.loco.packet.outbound.LoginListOut; +import com.github.netricecake.loco.packet.outbound.PingOut; +import com.github.netricecake.loco.packet.outbound.WriteOut; +import com.github.netricecake.loco.util.ByteUtil; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.Getter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class TalkClient { + + private String email; + private String password; + private String deviceName; + private String deviceUuid; + private String sessionDir; + + @Getter + private Map chatRooms = new HashMap<>(); + + @Getter + private boolean connected; + + private KakaoApi.LoginData loginData; + private GetConfIn bookingData; + private CheckInIn checkInData; + private LoginListIn loginListData; + + private ExecutorService locoHandlerPool; + + @Getter + private TalkHandler talkHandler; + + @Getter + private LocoSocket socket; + + public TalkClient(String email, String password, String deviceName, String deviceUuid, TalkHandler talkHandler) { + this.email = email; + this.password = password; + this.deviceName = deviceName; + this.deviceUuid = deviceUuid; + this.sessionDir = System.getProperty("user.dir") + "/" + email + "_" + deviceName + "/"; + this.talkHandler = talkHandler; + talkHandler.setTalkClient(this); + + new File(sessionDir).mkdir(); + loginData = readLoginData(); + } + + public void connect() throws Exception { + if (this.connected) throw new Exception("이미 연결되어 있습니다."); + if (loginData == null) { + System.out.println("로그인 데이터가 없습니다. 새로 로그인을 시도합니다."); + try { + loginData = KakaoApi.loginRequest(email, password, deviceName, deviceUuid); + } catch (IllegalStateException e) { + Map.Entry registerInfo = KakaoApi.generatePasscode(email, password, deviceName, deviceUuid); + System.out.println("기기 등록이 필요합니다."); + System.out.println("카카오톡 앱에서 " + registerInfo.getValue() + "초 안에 코드를 입력해주세요. 코드 : " + registerInfo.getKey()); + boolean registerResult = KakaoApi.registerDevice(email, password, deviceUuid); + if (!registerResult) throw new IllegalStateException("기기등록 실패"); + System.out.println("기기 등록 성공"); + loginData = KakaoApi.loginRequest(email, password, deviceName, deviceUuid); + } + System.out.println("로그인이 완료되었습니다."); + writeLoginData(); + } + + bookingData = KakaoApi.getBookingData(loginData.userId); + + LocoSocket checkInSocket = new LocoSocket(bookingData.getAddr(), bookingData.getPort(), new LocoSocektHandler() { + @Override + public void onPacket(LocoPacket packet) { + + } + + @Override + public void onConnect() { + + } + + @Override + public void onDisconnect() { + + } + + @Override + public void onError(Exception e) { + e.printStackTrace(); + } + }, Executors.newFixedThreadPool(1)); + CheckInOut checkInRequest = new CheckInOut(loginData.userId); + byte[] body = checkInRequest.toBson(); + checkInSocket.connect(); + LocoPacket checkinResponse = checkInSocket.writeAndRead(new LocoPacket(1000, checkInRequest.getMethod(), body)); + checkInData = new CheckInIn(); + checkInData.fromBson(checkinResponse.getBody()); + checkInSocket.close(); + + + + long lastTokenId = 0; + long lbk = 0; + byte[] rp = ByteUtil.hexStringToByteArray("0000ffff0000"); + + try { + File loginListDataFile = new File(sessionDir + "loginListData.json"); + if (loginListDataFile.exists()) { + String loginDataJson = Files.readString(Paths.get(loginListDataFile.getAbsolutePath())); + JsonObject loginListData = JsonParser.parseString(loginDataJson).getAsJsonObject(); + lastTokenId = loginListData.getAsJsonPrimitive("lastTokenId").getAsLong(); + lbk = loginListData.getAsJsonPrimitive("lbk").getAsLong(); + rp = ByteUtil.hexStringToByteArray("0100ffff0100"); // 이게 도당체 뭐임 + } + } catch (IOException e) { + e.printStackTrace(); + } + + locoHandlerPool = Executors.newFixedThreadPool(1); + + socket = new LocoSocket(checkInData.getHost(), checkInData.getPort(), new LocoSocketHandlerImpl(this), locoHandlerPool); + socket.connect(); + LoginListOut req = new LoginListOut(); + req.setDuuid(deviceUuid); + req.setOauthToken(loginData.accessToken); + req.setLastTokenId(lastTokenId); + req.setLbk(lbk); + req.setRp(rp); + loginListData = new LoginListIn(); + loginListData.fromBson(socket.writeAndRead(new LocoPacket("LOGINLIST", req.toBson())).getBody()); + if (loginListData.getStatus() != 0) { + System.out.println("카카오톡 서버와 연결을 실패했습니다. 로그인 정보 파일을 삭제 후 다시 시도해보세요."); + throw new Exception("카카오톡 서버와 연결을 실패했습니다. 로그인 정보 파일을 삭제 후 다시 시도해보세요."); + } + System.out.println("연결 성공"); + + try { + File loginListDataFile = new File(sessionDir + "loginListData.json"); + if (!loginListDataFile.exists()) loginListDataFile.createNewFile(); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("lastTokenId", loginListData.getLastTokenId()); + jsonObject.addProperty("lbk", loginListData.getLbk()); + Files.write(Paths.get(loginListDataFile.getAbsolutePath()), new Gson().toJson(jsonObject).getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + + new Thread(() -> { + try { + while (socket.isAlive()) { + Thread.sleep(10 * 60 * 1000); + PingOut pingOut = new PingOut(); + LocoPacket pingPacket = new LocoPacket("PING", pingOut.toBson()); + socket.write(pingPacket); + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + private KakaoApi.LoginData readLoginData() { + try { + File loginDataFile = new File(sessionDir + "loginData.json"); + if (!loginDataFile.exists()) return null; + String loginDataJson = Files.readString(Paths.get(loginDataFile.getAbsolutePath())); + KakaoApi.LoginData loginData = new KakaoApi.LoginData(); + loginData.fromJson(loginDataJson); + return loginData; + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + private void writeLoginData() { + try { + File loginDataFile = new File(sessionDir + "loginData.json"); + if (!loginDataFile.exists()) loginDataFile.createNewFile(); + Files.write(Paths.get(loginDataFile.getAbsolutePath()), loginData.toJson().getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public boolean sendMessage(ChatRoom room, int type, String message, String extra) { + WriteOut wo = new WriteOut(); + wo.setChatId(room.getChatId()); + wo.setType(type); + wo.setMessage(message); + wo.setExtra(extra); + WriteIn wi = new WriteIn(); + wi.fromBson(socket.writeAndRead(new LocoPacket("WRITE", wo.toBson())).getBody()); + return wi.getStatus() == 0; + } + + public boolean sendMessage(ChatRoom room, String message) { + return sendMessage(room, 1, message, "{}"); + } + +} diff --git a/src/main/java/com/github/netricecake/kakao/TalkHandler.java b/src/main/java/com/github/netricecake/kakao/TalkHandler.java new file mode 100644 index 0000000..3be6ab7 --- /dev/null +++ b/src/main/java/com/github/netricecake/kakao/TalkHandler.java @@ -0,0 +1,27 @@ +package com.github.netricecake.kakao; + +import com.github.netricecake.kakao.structs.ChatRoom; +import com.github.netricecake.kakao.structs.Member; +import com.github.netricecake.kakao.structs.Message; +import lombok.Getter; +import lombok.Setter; + +public class TalkHandler { + + @Getter + @Setter + private TalkClient talkClient; + + public void onMessage(Message message) { + + } + + public void onNewMember(ChatRoom chaRoom, Member member) { + + } + + public void onDelMember(ChatRoom chatRoom, Member member) { + + } + +} diff --git a/src/main/java/com/github/netricecake/kakao/structs/ChatRoom.java b/src/main/java/com/github/netricecake/kakao/structs/ChatRoom.java new file mode 100644 index 0000000..d84f81f --- /dev/null +++ b/src/main/java/com/github/netricecake/kakao/structs/ChatRoom.java @@ -0,0 +1,20 @@ +package com.github.netricecake.kakao.structs; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ChatRoom { + + private long chatId; + + private String type; + + private String name; + + private long linkId; + + //private Map members = new HashMap<>(); + +} diff --git a/src/main/java/com/github/netricecake/kakao/structs/Member.java b/src/main/java/com/github/netricecake/kakao/structs/Member.java new file mode 100644 index 0000000..42595ca --- /dev/null +++ b/src/main/java/com/github/netricecake/kakao/structs/Member.java @@ -0,0 +1,17 @@ +package com.github.netricecake.kakao.structs; + +import lombok.Getter; + +@Getter +public class Member { + + private long id; + + private String name; + + public Member(long id, String name) { + this.id = id; + this.name = name; + } + +} diff --git a/src/main/java/com/github/netricecake/kakao/structs/Message.java b/src/main/java/com/github/netricecake/kakao/structs/Message.java new file mode 100644 index 0000000..bebff03 --- /dev/null +++ b/src/main/java/com/github/netricecake/kakao/structs/Message.java @@ -0,0 +1,29 @@ +package com.github.netricecake.kakao.structs; + +import lombok.Getter; + +@Getter +public class Message { + + private long logId; + + private ChatRoom chatRoom; + + private Member author; + + private int type; + + private String message; + + private String attachment; + + public Message(long logId, ChatRoom chatRoom, Member author, int type, String message, String attachment) { + this.logId = logId; + this.chatRoom = chatRoom; + this.author = author; + this.type = type; + this.message = message; + this.attachment = attachment; + } + +} diff --git a/src/main/java/com/github/netricecake/network/LocoPacket.java b/src/main/java/com/github/netricecake/loco/LocoPacket.java similarity index 73% rename from src/main/java/com/github/netricecake/network/LocoPacket.java rename to src/main/java/com/github/netricecake/loco/LocoPacket.java index edbff56..c5bdb27 100644 --- a/src/main/java/com/github/netricecake/network/LocoPacket.java +++ b/src/main/java/com/github/netricecake/loco/LocoPacket.java @@ -1,27 +1,22 @@ -package com.github.netricecake.network; +package com.github.netricecake.loco; import lombok.Getter; import lombok.Setter; +@Getter +@Setter public class LocoPacket { - @Getter - private final int packetId; + private int packetId; - @Getter - private final short statusCode; + private short statusCode; - @Getter private String method; - @Getter - private final byte bodyType; + private byte bodyType; - @Getter private int bodyLength; - @Getter - @Setter private byte[] body; public LocoPacket(int packetId, short statusCode, String method, byte bodyType, int bodyLength, byte[] body) { @@ -37,4 +32,8 @@ public class LocoPacket { this(packetId, (short) 0, method, (byte) 0, body.length, body); } + public LocoPacket(String method, byte[] body) { + this(-1, method, body); + } + } diff --git a/src/main/java/com/github/netricecake/loco/LocoSocektHandler.java b/src/main/java/com/github/netricecake/loco/LocoSocektHandler.java new file mode 100644 index 0000000..3d8b94d --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/LocoSocektHandler.java @@ -0,0 +1,13 @@ +package com.github.netricecake.loco; + +public interface LocoSocektHandler { + + void onPacket(LocoPacket packet); + + void onConnect(); + + void onDisconnect(); + + void onError(Exception e); + +} diff --git a/src/main/java/com/github/netricecake/loco/LocoSocket.java b/src/main/java/com/github/netricecake/loco/LocoSocket.java new file mode 100644 index 0000000..d56058a --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/LocoSocket.java @@ -0,0 +1,139 @@ +package com.github.netricecake.loco; + +import com.github.netricecake.loco.crypto.CryptoManager; +import com.github.netricecake.loco.codec.LocoCodec; +import com.github.netricecake.loco.codec.SecureLayerCodec; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioIoHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.bytes.ByteArrayDecoder; +import io.netty.handler.codec.bytes.ByteArrayEncoder; +import lombok.Getter; + +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.*; + +public class LocoSocket { + + @Getter + private String ip; + @Getter + private int port; + + private CryptoManager cryptoManager; + + private Channel channel; + private EventLoopGroup eventLoopGroup; + + @Getter + private boolean alive = false; + + @Getter + private Map> waitList = new HashMap<>(); + + @Getter + private LocoSocektHandler locoSocektHandler; + + @Getter + private ExecutorService handlerPool; + + private int packetIdCounter = 1000; + + public LocoSocket(String ip, int port, LocoSocektHandler locoSocektHandler, ExecutorService handlerPool) { + this.ip = ip; + this.port = port; + this.locoSocektHandler = locoSocektHandler; + this.handlerPool = handlerPool; + cryptoManager = new CryptoManager(); + } + + public void connect() { + try { + byte[] handshakePacket = cryptoManager.generateHandshakeMessage(); + eventLoopGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); + Bootstrap bootstrap = new Bootstrap(); + bootstrap.remoteAddress(new InetSocketAddress(ip, port)) + .group(eventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + ChannelPipeline pipeline = socketChannel.pipeline(); + pipeline.addLast(new ByteArrayEncoder()); + pipeline.addLast(new ByteArrayDecoder()); + } + }); + channel = bootstrap.connect().sync().channel(); + alive = true; + channel.writeAndFlush(handshakePacket).sync(); + channel.pipeline().addLast(new SecureLayerCodec(cryptoManager)); + channel.pipeline().addLast(new LocoCodec(this)); + handlerPool.execute(() -> { + locoSocektHandler.onConnect(); + }); + new Thread() { + @Override + public void run() { + try { + channel.closeFuture().sync(); + eventLoopGroup.shutdownGracefully(); + handlerPool.execute(() -> { + locoSocektHandler.onDisconnect(); + }); + alive = false; + } catch (Exception e) { + handlerPool.execute(() -> { + locoSocektHandler.onError(e); + }); + } + } + }.start(); + } catch (Exception e) { + handlerPool.execute(() -> { + locoSocektHandler.onError(e); + }); + } + } + + public void write(LocoPacket packet) { + if (!alive) return; + synchronized (this) { + if (packet.getPacketId() == -1) packet.setPacketId(++packetIdCounter); + } + channel.writeAndFlush(packet); + } + + public LocoPacket writeAndRead(LocoPacket packet) { + if (!alive) return null; + int packetId = packet.getPacketId(); + synchronized (this) { + if (packet.getPacketId() == -1) packetId = ++packetIdCounter; + } + packet.setPacketId(packetId); + Future future = new CompletableFuture<>(); + waitList.put(packetId, future); + channel.writeAndFlush(packet); + try { + return future.get(); + } catch (Exception e) { + handlerPool.execute(() -> { + locoSocektHandler.onError(e); + }); + } + return null; + } + + public void close() { + handlerPool.execute(() -> { + locoSocektHandler.onDisconnect(); + }); + channel.close(); + eventLoopGroup.shutdownGracefully(); + alive = false; + } + +} diff --git a/src/main/java/com/github/netricecake/network/codec/LocoCodec.java b/src/main/java/com/github/netricecake/loco/codec/LocoCodec.java similarity index 72% rename from src/main/java/com/github/netricecake/network/codec/LocoCodec.java rename to src/main/java/com/github/netricecake/loco/codec/LocoCodec.java index 0d5ea71..45734a9 100644 --- a/src/main/java/com/github/netricecake/network/codec/LocoCodec.java +++ b/src/main/java/com/github/netricecake/loco/codec/LocoCodec.java @@ -1,22 +1,24 @@ -package com.github.netricecake.network.codec; +package com.github.netricecake.loco.codec; -import com.github.netricecake.network.LocoPacket; -import com.github.netricecake.util.ByteUtil; +import com.github.netricecake.loco.LocoPacket; +import com.github.netricecake.loco.LocoSocket; +import com.github.netricecake.loco.util.BsonUtil; +import com.github.netricecake.loco.util.ByteUtil; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageCodec; import java.util.List; -import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; public class LocoCodec extends MessageToMessageCodec { private LocoPacket currentLocoPacket = null; private byte[] buffer = new byte[0]; - private BlockingQueue locoPacketQueue; + private final LocoSocket locoSocket; - public LocoCodec(BlockingQueue locoPacketHandler) { - this.locoPacketQueue = locoPacketHandler; + public LocoCodec(LocoSocket locoSocekt) { + this.locoSocket = locoSocekt; } @Override @@ -55,7 +57,15 @@ public class LocoCodec extends MessageToMessageCodec { byte[] body = ByteUtil.sliceBytes(buffer, 0, currentLocoPacket.getBodyLength()); buffer = ByteUtil.sliceBytes(buffer, currentLocoPacket.getBodyLength(), buffer.length - currentLocoPacket.getBodyLength()); currentLocoPacket.setBody(body); - locoPacketQueue.put(currentLocoPacket); + if (locoSocket.getWaitList().containsKey(currentLocoPacket.getPacketId())) { + ((CompletableFuture) locoSocket.getWaitList().get(currentLocoPacket.getPacketId())).complete(currentLocoPacket); + locoSocket.getWaitList().remove(currentLocoPacket.getPacketId()); + } else { + final LocoPacket p = currentLocoPacket; + locoSocket.getHandlerPool().execute(() -> { + locoSocket.getLocoSocektHandler().onPacket(p); + }); + } currentLocoPacket = null; } while (buffer.length > 0); diff --git a/src/main/java/com/github/netricecake/network/codec/SecureLayerCodec.java b/src/main/java/com/github/netricecake/loco/codec/SecureLayerCodec.java similarity index 91% rename from src/main/java/com/github/netricecake/network/codec/SecureLayerCodec.java rename to src/main/java/com/github/netricecake/loco/codec/SecureLayerCodec.java index df5a288..0fab3da 100644 --- a/src/main/java/com/github/netricecake/network/codec/SecureLayerCodec.java +++ b/src/main/java/com/github/netricecake/loco/codec/SecureLayerCodec.java @@ -1,7 +1,7 @@ -package com.github.netricecake.network.codec; +package com.github.netricecake.loco.codec; -import com.github.netricecake.crypto.CryptoManager; -import com.github.netricecake.util.ByteUtil; +import com.github.netricecake.loco.crypto.CryptoManager; +import com.github.netricecake.loco.util.ByteUtil; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageCodec; diff --git a/src/main/java/com/github/netricecake/crypto/CryptoManager.java b/src/main/java/com/github/netricecake/loco/crypto/CryptoManager.java similarity index 94% rename from src/main/java/com/github/netricecake/crypto/CryptoManager.java rename to src/main/java/com/github/netricecake/loco/crypto/CryptoManager.java index 0254873..73857bc 100644 --- a/src/main/java/com/github/netricecake/crypto/CryptoManager.java +++ b/src/main/java/com/github/netricecake/loco/crypto/CryptoManager.java @@ -1,11 +1,14 @@ -package com.github.netricecake.crypto; +package com.github.netricecake.loco.crypto; -import com.github.netricecake.util.ByteUtil; +import com.github.netricecake.loco.util.ByteUtil; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.spec.GCMParameterSpec; -import java.security.*; +import java.security.Key; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.SecureRandom; import java.security.spec.X509EncodedKeySpec; public class CryptoManager { diff --git a/src/main/java/com/github/netricecake/loco/packet/inbound/ChatInfoIn.java b/src/main/java/com/github/netricecake/loco/packet/inbound/ChatInfoIn.java new file mode 100644 index 0000000..62ba02c --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/inbound/ChatInfoIn.java @@ -0,0 +1,22 @@ +package com.github.netricecake.loco.packet.inbound; + +import com.github.netricecake.loco.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; + +@Getter +public class ChatInfoIn { + + private String type; + + private long linkId; + + public void fromBson(byte[] bson) { + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); + type = jsonObject.get("chatInfo").getAsJsonObject().get("type").getAsString(); + try { + linkId = jsonObject.get("chatInfo").getAsJsonObject().get("li").getAsLong(); + } catch (Exception e) {} + } + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/inbound/CheckInIn.java b/src/main/java/com/github/netricecake/loco/packet/inbound/CheckInIn.java new file mode 100644 index 0000000..450dc37 --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/inbound/CheckInIn.java @@ -0,0 +1,52 @@ +package com.github.netricecake.loco.packet.inbound; + +import com.github.netricecake.loco.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.Setter; + +@Getter +public class CheckInIn { + + private int status; + + private String host; + + private String host6; + + private int port; + + private String cshost; + + private String cshost6; + + private int csport; + + private String vsshost; + + private String vsshost6; + + private int vssport; + + private long cacheExpire; + + private String MCCMNC; + + public void fromBson(byte[] bson) + { + JsonObject json = BsonUtil.bsonToJsonObject(bson); + status = json.get("status").getAsInt(); + host = json.get("host").getAsString(); + host6 = json.get("host6").getAsString(); + port = json.get("port").getAsInt(); + cshost = json.get("cshost").getAsString(); + cshost6 = json.get("cshost6").getAsString(); + csport = json.get("csport").getAsInt(); + vsshost = json.get("vsshost").getAsString(); + vsshost6 = json.get("vsshost6").getAsString(); + vssport = json.get("vssport").getAsInt(); + cacheExpire = json.get("cacheExpire").getAsLong(); + MCCMNC = json.get("MCCMNC").getAsString(); + } + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/inbound/DelMemIn.java b/src/main/java/com/github/netricecake/loco/packet/inbound/DelMemIn.java new file mode 100644 index 0000000..b4f095d --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/inbound/DelMemIn.java @@ -0,0 +1,25 @@ +package com.github.netricecake.loco.packet.inbound; + +import com.github.netricecake.loco.util.BsonUtil; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.Getter; + +@Getter +public class DelMemIn { + + private long chatId; + + private long userId; + + private String nickname; + + public void fromBson(byte[] bson) { + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson).get("chatLog").getAsJsonObject(); + chatId = jsonObject.get("chatId").getAsLong(); + userId = jsonObject.get("authorId").getAsLong(); + JsonObject msgObject = JsonParser.parseString(jsonObject.get("message").getAsString()).getAsJsonObject(); + nickname = msgObject.get("member").getAsJsonObject().get("nickName").getAsString(); + } + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/inbound/GetConfIn.java b/src/main/java/com/github/netricecake/loco/packet/inbound/GetConfIn.java new file mode 100644 index 0000000..ee92d6d --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/inbound/GetConfIn.java @@ -0,0 +1,24 @@ +package com.github.netricecake.loco.packet.inbound; + +import com.github.netricecake.loco.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.Setter; + +@Getter +public class GetConfIn { + + private int status; + + private String addr; + + private int port; + + public void fromBson(byte[] bson) { + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); + status = jsonObject.get("status").getAsInt(); + addr = jsonObject.get("ticket").getAsJsonObject().get("lsl").getAsJsonArray().get(0).getAsString(); + port = jsonObject.get("wifi").getAsJsonObject().get("ports").getAsJsonArray().get(0).getAsInt(); + } + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/inbound/InfoLinkIn.java b/src/main/java/com/github/netricecake/loco/packet/inbound/InfoLinkIn.java new file mode 100644 index 0000000..a01a2a3 --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/inbound/InfoLinkIn.java @@ -0,0 +1,17 @@ +package com.github.netricecake.loco.packet.inbound; + +import com.github.netricecake.loco.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; + +@Getter +public class InfoLinkIn { + + private String name; + + public void fromBson(byte[] bson) { + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); + name = jsonObject.get("ols").getAsJsonArray().get(0).getAsJsonObject().get("ln").getAsString(); + } + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/inbound/LoginListIn.java b/src/main/java/com/github/netricecake/loco/packet/inbound/LoginListIn.java new file mode 100644 index 0000000..0909402 --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/inbound/LoginListIn.java @@ -0,0 +1,65 @@ +package com.github.netricecake.loco.packet.inbound; + +import com.github.netricecake.loco.util.BsonUtil; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.Getter; + +import java.util.Base64; + +@Getter +public class LoginListIn { + + private int status; + + private long userId; + + private int revision; + + private String revisionInfo; + + private byte[] rp; + + private long minLogId; + + private int sb; + + private JsonArray chatDatas; + + private JsonArray delChatIds; + + private JsonArray kc; + + private int mcmRevision; + + private long lastTokenId; + + private long lastChatId; + + private long ltk; + + private long lbk; + + private boolean eof; + + public void fromBson(byte[] bson) { + JsonObject json = BsonUtil.bsonToJsonObject(bson); + status = json.get("status").getAsInt(); + userId = json.get("userId").getAsLong(); + revision = json.get("revision").getAsInt(); + revisionInfo = json.get("revisionInfo").getAsString(); + rp = Base64.getDecoder().decode(json.get("rp").getAsJsonObject().get("$binary").getAsJsonObject().get("base64").getAsString()); + minLogId = json.get("minLogId").getAsLong(); + sb = json.get("sb").getAsInt(); + chatDatas = json.get("chatDatas").getAsJsonArray(); + delChatIds = json.get("delChatIds").getAsJsonArray(); + kc = json.get("kc").getAsJsonArray(); + mcmRevision = json.get("mcmRevision").getAsInt(); + lastTokenId = json.get("lastTokenId").getAsLong(); + lastChatId = json.get("lastChatId").getAsLong(); + ltk = json.get("ltk").getAsLong(); + lbk = json.get("lbk").getAsLong(); + eof = json.get("eof").getAsBoolean(); + } + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/inbound/MessageIn.java b/src/main/java/com/github/netricecake/loco/packet/inbound/MessageIn.java new file mode 100644 index 0000000..c62a3c4 --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/inbound/MessageIn.java @@ -0,0 +1,41 @@ +package com.github.netricecake.loco.packet.inbound; + +import com.github.netricecake.loco.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; + +@Getter +public class MessageIn { + + private long chatId; + + private long logId; + + private long authorId; + + private String authorNickname; + + private int type; + + private String message; + + private String attachment; + + public void fromBson(byte[] bson) { + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); + chatId = jsonObject.get("chatId").getAsLong(); + logId = jsonObject.get("logId").getAsLong(); + authorId = jsonObject.get("chatLog").getAsJsonObject().get("authorId").getAsLong(); + try { + authorNickname = jsonObject.get("authorNickname").getAsString(); // 옵챗 아니면 이필드가 없음;; + } catch (Exception e) {} + type = jsonObject.get("chatLog").getAsJsonObject().get("type").getAsInt(); + message = jsonObject.get("chatLog").getAsJsonObject().get("message").getAsString(); + try { + attachment = jsonObject.get("chatLog").getAsJsonObject().get("attachment").getAsString(); + } catch (Exception e) { + attachment = ""; + } + } + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/inbound/NewMemIn.java b/src/main/java/com/github/netricecake/loco/packet/inbound/NewMemIn.java new file mode 100644 index 0000000..15f765c --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/inbound/NewMemIn.java @@ -0,0 +1,25 @@ +package com.github.netricecake.loco.packet.inbound; + +import com.github.netricecake.loco.util.BsonUtil; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.Getter; + +@Getter +public class NewMemIn { + + private long chatId; + + private long userId; + + private String nickname; + + public void fromBson(byte[] bson) { + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson).get("chatLog").getAsJsonObject(); + chatId = jsonObject.get("chatId").getAsLong(); + userId = jsonObject.get("authorId").getAsLong(); + JsonObject msgObject = JsonParser.parseString(jsonObject.get("message").getAsString()).getAsJsonObject(); + nickname = msgObject.get("members").getAsJsonArray().get(0).getAsJsonObject().get("nickName").getAsString(); + } + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/inbound/PingIn.java b/src/main/java/com/github/netricecake/loco/packet/inbound/PingIn.java new file mode 100644 index 0000000..b0af053 --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/inbound/PingIn.java @@ -0,0 +1,4 @@ +package com.github.netricecake.loco.packet.inbound; + +public class PingIn { +} diff --git a/src/main/java/com/github/netricecake/loco/packet/inbound/WriteIn.java b/src/main/java/com/github/netricecake/loco/packet/inbound/WriteIn.java new file mode 100644 index 0000000..5f9b42b --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/inbound/WriteIn.java @@ -0,0 +1,17 @@ +package com.github.netricecake.loco.packet.inbound; + +import com.github.netricecake.loco.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; + +@Getter +public class WriteIn { + + private int status; + + public void fromBson(byte[] bson) { + JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); + status = jsonObject.get("status").getAsInt(); + } + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/outbound/ChatInfoOut.java b/src/main/java/com/github/netricecake/loco/packet/outbound/ChatInfoOut.java new file mode 100644 index 0000000..1588512 --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/outbound/ChatInfoOut.java @@ -0,0 +1,20 @@ +package com.github.netricecake.loco.packet.outbound; + +import com.github.netricecake.loco.util.BsonUtil; +import com.google.gson.JsonObject; + +public class ChatInfoOut { + + private long chatId; + + public ChatInfoOut(long chatId) { + this.chatId = chatId; + } + + public byte[] toBson() { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("chatId", chatId); + return BsonUtil.jsonObjectToBson(jsonObject); + } + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/outbound/CheckInOut.java b/src/main/java/com/github/netricecake/loco/packet/outbound/CheckInOut.java new file mode 100644 index 0000000..2dd7e37 --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/outbound/CheckInOut.java @@ -0,0 +1,45 @@ +package com.github.netricecake.loco.packet.outbound; + +import com.github.netricecake.kakao.KakaoApi; +import com.github.netricecake.loco.util.BsonUtil; +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CheckInOut { + + private long userId; + + private String os = KakaoApi.AGENT; + + private int ntype = KakaoApi.NETWORK_TYPE; + + private String appVer = KakaoApi.VERSION; + + private String lang = KakaoApi.LANGUAGE; + + private String MCCMNC = KakaoApi.MCCMNC; + + public CheckInOut(long userId) { + this.userId = userId; + } + + public String getMethod() { + return "CHECKIN"; + } + + public byte[] toBson() { + JsonObject checkInObject = new JsonObject(); + checkInObject.addProperty("userId", userId); + checkInObject.addProperty("os", os); + checkInObject.addProperty("ntype", ntype); + checkInObject.addProperty("appVer", appVer); + checkInObject.addProperty("lang", lang); + checkInObject.addProperty("MCCMNC", MCCMNC); + + return BsonUtil.jsonObjectToBson(checkInObject); + } + +} diff --git a/src/main/java/com/github/netricecake/message/request/GetConfRequest.java b/src/main/java/com/github/netricecake/loco/packet/outbound/GetConfOut.java similarity index 55% rename from src/main/java/com/github/netricecake/message/request/GetConfRequest.java rename to src/main/java/com/github/netricecake/loco/packet/outbound/GetConfOut.java index 713290b..7e4fd01 100644 --- a/src/main/java/com/github/netricecake/message/request/GetConfRequest.java +++ b/src/main/java/com/github/netricecake/loco/packet/outbound/GetConfOut.java @@ -1,29 +1,22 @@ -package com.github.netricecake.message.request; +package com.github.netricecake.loco.packet.outbound; -import com.github.netricecake.message.LocoRequest; -import com.github.netricecake.util.BsonUtil; +import com.github.netricecake.loco.util.BsonUtil; import com.google.gson.JsonObject; -public class GetConfRequest implements LocoRequest { +public class GetConfOut { private String MCCMNC; private String os; - private int userId; + private long userId; - public GetConfRequest(String MCCMNC, String os, int userId) { + public GetConfOut(String MCCMNC, String os, long userId) { this.MCCMNC = MCCMNC; this.os = os; this.userId = userId; } - @Override - public String getMethod() { - return "GETCONF"; - } - - @Override public byte[] toBson() { JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("MCCMNC", MCCMNC); @@ -31,4 +24,5 @@ public class GetConfRequest implements LocoRequest { jsonObject.addProperty("userId", userId); return BsonUtil.jsonObjectToBson(jsonObject); } + } diff --git a/src/main/java/com/github/netricecake/loco/packet/outbound/InfoLinkOut.java b/src/main/java/com/github/netricecake/loco/packet/outbound/InfoLinkOut.java new file mode 100644 index 0000000..9406749 --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/outbound/InfoLinkOut.java @@ -0,0 +1,23 @@ +package com.github.netricecake.loco.packet.outbound; + +import com.github.netricecake.loco.util.BsonUtil; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +public class InfoLinkOut { + + private long linkId; + + public InfoLinkOut(long linkId) { + this.linkId = linkId; + } + + public byte[] toBson() { + JsonObject jsonObject = new JsonObject(); + JsonArray jsonArray = new JsonArray(); + jsonArray.add(linkId); + jsonObject.add("lis", jsonArray); + return BsonUtil.jsonObjectToBson(jsonObject); + } + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/outbound/LoginListOut.java b/src/main/java/com/github/netricecake/loco/packet/outbound/LoginListOut.java new file mode 100644 index 0000000..b0b8803 --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/outbound/LoginListOut.java @@ -0,0 +1,79 @@ +package com.github.netricecake.loco.packet.outbound; + +import com.github.netricecake.kakao.KakaoApi; +import com.github.netricecake.loco.util.BsonUtil; +import com.github.netricecake.loco.util.ByteUtil; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.Setter; + +import java.util.Base64; + +@Getter +@Setter +public class LoginListOut { + + private String appVer = KakaoApi.VERSION; + + private String prtVer = KakaoApi.PROTOCOL_VERSION; + + private String os = KakaoApi.AGENT; + + private String lang = KakaoApi.LANGUAGE; + + private String duuid; + + private int ntype = KakaoApi.NETWORK_TYPE; + + private String MCCMNC = KakaoApi.MCCMNC; + + private int revision = 0; + + private JsonArray chatIds = new JsonArray(); + + private JsonArray maxIds = new JsonArray(); + + private long lastTokenId = 0; + + private long lbk = 0; + + private byte[] rp; // 이거 뭐임 + + private boolean bg = true; + + private String oauthToken; + + public LoginListOut() { + rp = ByteUtil.hexStringToByteArray("0000ffff0000"); + } + + public byte[] toBson() { + JsonObject rpObject = new JsonObject(); + JsonObject binary = new JsonObject(); + binary.addProperty("base64", new String(Base64.getEncoder().encode(rp))); + binary.addProperty("subType", "00"); + rpObject.add("$binary", binary); + + JsonObject resultObject = new JsonObject(); + resultObject.addProperty("appVer", appVer); + resultObject.addProperty("prtVer", prtVer); + resultObject.addProperty("os", os); + resultObject.addProperty("lang", lang); + resultObject.addProperty("duuid", duuid); + resultObject.addProperty("ntype", ntype); + resultObject.addProperty("MCCMNC", MCCMNC); + resultObject.addProperty("revision", revision); + resultObject.add("chatIds", chatIds); + resultObject.add("maxIds", maxIds); + resultObject.addProperty("lastTokenId", lastTokenId); + resultObject.addProperty("lbk", lbk); + resultObject.add("rp", rpObject); + resultObject.addProperty("bg", bg); + resultObject.addProperty("oauthToken", oauthToken); + + return BsonUtil.jsonObjectToBson(resultObject); + } + + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/outbound/MessageOut.java b/src/main/java/com/github/netricecake/loco/packet/outbound/MessageOut.java new file mode 100644 index 0000000..bbebade --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/outbound/MessageOut.java @@ -0,0 +1,11 @@ +package com.github.netricecake.loco.packet.outbound; + +import com.github.netricecake.loco.util.BsonUtil; + +public class MessageOut { + + public byte[] toBson() { + return BsonUtil.jsonToBson("{ \"notiRead\": false }"); + } + +} diff --git a/src/main/java/com/github/netricecake/loco/packet/outbound/PingOut.java b/src/main/java/com/github/netricecake/loco/packet/outbound/PingOut.java new file mode 100644 index 0000000..a5a5b28 --- /dev/null +++ b/src/main/java/com/github/netricecake/loco/packet/outbound/PingOut.java @@ -0,0 +1,11 @@ +package com.github.netricecake.loco.packet.outbound; + +import com.github.netricecake.loco.util.BsonUtil; + +public class PingOut { + + public byte[] toBson() { + return BsonUtil.jsonToBson("{}"); + } + +} diff --git a/src/main/java/com/github/netricecake/message/request/WriteRequest.java b/src/main/java/com/github/netricecake/loco/packet/outbound/WriteOut.java similarity index 64% rename from src/main/java/com/github/netricecake/message/request/WriteRequest.java rename to src/main/java/com/github/netricecake/loco/packet/outbound/WriteOut.java index 5d9ef27..cbaad6f 100644 --- a/src/main/java/com/github/netricecake/message/request/WriteRequest.java +++ b/src/main/java/com/github/netricecake/loco/packet/outbound/WriteOut.java @@ -1,33 +1,24 @@ -package com.github.netricecake.message.request; +package com.github.netricecake.loco.packet.outbound; -import com.github.netricecake.message.LocoRequest; -import com.github.netricecake.util.BsonUtil; +import com.github.netricecake.loco.util.BsonUtil; import com.google.gson.JsonObject; import lombok.Getter; import lombok.Setter; +import java.security.SecureRandom; + @Getter @Setter -public class WriteRequest implements LocoRequest { +public class WriteOut { private long chatId; - private long msgId; // 이거 뭐임??? + private long msgId = new SecureRandom().nextLong(); private String message; private int type = 1; private boolean noSeen = false; private String extra = "{}"; private int scope = 1; - public WriteRequest() { - msgId = (long) Math.ceil(Math.random() * 99999999); - } - - @Override - public String getMethod() { - return "WRITE"; - } - - @Override public byte[] toBson() { JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("chatId", chatId); diff --git a/src/main/java/com/github/netricecake/util/BsonUtil.java b/src/main/java/com/github/netricecake/loco/util/BsonUtil.java similarity index 96% rename from src/main/java/com/github/netricecake/util/BsonUtil.java rename to src/main/java/com/github/netricecake/loco/util/BsonUtil.java index 4dd064d..75b3a4c 100644 --- a/src/main/java/com/github/netricecake/util/BsonUtil.java +++ b/src/main/java/com/github/netricecake/loco/util/BsonUtil.java @@ -1,4 +1,4 @@ -package com.github.netricecake.util; +package com.github.netricecake.loco.util; import com.google.gson.Gson; import com.google.gson.JsonObject; diff --git a/src/main/java/com/github/netricecake/util/ByteUtil.java b/src/main/java/com/github/netricecake/loco/util/ByteUtil.java similarity index 97% rename from src/main/java/com/github/netricecake/util/ByteUtil.java rename to src/main/java/com/github/netricecake/loco/util/ByteUtil.java index ae4ea15..0156d35 100644 --- a/src/main/java/com/github/netricecake/util/ByteUtil.java +++ b/src/main/java/com/github/netricecake/loco/util/ByteUtil.java @@ -1,4 +1,4 @@ -package com.github.netricecake.util; +package com.github.netricecake.loco.util; public class ByteUtil { diff --git a/src/main/java/com/github/netricecake/message/LocoRequest.java b/src/main/java/com/github/netricecake/message/LocoRequest.java deleted file mode 100644 index aa715a6..0000000 --- a/src/main/java/com/github/netricecake/message/LocoRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.netricecake.message; - -public interface LocoRequest { - - String getMethod(); - - byte[] toBson(); - -} diff --git a/src/main/java/com/github/netricecake/message/LocoResponse.java b/src/main/java/com/github/netricecake/message/LocoResponse.java deleted file mode 100644 index 1bbdbf0..0000000 --- a/src/main/java/com/github/netricecake/message/LocoResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.netricecake.message; - -public interface LocoResponse { - - String getMethod(); - - void fromBson(byte[] bson); - -} diff --git a/src/main/java/com/github/netricecake/message/request/CheckInRequest.java b/src/main/java/com/github/netricecake/message/request/CheckInRequest.java deleted file mode 100644 index c1ef6d4..0000000 --- a/src/main/java/com/github/netricecake/message/request/CheckInRequest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.netricecake.message.request; - -import com.github.netricecake.KakaoApi; -import com.github.netricecake.kakao.KakaoDefaultValues; -import com.github.netricecake.message.LocoRequest; -import com.github.netricecake.util.BsonUtil; -import com.google.gson.JsonObject; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class CheckInRequest implements LocoRequest { - - private int userId; - - private String os = KakaoDefaultValues.os; - - private int ntype = KakaoDefaultValues.ntype; - - private String appVer = KakaoApi.VERSION; - - private String lang = KakaoApi.LANGUAGE; - - private String MCCMNC = KakaoDefaultValues.MCCMNC; - - @Override - public String getMethod() { - return "CHECKIN"; - } - - @Override - public byte[] toBson() { - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("userId", userId); - jsonObject.addProperty("os", os); - jsonObject.addProperty("ntype", ntype); - jsonObject.addProperty("appVer", appVer); - jsonObject.addProperty("lang", lang); - jsonObject.addProperty("MCCMNC", MCCMNC); - - return BsonUtil.jsonObjectToBson(jsonObject); - } -} diff --git a/src/main/java/com/github/netricecake/message/request/LoginListRequest.java b/src/main/java/com/github/netricecake/message/request/LoginListRequest.java deleted file mode 100644 index b72fa9b..0000000 --- a/src/main/java/com/github/netricecake/message/request/LoginListRequest.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.github.netricecake.message.request; - -import com.github.netricecake.KakaoApi; -import com.github.netricecake.message.LocoRequest; -import com.github.netricecake.util.BsonUtil; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class LoginListRequest implements LocoRequest { - - private String appVer = KakaoApi.VERSION; - - private String prtVer = "1"; - - private String os = KakaoApi.AGENT; - - private String lang = "ko"; - - private String duuid; - - private int ntype = 0; // 0 : WIFI, 3: Cellular - - private String MCCMNC = "45006"; // 앞자리 세자리(한국) 450 고정, 뒤에 두자리 SKT: 05 KT: 08 LGU+: 06 ex) 45006 - - private int revision = 0; // TODO 이거뭐임 - - private JsonArray chatIds = new JsonArray(); - - private JsonArray maxIds = new JsonArray(); - - private int lastTokenId = 0; - - private int lbk = 0; // TODO 이거 뭐임 2 - - private JsonObject rp = new JsonObject(); // TODO 이거 뭐임 3 - - private boolean bg = true; // TODO 이거 뭐임 4 - - private String oauthToken; - - public LoginListRequest() { - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("base64", "AAD//wAA"); - jsonObject.addProperty("subType", "00"); - rp.add("$binary", jsonObject); - } - - @Override - public String getMethod() { - return "LOGINLIST"; - } - - @Override - public byte[] toBson() { - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("appVer", appVer); - jsonObject.addProperty("prtVer", prtVer); - jsonObject.addProperty("os", os); - jsonObject.addProperty("lang", lang); - jsonObject.addProperty("duuid", duuid); - jsonObject.addProperty("ntype", ntype); - jsonObject.addProperty("MCCMNC", MCCMNC); - jsonObject.addProperty("revision", revision); - jsonObject.add("chatIds", chatIds); - jsonObject.add("maxIds", maxIds); - jsonObject.addProperty("lastTokenId", lastTokenId); - jsonObject.addProperty("lbk", lbk); - jsonObject.add("rp", rp); - jsonObject.addProperty("bg", bg); - jsonObject.addProperty("oauthToken", oauthToken); - - return BsonUtil.jsonObjectToBson(jsonObject); - } - -} diff --git a/src/main/java/com/github/netricecake/message/request/MessageRequest.java b/src/main/java/com/github/netricecake/message/request/MessageRequest.java deleted file mode 100644 index 756eb92..0000000 --- a/src/main/java/com/github/netricecake/message/request/MessageRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.netricecake.message.request; - -import com.github.netricecake.message.LocoRequest; -import com.github.netricecake.util.BsonUtil; - -public class MessageRequest implements LocoRequest { - @Override - public String getMethod() { - return "MSG"; - } - - @Override - public byte[] toBson() { - return BsonUtil.jsonToBson("{ notiRead: false }"); - } -} diff --git a/src/main/java/com/github/netricecake/message/request/PingRequest.java b/src/main/java/com/github/netricecake/message/request/PingRequest.java deleted file mode 100644 index 1fbe9a0..0000000 --- a/src/main/java/com/github/netricecake/message/request/PingRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.github.netricecake.message.request; - -import com.github.netricecake.message.LocoRequest; -import com.github.netricecake.util.BsonUtil; - -public class PingRequest implements LocoRequest { - - - @Override - public String getMethod() { - return "PING"; - } - - @Override - public byte[] toBson() { - return BsonUtil.jsonToBson("{}"); - } -} diff --git a/src/main/java/com/github/netricecake/message/request/SetStatusRequest.java b/src/main/java/com/github/netricecake/message/request/SetStatusRequest.java deleted file mode 100644 index 9e78f1b..0000000 --- a/src/main/java/com/github/netricecake/message/request/SetStatusRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.netricecake.message.request; - -import com.github.netricecake.message.LocoRequest; -import com.github.netricecake.util.BsonUtil; -import com.google.gson.JsonObject; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class SetStatusRequest implements LocoRequest { - - /* - 카톡 켜고 있는지 안켜고 있는지 알리는 패킷인듯 - */ - - private int status; // 1 : 본다, 2 : 안본다 - - public SetStatusRequest(int atatus) { - this.status = atatus; - } - - @Override - public String getMethod() { - return "SETST"; - } - - @Override - public byte[] toBson() { - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("st", status); - return BsonUtil.jsonObjectToBson(jsonObject); - } -} diff --git a/src/main/java/com/github/netricecake/message/response/CheckInResponse.java b/src/main/java/com/github/netricecake/message/response/CheckInResponse.java deleted file mode 100644 index eb4a54b..0000000 --- a/src/main/java/com/github/netricecake/message/response/CheckInResponse.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.netricecake.message.response; - -import com.github.netricecake.message.LocoResponse; -import com.github.netricecake.util.BsonUtil; -import com.github.netricecake.util.ByteUtil; -import com.google.gson.JsonObject; -import lombok.Getter; - -@Getter -public class CheckInResponse implements LocoResponse { - - private String host; - - private String host6; - - private int port; - - private String cshost; - - private String cshost6; - - private int csport; - - private String vshost; - - private String vshost6; - - private int vsport; - - private int cacheExpire; - - private String MCCMNC; - - @Override - public String getMethod() { - return "CHECKIN"; - } - - @Override - public void fromBson(byte[] bson) { - JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); - this.host = jsonObject.get("host").getAsString(); - this.host6 = jsonObject.get("host6").getAsString(); - this.port = jsonObject.get("port").getAsInt(); - this.cshost = jsonObject.get("cshost").getAsString(); - this.cshost6 = jsonObject.get("cshost6").getAsString(); - this.csport = jsonObject.get("csport").getAsInt(); - this.vshost = jsonObject.get("vsshost").getAsString(); - this.vshost6 = jsonObject.get("vsshost6").getAsString(); - this.vsport = jsonObject.get("vssport").getAsInt(); - this.cacheExpire = jsonObject.get("cacheExpire").getAsInt(); - this.MCCMNC = jsonObject.get("MCCMNC").getAsString(); - } -} diff --git a/src/main/java/com/github/netricecake/message/response/GetConfResponse.java b/src/main/java/com/github/netricecake/message/response/GetConfResponse.java deleted file mode 100644 index a6d73fb..0000000 --- a/src/main/java/com/github/netricecake/message/response/GetConfResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.netricecake.message.response; - -import com.github.netricecake.message.LocoResponse; -import com.github.netricecake.util.BsonUtil; -import com.google.gson.JsonObject; -import lombok.Getter; - -@Getter -public class GetConfResponse implements LocoResponse { - - private String addr; - - private int port; - - @Override - public String getMethod() { - return "GetConf"; - } - - @Override - public void fromBson(byte[] bson) { - JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); - this.addr = jsonObject.get("ticket").getAsJsonObject().get("lsl").getAsJsonArray().get(0).getAsString(); - this.port = jsonObject.get("wifi").getAsJsonObject().get("ports").getAsJsonArray().get(0).getAsInt(); - } -} diff --git a/src/main/java/com/github/netricecake/message/response/LoginListResponse.java b/src/main/java/com/github/netricecake/message/response/LoginListResponse.java deleted file mode 100644 index 3c5fe78..0000000 --- a/src/main/java/com/github/netricecake/message/response/LoginListResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.netricecake.message.response; - -import com.github.netricecake.message.LocoResponse; -import com.github.netricecake.util.BsonUtil; -import com.google.gson.JsonObject; -import lombok.Getter; - -@Getter -public class LoginListResponse implements LocoResponse { - - private int status; - - @Override - public String getMethod() { - return "LOGINLIST"; - } - - @Override - public void fromBson(byte[] bson) { - // 너무 많음;; - JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); - this.status = jsonObject.get("status").getAsInt(); - } - -} diff --git a/src/main/java/com/github/netricecake/message/response/MessageResponse.java b/src/main/java/com/github/netricecake/message/response/MessageResponse.java deleted file mode 100644 index cb3055f..0000000 --- a/src/main/java/com/github/netricecake/message/response/MessageResponse.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.netricecake.message.response; - -import com.github.netricecake.message.LocoResponse; -import com.github.netricecake.util.BsonUtil; -import com.google.gson.JsonObject; -import lombok.Getter; - -@Getter -public class MessageResponse implements LocoResponse { - - private long chatId; - private long logId; - private int type; - private long authorId; - private String message; - - @Override - public String getMethod() { - return "MSG"; - } - - @Override - public void fromBson(byte[] bson) { - JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); - chatId = jsonObject.get("chatId").getAsLong(); - logId = jsonObject.get("logId").getAsLong(); - type = jsonObject.get("chatLog").getAsJsonObject().get("type").getAsInt(); - message = jsonObject.get("chatLog").getAsJsonObject().get("message").getAsString(); - } -} diff --git a/src/main/java/com/github/netricecake/message/response/PingResponse.java b/src/main/java/com/github/netricecake/message/response/PingResponse.java deleted file mode 100644 index 9f0e381..0000000 --- a/src/main/java/com/github/netricecake/message/response/PingResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.netricecake.message.response; - -import com.github.netricecake.message.LocoResponse; -import com.github.netricecake.util.BsonUtil; -import com.google.gson.JsonObject; -import lombok.Getter; - -@Getter -public class PingResponse implements LocoResponse { - - private int status; - - @Override - public String getMethod() { - return "PING"; - } - - @Override - public void fromBson(byte[] bson) { - JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); - this.status = jsonObject.get("status").getAsInt(); - } -} diff --git a/src/main/java/com/github/netricecake/message/response/SetStatusResponse.java b/src/main/java/com/github/netricecake/message/response/SetStatusResponse.java deleted file mode 100644 index e0b9f9e..0000000 --- a/src/main/java/com/github/netricecake/message/response/SetStatusResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.github.netricecake.message.response; - -import com.github.netricecake.message.LocoResponse; -import com.github.netricecake.util.BsonUtil; -import com.google.gson.JsonObject; -import lombok.Getter; -import lombok.Setter; - -@Getter -public class SetStatusResponse implements LocoResponse { - - private int status; - - @Override - public String getMethod() { - return "SETST"; - } - - @Override - public void fromBson(byte[] bson) { - JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson); - status = jsonObject.get("status").getAsInt(); - } -} diff --git a/src/main/java/com/github/netricecake/network/LocoPacketHandler.java b/src/main/java/com/github/netricecake/network/LocoPacketHandler.java deleted file mode 100644 index c4b404f..0000000 --- a/src/main/java/com/github/netricecake/network/LocoPacketHandler.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.netricecake.network; - -@FunctionalInterface -public interface LocoPacketHandler { - void onPacket(LocoPacket packet); -} diff --git a/src/main/java/com/github/netricecake/network/LocoSocket.java b/src/main/java/com/github/netricecake/network/LocoSocket.java deleted file mode 100644 index d2ab52c..0000000 --- a/src/main/java/com/github/netricecake/network/LocoSocket.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.github.netricecake.network; - -import com.github.netricecake.crypto.CryptoManager; -import com.github.netricecake.network.codec.LocoCodec; -import com.github.netricecake.network.codec.SecureLayerCodec; -import io.netty.bootstrap.Bootstrap; -import io.netty.channel.*; -import io.netty.channel.nio.NioIoHandler; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.codec.bytes.ByteArrayDecoder; -import io.netty.handler.codec.bytes.ByteArrayEncoder; -import lombok.Getter; - -import java.net.InetSocketAddress; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; - -public class LocoSocket { - - @Getter - private String ip; - @Getter - private int port; - - private CryptoManager cryptoManager; - - private Channel channel; - private EventLoopGroup eventLoopGroup; - - @Getter - private boolean alive = false; - - private BlockingQueue locoPacketQueue = new LinkedBlockingQueue<>(); - - public LocoSocket(String ip, int port) { - this.ip = ip; - this.port = port; - cryptoManager = new CryptoManager(); - } - - public void connect() throws Exception { - byte[] handshakePacket = cryptoManager.generateHandshakeMessage(); - eventLoopGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); - Bootstrap bootstrap = new Bootstrap(); - bootstrap.remoteAddress(new InetSocketAddress(ip, port)) - .group(eventLoopGroup) - .channel(NioSocketChannel.class) - .handler(new ChannelInitializer() { - @Override - protected void initChannel(SocketChannel socketChannel) throws Exception { - ChannelPipeline pipeline = socketChannel.pipeline(); - pipeline.addLast(new ByteArrayEncoder()); - pipeline.addLast(new ByteArrayDecoder()); - } - }); - channel = bootstrap.connect().sync().channel(); - alive = true; - channel.writeAndFlush(handshakePacket).sync(); - channel.pipeline().addLast(new SecureLayerCodec(cryptoManager)); - channel.pipeline().addLast(new LocoCodec(locoPacketQueue)); - new Thread() { - @Override - public void run() { - try { - channel.closeFuture().sync(); - eventLoopGroup.shutdownGracefully(); - locoPacketQueue.offer(null); - alive = false; - } catch (Exception e) {} - } - }.start(); - } - - public void write(LocoPacket packet) { - if (!alive) return; - channel.writeAndFlush(packet); - } - - public LocoPacket read() throws Exception { - if (!alive) return null; - return locoPacketQueue.poll(100000, TimeUnit.SECONDS); - } - - public void close() { - channel.close(); - eventLoopGroup.shutdownGracefully(); - alive = false; - } - -} diff --git a/src/main/resources/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF deleted file mode 100644 index f7fb8a9..0000000 --- a/src/main/resources/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0 -Main-Class: com.github.netricecake.Main - diff --git a/test.png b/test.png deleted file mode 100644 index bd33f94ad633fb6121ff05c1c19f9198d34a7b96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3873 zcmZ`+c{mha_a9lZ4#v=k(1-36@O366R!K0-*MaFES@=KE@Y}0f353o_#bsbIygg^auk0fV97j zr9GthDs%AO3SsOLd3j|bAkbz03CH?8aUK@~$rZq2mF;?h9=<6ixm>7@m41h);d{os0GuBP2FrN*@tV7M7bh)` zkkoF&v|q3S%Z^? ziT#U`cTKGzZ3qNAh(K+!nDv1RN7;(!Co^?qiUPzw?{j>v6uD%1%`)yQBlV`*lKy6M-a{<}+=$fPOtWW_QHqtHaUpDt=(&UaP861DEF^(knk&S@L{c{bB3;S$SX_-E= zU+d}#a&{FY5Hl&(Lt;B)f-mQc)}p4qILGgU%5Ug+`JXro2LA7&{c4poNaX}TPdMaK zDNBBww4DMkGfzqoD$uKtVXnWS-RW%9E!K&m*TxA2Tsh-U`Fbv8UVy19LZk+D!aO#H zw^cfIp>g`0q`+GGOZOcU#pg%)#Vf~vAD-$*P&Y=v2%auTZ}@V13FRLrnsQ}TT{*(l zTJuiWsgR=DSpLDgrW{VTeJ>#pXQe9j7P#z1IL26Sa6HpTtGJ66>#KVySGC+|_Eb7ai1AM3>5Pw%vxH^;=M06qXxmDG#pJRyW@xPKc>R)Uk+I9^@A#d*@?5HqCJi@AteTE3Y2o7L zcSjPnZ=X1~H^8G$9C<0F;5wkZys~KF^9A-$s<)?gR`>Ib>1)@yjW0DGv_7KOJjV%B z=@PE#IY25gRU|>9P8!s(cQ;P&u41q2T9+lc>eygO%=kiFjOee`Ko=x-zN(=8PQfs@ z!D6@GN0Td}>cmv*%N$DS9@kBfrDsP$JyoAi-voS0s{fPt{dmvfg{?p=)bpKV^Ak9wKk0|7Md*X zNUEM5OQ$sCwiFTJ#8d)re4RXj*)!pE6~K#yLRq~aLV}_I=Dq6x{DmDy)4w78FNOcU z_1=!OhUqk1Hj%{^W+c#beP#m8ep=4qjNcDWN~w`r>n2^6Mbom}CVt{-GdZp6(H4PV zeZdYbo`IO++Ep!*<3uUKQ9(2E9{1Zbx%^jih9iYfl5PD5WsRDp-4z2NPs8cKr?6_>D6G8NMmclqJyR4JZ{Qc zkN!daFj{o&O3j@yG5)EA-T>c_$)tIHzBF|Lf!kV(cqFp^?2}vdC7}^DRM0vbD@I4l z@Ft&y{jI0br;MZlfqL=JUKtu$#7Z7+^@zTc zxJHEYevekNYRqa(nhbz)0@>_$VO7We4ir+eL@T|Lm&`DdY7YuVAlhEkr~!YBkauye zt))^NfL8;=5VtvrO@Kg>Vh(|j_Sdz|ju|zs;y0+4O`<{_yLZ%2SVEMinBaX!zdP5o zGn*)t&sX_uHo`))``!r(ie}>=u#ztt>E)z`^n0B;(VI1zQ0TKSytBR%ls+Sbo#=bC zhq)Blu)xk>$Q*IiB_Cb-{T-d9T65aMW!9#tm^?L_bVboeh8`ddRfG1xGt!b3qiBxA z@$Ko$aAGl8>L0o46TH~(RQ^eP+*$!W5DLXIt=CNPS*WT_wP_kWJ%1h!@uC z#3MZ&E@$swnY~$nZ!V)#mHCcn`9!Pj+#(huH}Eda(UzIUE%q?RVV8mA;p8H?TfXc{ z1B8-;)ABL!tdHfFekO!@M6izkq1K;eYU`HQy}kfC&Je$$UHZd=$s1&?5~L45q*>%@ zJD`o&9kfsirLC7w&r$X2szX0!^I`v_8Rgo(PTP>not8ewAr ztX>_1NjaDrJ0^(okM`6vzvX53hPvD4hh#r!z1nQ;bED6;_tz-%>~7-{M&G)}S_4GJ zLL2L&DwMWYzLEo}>4ai;-?-Zm?YivtQ8;@f1)e*-wz=FHXRw-V_+aFH6(e5K=G18Y z0OmJYIs52$;}Ry784V5+!y4zQ{ur(HB7}Rm%NFmer>rhE>A?2U_Ydm%vuXHX-kFI; z(82dAq)0418*S^7uR()E1%|+$UmqJ~NQ6$IwryHM9nlb>Y}pC#tJy55w~N%p!J*4H zcYn(rD9dDRxnXHB}Sa{ zDw%WO4g_M$Op6E$x-o2GT%i}zMMGzem0~=crNEugd#|5TteOWeKaHTUm6Y&Qn3al^ zJz%qP8p2S#tR6^NmEo6|FHu)UFod;<`!Sz2{*cuJ$X5QPU?T3jkz4}5p=9fmm_><^ z8jd{^51Culn5&kf67U zHv~Dn?5<#Oy8BU(E{sj*q!}ML3#E<{rkIh{ls&fzb3ezAl5;gl#+k4i)i^p!CGVBA zvS=a0=>(X89U|*>7Md4ebL#HY&zO0cdvm_vUcbWChM5G$#K{l~%X`nu77plr_>lqD zxh5ZXnAN?|gZG&POVd#}NBp0EQXGVMO$rJ`Qn<~5Yz{))mXA-IOPg!osALF*=R(bgQ)h={N5Mzw;d_GsYWS|A;~2L% z;cVdM_%QJ99tF3-1hD3W?z-9>Kl?OKGQ136EXVoJq>_{cI?b@>LXLM9CHVDwliR02 z%d%YAZj@D}-mvMy!=lTIqHYY`^)@^eYiM`LHT&d^qWTH;gFRq7o;s`KQnmIm`Wo|l zmtKP_bMQzLvHv1`1E(HL8&p1_+5o>ZWqq7mxYL_ID=Fm>HtFsSeY55}f>S!G-?tfn zwmu0&duMl3=?*eB{`tX_l)@!mY+^tE?CJd?EZF`N_uCrJ-))IP0gjn#B__z%1@NRW zpIr1-PX2C9RxJ3OT;l$L8C2J0xNwL)_)~8O!aM8W-B% zcFcHA?796M6+9oFDK-(JLYsV&&hTH}AKV)LEEJG?Ciam9!pa&-ygS#Y%EZm&!B3kC z(%JgMYYOL!V1q(qjQo%y(_KIwSIqYDPX5u)I(JS;&qUg;;`fJ;xw}3elbz3K%PjnBbXvwpw_ZX>PP$(*^PjbLCjL% g|7y1XBzU{Gkh04^pDDX&Gs{|lm6;uaYT}jfKO!P8n*aa+