diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index b2abaf15..00000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/client/src/main/java/jdash/client/GDClient.java b/client/src/main/java/jdash/client/GDClient.java
index 1622f438..30432642 100644
--- a/client/src/main/java/jdash/client/GDClient.java
+++ b/client/src/main/java/jdash/client/GDClient.java
@@ -403,12 +403,29 @@ public Mono downloadWeeklyDemon() {
return downloadLevel(-2);
}
- private Mono getTimelyInfo(int weekly) {
- return Mono.defer(() -> GDRequest.of(GET_GJ_DAILY_LEVEL)
- .addParameters(commonParams())
- .addParameter("weekly", weekly)
- .execute(cache, router)
- .deserialize(dailyInfoResponse()));
+ /**
+ * Downloads full data of the current Event level. It is a shorthand for:
+ *
+ * downloadLevel(-3)
+ *
+ *
+ * @return a Mono emitting the {@link GDLevelDownload} corresponding to the level. A {@link GDClientException} will
+ * be emitted if an error occurs.
+ */
+ public Mono downloadEventLevel() {
+ return downloadLevel(-3);
+ }
+
+ private Mono getDailyInfo(int type, @Nullable SecretRewardChkGenerator chkGenerator) {
+ return Mono.defer(() -> {
+ final var request = GDRequest.of(GET_GJ_DAILY_LEVEL)
+ .addParameters(commonParams())
+ .addParameter("type", type);
+ if (chkGenerator != null) {
+ request.addParameter("chk", chkGenerator.get());
+ }
+ return request.execute(cache, router).deserialize(dailyInfoResponse());
+ });
}
/**
@@ -418,7 +435,7 @@ private Mono getTimelyInfo(int weekly) {
* if an error occurs.
*/
public Mono getDailyLevelInfo() {
- return getTimelyInfo(0);
+ return getDailyInfo(0, null);
}
/**
@@ -428,7 +445,29 @@ public Mono getDailyLevelInfo() {
* if an error occurs.
*/
public Mono getWeeklyDemonInfo() {
- return getTimelyInfo(1);
+ return getDailyInfo(1, null);
+ }
+
+ /**
+ * Requests information on the current Event level, such as its number or the time left before the next one. Rewards
+ * CHK is generated at random.
+ *
+ * @return a Mono emitting the {@link GDDailyInfo} of the Event level. A {@link GDClientException} will be emitted
+ * if an error occurs.
+ */
+ public Mono getEventLevelInfo() {
+ return getDailyInfo(2, SecretRewardChkGenerator.random());
+ }
+
+ /**
+ * Requests information on the current Event level, such as its number or the time left before the next one.
+ *
+ * @param chkGenerator customize the way CHK is generated
+ * @return a Mono emitting the {@link GDDailyInfo} of the Event level. A {@link GDClientException} will be emitted
+ * if an error occurs.
+ */
+ public Mono getEventLevelInfo(SecretRewardChkGenerator chkGenerator) {
+ return getDailyInfo(2, Objects.requireNonNull(chkGenerator));
}
/**
diff --git a/client/src/main/java/jdash/client/request/GDRequests.java b/client/src/main/java/jdash/client/request/GDRequests.java
index 20e392bb..945c3462 100644
--- a/client/src/main/java/jdash/client/request/GDRequests.java
+++ b/client/src/main/java/jdash/client/request/GDRequests.java
@@ -31,7 +31,7 @@ public final class GDRequests {
/* Params */
public static final String GAME_VERSION = "22";
- public static final String BINARY_VERSION = "42";
+ public static final String BINARY_VERSION = "45";
public static final String SECRET = "Wmfd2893gb7";
private GDRequests() {
diff --git a/client/src/main/java/jdash/client/request/GDRouterImpl.java b/client/src/main/java/jdash/client/request/GDRouterImpl.java
index 494821f2..eb0ed2c8 100644
--- a/client/src/main/java/jdash/client/request/GDRouterImpl.java
+++ b/client/src/main/java/jdash/client/request/GDRouterImpl.java
@@ -37,6 +37,7 @@ public GDRouterImpl(RequestLimiter limiter, Duration timeout, String baseUrl, Sc
.headers(h -> {
h.add("Content-Type", "application/x-www-form-urlencoded");
h.add("User-Agent", "");
+ h.add("Cookie", "gd=1;");
});
if (baseUrl.startsWith("https://")) {
httpClient = httpClient.secure();
diff --git a/client/src/main/java/jdash/client/response/DailyInfoResponseDeserializer.java b/client/src/main/java/jdash/client/response/DailyInfoResponseDeserializer.java
index 401a2c77..a03f533b 100644
--- a/client/src/main/java/jdash/client/response/DailyInfoResponseDeserializer.java
+++ b/client/src/main/java/jdash/client/response/DailyInfoResponseDeserializer.java
@@ -10,7 +10,7 @@ class DailyInfoResponseDeserializer implements Function {
@Override
public GDDailyInfo apply(String response) {
- if (!response.matches("\\d+\\|\\d+")) {
+ if (!response.matches("\\d+\\|\\d+(|.*)*")) {
throw new ActionFailedException(response, "Failed to load daily level info");
}
final var tokens = response.split("\\|");
diff --git a/client/src/test/java/jdash/client/GDClientTest.java b/client/src/test/java/jdash/client/GDClientTest.java
index 1bd537c8..fd70f75c 100644
--- a/client/src/test/java/jdash/client/GDClientTest.java
+++ b/client/src/test/java/jdash/client/GDClientTest.java
@@ -25,7 +25,7 @@ public final class GDClientTest {
/* Not part of unit tests, this is only to test the real router implementation */
public static void main(String[] args) {
final var client = GDClient.create();
- System.out.println(client.getSongInfo(10010067).block());
+ System.out.println(client.getEventLevelInfo().block());
}
@BeforeEach
@@ -329,6 +329,13 @@ public void getWeeklyDemonInfoTest() {
assertEquals(expected, actual);
}
+ @Test
+ public void getEventLevelTest() {
+ final var expected = new GDDailyInfo(1, Duration.ofSeconds(10));
+ final var actual = client.getEventLevelInfo(SecretRewardChkGenerator.fixed("ALEX1", "123456")).block();
+ assertEquals(expected, actual);
+ }
+
@Test
public void getCommentsForLevelTest() {
final var expectedFirstComment = new GDComment(
diff --git a/client/src/test/java/jdash/client/GDRouterMock.java b/client/src/test/java/jdash/client/GDRouterMock.java
index 01a9ea38..3d7060c2 100644
--- a/client/src/test/java/jdash/client/GDRouterMock.java
+++ b/client/src/test/java/jdash/client/GDRouterMock.java
@@ -61,10 +61,14 @@ public final class GDRouterMock implements GDRouter {
.addParameter("levelID", 10565740), "downloadLevel"),
Map.entry(GDRequest.of(GET_GJ_DAILY_LEVEL)
.addParameters(commonParams())
- .addParameter("weekly", 0), "getDailyLevelInfo"),
+ .addParameter("type", 0), "getDailyLevelInfo"),
Map.entry(GDRequest.of(GET_GJ_DAILY_LEVEL)
.addParameters(commonParams())
- .addParameter("weekly", 1), "getWeeklyDemonInfo"),
+ .addParameter("type", 1), "getWeeklyDemonInfo"),
+ Map.entry(GDRequest.of(GET_GJ_DAILY_LEVEL)
+ .addParameters(commonParams())
+ .addParameter("type", 2)
+ .addParameter("chk", "ALEX1BAsCDAcD"), "getEventLevelInfo"),
Map.entry(GDRequest.of(GET_GJ_COMMENTS_21)
.addParameters(commonParams())
.addParameter("levelID", 10565740)
diff --git a/client/src/test/resources/getEventLevelInfo.txt b/client/src/test/resources/getEventLevelInfo.txt
new file mode 100644
index 00000000..b5f1d4e2
--- /dev/null
+++ b/client/src/test/resources/getEventLevelInfo.txt
@@ -0,0 +1 @@
+200001|10|SXDEsWE8DW2YPCAMLBgAPCwoCDwoLAB4BCR0OHgQVAAseBA==|2add3c41e4f44884d7200dd98c57feb1b07296df
\ No newline at end of file
diff --git a/common/src/main/java/jdash/common/RobTopsWeakEncryption.java b/common/src/main/java/jdash/common/RobTopsWeakEncryption.java
index b505be8c..4fc6e0e2 100644
--- a/common/src/main/java/jdash/common/RobTopsWeakEncryption.java
+++ b/common/src/main/java/jdash/common/RobTopsWeakEncryption.java
@@ -4,30 +4,31 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
+import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
-import static jdash.common.internal.InternalUtils.b64Decode;
-import static jdash.common.internal.InternalUtils.b64Encode;
+import static jdash.common.internal.InternalUtils.*;
/**
* Provides methods to encode and decode some data such as private message body, level passcodes and account passwords.
*/
public final class RobTopsWeakEncryption {
- private static final XORCipher GD_MESSAGE_BODY_XOR_CIPHER = new XORCipher("14251");
- private static final XORCipher LEVEL_PASSCODE_XOR_CIPHER = new XORCipher("26364");
- private static final XORCipher ACCOUNT_PASSWORD_XOR_CIPHER = new XORCipher("37526");
- private static final XORCipher CHK_XOR_CIPHER = new XORCipher("58281");
+ static final XORCipher GD_MESSAGE_BODY_XOR_CIPHER = new XORCipher("14251");
+ static final XORCipher LEVEL_PASSCODE_XOR_CIPHER = new XORCipher("26364");
+ static final XORCipher ACCOUNT_PASSWORD_XOR_CIPHER = new XORCipher("37526");
+ static final XORCipher CHK_XOR_CIPHER = new XORCipher("58281");
+ static final XORCipher CHK_REWARDS_XOR_CIPHER = new XORCipher("59182");
private RobTopsWeakEncryption() {
}
- private static String decode(String str, XORCipher algorithm) {
+ static String decode(String str, XORCipher algorithm) {
return algorithm.cipher(b64Decode(str));
}
- private static String encode(String str, XORCipher algorithm) {
+ static String encode(String str, XORCipher algorithm) {
return b64Encode(algorithm.cipher(str));
}
@@ -103,6 +104,15 @@ public static String encodeChk(Object... params) {
.collect(Collectors.joining())), CHK_XOR_CIPHER);
}
+ /**
+ * Generates a valid CHK to get secret rewards (chests or Event level rewards).
+ *
+ * @return the encoded string
+ */
+ public static String generateSecretRewardChk() {
+ return randomString(5) + encode("" + (new Random().nextInt(900000) + 100000), CHK_REWARDS_XOR_CIPHER);
+ }
+
/**
* Encodes the given password into a string that can be passed as the gjp2 parameter for authenticated requests.
*
diff --git a/common/src/main/java/jdash/common/SecretRewardChkGenerator.java b/common/src/main/java/jdash/common/SecretRewardChkGenerator.java
new file mode 100644
index 00000000..391eab7f
--- /dev/null
+++ b/common/src/main/java/jdash/common/SecretRewardChkGenerator.java
@@ -0,0 +1,39 @@
+package jdash.common;
+
+import java.util.function.Supplier;
+
+import static jdash.common.RobTopsWeakEncryption.CHK_REWARDS_XOR_CIPHER;
+import static jdash.common.RobTopsWeakEncryption.encode;
+
+/**
+ * Function that generates a client CHK for secret rewards. Use {@link #random()} or {@link #fixed(String, String)} to
+ * customize
+ * CHK generation behavior.
+ */
+@FunctionalInterface
+public interface SecretRewardChkGenerator extends Supplier {
+
+ /**
+ * Function that generates a CHK at random, using a key representing a random number between 100_000 (inclusive) and
+ * 900_000 (exclusive).
+ *
+ * @return a random {@link SecretRewardChkGenerator}
+ */
+ static SecretRewardChkGenerator random() {
+ return RobTopsWeakEncryption::generateSecretRewardChk;
+ }
+
+ /**
+ * Function that generates a CHK with a fixed input. Use this when you need deterministic behavior.
+ *
+ * @param prefix a string that will be prepended to the CHK. Only the first 5 characters are taken, and will be
+ * padded with 'X' if provided prefix is less than 5 characters.
+ * @param key the key encoded in the CHK. It is recommended to be 6 characters or highers for the server to
+ * accept it.
+ * @return a {@link SecretRewardChkGenerator} with fixed input
+ */
+ static SecretRewardChkGenerator fixed(String prefix, String key) {
+ return () -> String.format("%5s", prefix.substring(0, 5)).replace(' ', 'X') +
+ encode(key, CHK_REWARDS_XOR_CIPHER);
+ }
+}
diff --git a/common/src/test/java/jdash/common/RobTopsWeakEncryptionTest.java b/common/src/test/java/jdash/common/RobTopsWeakEncryptionTest.java
index e4d71254..7fbfe2ba 100644
--- a/common/src/test/java/jdash/common/RobTopsWeakEncryptionTest.java
+++ b/common/src/test/java/jdash/common/RobTopsWeakEncryptionTest.java
@@ -1,5 +1,6 @@
package jdash.common;
+import jdash.common.internal.InternalUtils;
import org.junit.jupiter.api.Test;
import static jdash.common.RobTopsWeakEncryption.*;
@@ -39,8 +40,12 @@ public void encodePrivateMessageBodyTest() {
@Test
public void encodeChkTest() {
+ System.out.println(new XORCipher("59482").cipher(InternalUtils.b64Decode("BgkBCgYNCAEIAgUJ")));
+
assertEquals("BFxRDAIFDlZeV1cKAAFVBwoADANRXQoKAAZbAAkAVA9UCwBTCgpeCQ==",
- encodeChk(69101896, 2, "0ej5u5v6Nl", 98006, "00000000-24d6-c259-ffff-ffffddb46189", 4063664,
- "ysg6pUrtjn0J"));
+ encodeChk(69101896, 2, "0ej5u5v6Nl", 98006,
+ "00000000-24d6-c259-ffff" + "-ffffddb46189", 4063664, "ysg6pUrtjn0J"));
}
+
+
}
\ No newline at end of file
diff --git a/common/src/test/java/jdash/common/SecretRewardChkGeneratorTest.java b/common/src/test/java/jdash/common/SecretRewardChkGeneratorTest.java
new file mode 100644
index 00000000..231d43bc
--- /dev/null
+++ b/common/src/test/java/jdash/common/SecretRewardChkGeneratorTest.java
@@ -0,0 +1,13 @@
+package jdash.common;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public final class SecretRewardChkGeneratorTest {
+
+ @Test
+ public void testFixedChkGenerator() {
+ assertEquals("ALEX1BAsCDAcD", SecretRewardChkGenerator.fixed("ALEX1", "123456").get());
+ }
+}
\ No newline at end of file
diff --git a/events/src/main/java/jdash/events/object/EventLevelChange.java b/events/src/main/java/jdash/events/object/EventLevelChange.java
new file mode 100644
index 00000000..77e7a842
--- /dev/null
+++ b/events/src/main/java/jdash/events/object/EventLevelChange.java
@@ -0,0 +1,11 @@
+package jdash.events.object;
+
+import jdash.common.entity.GDDailyInfo;
+
+/**
+ * Event emitted when the Event level changes.
+ *
+ * @param before The Event level info before the change.
+ * @param after The Event level info after the change.
+ */
+public record EventLevelChange(GDDailyInfo before, GDDailyInfo after) {}
diff --git a/events/src/main/java/jdash/events/producer/EventLevelProducer.java b/events/src/main/java/jdash/events/producer/EventLevelProducer.java
new file mode 100644
index 00000000..8fc2c5e9
--- /dev/null
+++ b/events/src/main/java/jdash/events/producer/EventLevelProducer.java
@@ -0,0 +1,25 @@
+package jdash.events.producer;
+
+import jdash.client.GDClient;
+import jdash.common.entity.GDDailyInfo;
+import jdash.events.object.EventLevelChange;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+class EventLevelProducer implements GDEventProducer {
+
+ private GDDailyInfo previousEventLevel;
+
+ @Override
+ public Flux