Compare commits
153 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cbb70a07b3 | ||
|
78d98b5b41 | ||
|
f667a99076 | ||
|
29fdd55405 | ||
|
bf77691f86 | ||
|
70fb94df97 | ||
|
ffa33ac046 | ||
|
75d4791f98 | ||
|
072be4c40f | ||
|
99eaebc622 | ||
|
84ad72f4cb | ||
|
0b76d5ada4 | ||
|
a14f8e1f0e | ||
|
e5712361e0 | ||
|
c5d3627970 | ||
|
88f68f96f3 | ||
|
e27ffc8d92 | ||
|
29cb9f6078 | ||
|
dd70f2492c | ||
|
677ceb70a1 | ||
|
58770ca649 | ||
|
171f07ccec | ||
|
24b4387b08 | ||
|
a8510ed336 | ||
|
abf9f28484 | ||
|
708fcbd1e4 | ||
|
59027e8e62 | ||
|
1eb1d7b95f | ||
|
edb8cec873 | ||
|
3bed3052d0 | ||
|
705e5ca65e | ||
|
0a74e1ab1a | ||
|
5dc543f090 | ||
|
ba093e4c27 | ||
|
03b8cfa579 | ||
|
a93d6d4e24 | ||
|
116e082d56 | ||
|
2b2e690da4 | ||
|
b322400d87 | ||
|
83c064220f | ||
|
dfc393c953 | ||
|
41be43d711 | ||
|
c36812e052 | ||
|
e85a1de781 | ||
|
8651ce3e97 | ||
|
5e40530a20 | ||
|
83613b2d01 | ||
|
bd463a74d2 | ||
|
e9cbfaaa39 | ||
|
aa12013479 | ||
|
0055c54826 | ||
|
202a90846b | ||
|
a77442bae5 | ||
|
d2f74d7bbb | ||
|
fd658e5e6c | ||
|
3eea163a73 | ||
|
e41ee220cd | ||
|
ab080b9838 | ||
|
4d8e5fd3be | ||
|
d931217e81 | ||
|
07c22d39f2 | ||
|
6339f78db8 | ||
|
77593c2722 | ||
|
cf724d4b51 | ||
|
84bee0da20 | ||
|
79a0344921 | ||
|
cdf077fddd | ||
|
aa18865601 | ||
|
ca536b788b | ||
|
8ab21d8d7a | ||
|
da1bab9adc | ||
|
faeda1b693 | ||
|
5282a9d1cd | ||
|
cc3bdc76c5 | ||
|
272f7dfe75 | ||
|
be144b9d1b | ||
|
b44fb2b20c | ||
|
2a390a60bb | ||
|
afa70c75d9 | ||
|
7b25092fa8 | ||
|
8645231031 | ||
|
5cf00542da | ||
|
c92f0aa589 | ||
|
9a2015ef56 | ||
|
830cc51012 | ||
|
d7ffd8bfa2 | ||
|
cf6d9875d4 | ||
|
a3c27a76aa | ||
|
e61f5d04b9 | ||
|
5e2773a23e | ||
|
958d04519b | ||
|
0d5d4fb6a2 | ||
|
2527682d00 | ||
|
9d92d7b015 | ||
|
eb2ccf6b04 | ||
|
941ff5bc87 | ||
|
84910b7488 | ||
|
aa887ba954 | ||
|
344be2b320 | ||
|
68e904681d | ||
|
ee6a0534a8 | ||
|
5991b116f3 | ||
|
3a74997b49 | ||
|
76ba67b760 | ||
|
797808114c | ||
|
d4615b2cb4 | ||
|
e1fee1f90d | ||
|
7e166b0920 | ||
|
f903035643 | ||
|
79bfd5d95c | ||
|
101e9a814e | ||
|
788101aa0f | ||
|
2e21f765ab | ||
|
2d0ab31fd0 | ||
|
48fbca5fad | ||
|
4c4b7a3677 | ||
|
3dd6241e2c | ||
|
a140e7a2b1 | ||
|
006974ba23 | ||
|
f48a1d321b | ||
|
799fd4149c | ||
|
be89c549ef | ||
|
3cd57bf61f | ||
|
37d3355ca4 | ||
|
07c1e6c836 | ||
|
735fccf043 | ||
|
e723cc6d98 | ||
|
602a63fad1 | ||
|
2156ec9ed7 | ||
|
fd0bfda2eb | ||
|
172c770524 | ||
|
5b9fec980e | ||
|
4bbb9cd762 | ||
|
473783b501 | ||
|
af96cfb7dc | ||
|
ede105a6ea | ||
|
fd48777071 | ||
|
e44df86246 | ||
|
e76a596b85 | ||
|
430dbeb261 | ||
|
ee19a97b00 | ||
|
8b0220ccfc | ||
|
64cd9d4a9e | ||
|
a33a7f676a | ||
|
da61270350 | ||
|
f0d5706d77 | ||
|
815876e7da | ||
|
07c6bd1140 | ||
|
2dc4a35d9f | ||
|
c3912c2edf | ||
|
c7f696706b | ||
|
251ee4951a | ||
|
0bb4856c7e |
4
.github/workflows/maven-publish.yml
vendored
4
.github/workflows/maven-publish.yml
vendored
@ -27,11 +27,11 @@ jobs:
|
||||
export REVISION=${{ github.run_number }}
|
||||
|
||||
echo "REVISION=$REVISION" >> $GITHUB_ENV
|
||||
- name: Set up JDK 15
|
||||
- name: Set up JDK 17
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 15
|
||||
java-version: 17
|
||||
server-id: mchv-release-distribution
|
||||
server-username: MAVEN_USERNAME
|
||||
server-password: MAVEN_PASSWORD
|
||||
|
710
pom.xml
710
pom.xml
@ -1,293 +1,431 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>it.tdlight</groupId>
|
||||
<artifactId>tdlib-session-container</artifactId>
|
||||
<version>5.0.${revision}</version>
|
||||
<name>TDLib Session Container</name>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<revision>0-SNAPSHOT</revision>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>it.tdlight</groupId>
|
||||
<artifactId>tdlib-reactive-api</artifactId>
|
||||
<version>6.0.${revision}</version>
|
||||
<name>TDLib Reactive API</name>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<revision>0-SNAPSHOT</revision>
|
||||
|
||||
<vertx.version>4.2.0</vertx.version>
|
||||
</properties>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>protoarch</id>
|
||||
<name>protoarch</name>
|
||||
<url>http://home.apache.org/~aajisaka/repository</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>mchv-release</id>
|
||||
<name>MCHV Release Apache Maven Packages</name>
|
||||
<url>https://mvn.mchv.eu/repository/mchv</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>mchv-snapshot</id>
|
||||
<name>MCHV Snapshot Apache Maven Packages</name>
|
||||
<url>https://mvn.mchv.eu/repository/mchv-snapshot</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>mchv-release-distribution</id>
|
||||
<name>MCHV Release Apache Maven Packages Distribution</name>
|
||||
<url>https://mvn.mchv.eu/repository/mchv</url>
|
||||
</repository>
|
||||
<snapshotRepository>
|
||||
<id>mchv-snapshot-distribution</id>
|
||||
<name>MCHV Snapshot Apache Maven Packages Distribution</name>
|
||||
<url>https://mvn.mchv.eu/repository/mchv-snapshot</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
<scm>
|
||||
<connection>scm:git:https://git.ignuranza.net/tdlight-team/tdlib-session-container.git</connection>
|
||||
<developerConnection>scm:git:https://git.ignuranza.net/tdlight-team/tdlib-session-container.git
|
||||
</developerConnection>
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-stack-depchain</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.unimi.dsi</groupId>
|
||||
<artifactId>fastutil</artifactId>
|
||||
<version>8.5.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.tdlight</groupId>
|
||||
<artifactId>tdlight-java</artifactId>
|
||||
<version>2.7.9.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<record.builder.version>34</record.builder.version>
|
||||
</properties>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>mchv-release</id>
|
||||
<name>MCHV Release Apache Maven Packages</name>
|
||||
<url>https://mvn.mchv.eu/repository/mchv</url>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>mchv-release</id>
|
||||
<name>MCHV Release Apache Maven Packages</name>
|
||||
<url>https://mvn.mchv.eu/repository/mchv</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>mchv-snapshot</id>
|
||||
<name>MCHV Snapshot Apache Maven Packages</name>
|
||||
<url>https://mvn.mchv.eu/repository/mchv-snapshot</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>mchv-release-distribution</id>
|
||||
<name>MCHV Release Apache Maven Packages Distribution</name>
|
||||
<url>https://mvn.mchv.eu/repository/mchv</url>
|
||||
</repository>
|
||||
<snapshotRepository>
|
||||
<id>mchv-snapshot-distribution</id>
|
||||
<name>MCHV Snapshot Apache Maven Packages Distribution</name>
|
||||
<url>https://mvn.mchv.eu/repository/mchv-snapshot</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
<scm>
|
||||
<connection>scm:git:https://git.ignuranza.net/tdlight-team/tdlib-session-container.git</connection>
|
||||
<developerConnection>scm:git:https://git.ignuranza.net/tdlight-team/tdlib-session-container.git
|
||||
</developerConnection>
|
||||
<tag>HEAD</tag>
|
||||
</scm>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>it.tdlight</groupId>
|
||||
<artifactId>tdlight-java-bom</artifactId>
|
||||
<version>2.8.10.2</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-bom</artifactId>
|
||||
<version>2022.0.1</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rsocket</groupId>
|
||||
<artifactId>rsocket-bom</artifactId>
|
||||
<version>1.1.3</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-hazelcast</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-junit5</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-reactive-streams</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-rx-java2</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.8.1</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-circuit-breaker</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
<version>3.4.11</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-tools</artifactId>
|
||||
<version>3.4.11</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor.addons</groupId>
|
||||
<artifactId>reactor-adapter</artifactId>
|
||||
<version>3.4.5</version>
|
||||
<groupId>it.cavallium</groupId>
|
||||
<artifactId>filequeue</artifactId>
|
||||
<version>3.1.5</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.akaita.java</groupId>
|
||||
<artifactId>rxjava2-debug</artifactId>
|
||||
<version>1.4.0</version>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-tools</artifactId>
|
||||
<classifier>original</classifier>
|
||||
<scope>runtime</scope>
|
||||
<version>3.5.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>1.7.32</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<version>2.14.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j-impl</artifactId>
|
||||
<version>2.14.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.warp</groupId>
|
||||
<artifactId>common-utils</artifactId>
|
||||
<version>1.1.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-tcnative-boringssl-static</artifactId>
|
||||
<version>2.0.45.Final</version>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>23.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.9.0</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.9.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-tcnative-boringssl-static</artifactId>
|
||||
<version>2.0.54.Final</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.tdlight</groupId>
|
||||
<artifactId>tdlight-java-8</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.tdlight</groupId>
|
||||
<artifactId>tdlight-natives-linux-amd64</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm</artifactId>
|
||||
<version>9.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.unimi.dsi</groupId>
|
||||
<artifactId>fastutil</artifactId>
|
||||
<version>8.5.11</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor.kafka</groupId>
|
||||
<artifactId>reactor-kafka</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rsocket</groupId>
|
||||
<artifactId>rsocket-core</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rsocket</groupId>
|
||||
<artifactId>rsocket-load-balancer</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rsocket</groupId>
|
||||
<artifactId>rsocket-transport-local</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rsocket</groupId>
|
||||
<artifactId>rsocket-transport-netty</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-api</artifactId>
|
||||
<version>2.19.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<version>2.19.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j20-impl</artifactId>
|
||||
<version>2.18.1-SNAPSHOT</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.lmax</groupId>
|
||||
<artifactId>disruptor</artifactId>
|
||||
<version>3.4.4</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
<version>2.14.0-rc1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.xml.bind</groupId>
|
||||
<artifactId>jakarta.xml.bind-api</artifactId>
|
||||
<version>4.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.minecrell</groupId>
|
||||
<artifactId>terminalconsoleappender</artifactId>
|
||||
<version>1.3.0</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>it.tdlight</groupId>
|
||||
<artifactId>tdlight-java</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.cavallium</groupId>
|
||||
<artifactId>concurrent-locks</artifactId>
|
||||
<version>1.0.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.annotation</groupId>
|
||||
<artifactId>javax.annotation-api</artifactId>
|
||||
<version>1.3.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>it.unimi.dsi</groupId>
|
||||
<artifactId>fastutil</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi</artifactId>
|
||||
<version>1.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.zacsweers.moshix</groupId>
|
||||
<artifactId>moshi-records-reflect</artifactId>
|
||||
<version>0.14.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>../src/main/libs</directory>
|
||||
<excludes>
|
||||
<exclude>**/*.jar</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<excludes>
|
||||
<exclude>**/*.jar</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>1.5.0.Final</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-install-plugin</artifactId>
|
||||
<version>3.0.0-M1</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<release>11</release>
|
||||
<useIncrementalCompilation>false</useIncrementalCompilation>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<version>2.8.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>flatten-maven-plugin</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<configuration>
|
||||
<updatePomFile>true</updatePomFile>
|
||||
<flattenMode>resolveCiFriendliesOnly</flattenMode>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>flatten</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>flatten</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>flatten.clean</id>
|
||||
<phase>clean</phase>
|
||||
<goals>
|
||||
<goal>clean</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>templating-maven-plugin</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>filtering-java-templates</id>
|
||||
<goals>
|
||||
<goal>filter-sources</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.22.2</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-surefire-provider</artifactId>
|
||||
<version>1.3.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.8.0-M1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependency>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-processor</artifactId>
|
||||
<version>${record.builder.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>31.1-jre</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>../src/main/libs</directory>
|
||||
<excludes>
|
||||
<exclude>**/*.jar</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<excludes>
|
||||
<exclude>**/*.jar</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>1.5.0.Final</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-install-plugin</artifactId>
|
||||
<version>3.0.0-M1</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<annotationProcessorPaths>
|
||||
<annotationProcessorPath>
|
||||
<groupId>io.soabase.record-builder</groupId>
|
||||
<artifactId>record-builder-processor</artifactId>
|
||||
<version>${record.builder.version}</version>
|
||||
</annotationProcessorPath>
|
||||
</annotationProcessorPaths>
|
||||
<annotationProcessors>
|
||||
<annotationProcessor>io.soabase.recordbuilder.processor.RecordBuilderProcessor</annotationProcessor>
|
||||
</annotationProcessors>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<version>2.8.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>templating-maven-plugin</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>filtering-java-templates</id>
|
||||
<goals>
|
||||
<goal>filter-sources</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>flatten-maven-plugin</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<configuration>
|
||||
<updatePomFile>true</updatePomFile>
|
||||
<flattenMode>oss</flattenMode>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>flatten</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>flatten</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>flatten.clean</id>
|
||||
<phase>clean</phase>
|
||||
<goals>
|
||||
<goal>clean</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.9.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
|
||||
<plugin>
|
||||
<groupId>org.eclipse.m2e</groupId>
|
||||
<artifactId>lifecycle-mapping</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<configuration>
|
||||
<lifecycleMappingMetadata>
|
||||
<pluginExecutions>
|
||||
<pluginExecution>
|
||||
<pluginExecutionFilter>
|
||||
<groupId>
|
||||
org.codehaus.mojo
|
||||
</groupId>
|
||||
<artifactId>
|
||||
flatten-maven-plugin
|
||||
</artifactId>
|
||||
<versionRange>
|
||||
[1.1.0,)
|
||||
</versionRange>
|
||||
<goals>
|
||||
<goal>flatten</goal>
|
||||
</goals>
|
||||
</pluginExecutionFilter>
|
||||
<action>
|
||||
<ignore></ignore>
|
||||
</action>
|
||||
</pluginExecution>
|
||||
</pluginExecutions>
|
||||
</lifecycleMappingMetadata>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>reactor-agent</id>
|
||||
<activation>
|
||||
<activeByDefault>false</activeByDefault>
|
||||
<property>
|
||||
<name>reactor.agent.enable</name>
|
||||
<value>true</value>
|
||||
</property>
|
||||
</activation>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>net.bytebuddy</groupId>
|
||||
<artifactId>byte-buddy-maven-plugin</artifactId>
|
||||
<version>1.12.21</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>transform</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<transformations>
|
||||
<transformation>
|
||||
<plugin>reactor.tools.agent.ReactorDebugByteBuddyPlugin</plugin>
|
||||
</transformation>
|
||||
</transformations>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.tdlibsessioncontainer.utils.generated;
|
||||
package it.tdlight.reactiveapi.generated;
|
||||
public final class LibraryVersion {
|
||||
public static final String VERSION = "${project.version}";
|
||||
}
|
334
src/main/java/it/tdlight/reactiveapi/AtomixReactiveApi.java
Normal file
334
src/main/java/it/tdlight/reactiveapi/AtomixReactiveApi.java
Normal file
@ -0,0 +1,334 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.CreateBotSessionRequest;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.CreateUserSessionRequest;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.LoadSessionFromDiskRequest;
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class AtomixReactiveApi implements ReactiveApi {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(AtomixReactiveApi.class);
|
||||
|
||||
private final AtomixReactiveApiMode mode;
|
||||
|
||||
private final TdlibChannelsSharedReceive sharedTdlibClients;
|
||||
@Nullable
|
||||
private final TdlibChannelsSharedHost sharedTdlibServers;
|
||||
private final ReactiveApiMultiClient client;
|
||||
|
||||
private final Set<ResultingEventTransformer> resultingEventTransformerSet;
|
||||
/**
|
||||
* user id -> session
|
||||
*/
|
||||
private final ConcurrentMap<Long, ReactiveApiPublisher> localSessions = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* DiskSessions is null when nodeId is null
|
||||
*/
|
||||
@Nullable
|
||||
private final DiskSessionsManager diskSessions;
|
||||
private volatile boolean closeRequested;
|
||||
private volatile Disposable requestsSub;
|
||||
|
||||
public enum AtomixReactiveApiMode {
|
||||
CLIENT,
|
||||
SERVER,
|
||||
FULL
|
||||
}
|
||||
|
||||
public AtomixReactiveApi(AtomixReactiveApiMode mode,
|
||||
ChannelsParameters channelsParameters,
|
||||
@Nullable DiskSessionsManager diskSessions,
|
||||
@NotNull Set<ResultingEventTransformer> resultingEventTransformerSet) {
|
||||
this.mode = mode;
|
||||
ChannelFactory channelFactory = ChannelFactory.getFactoryFromParameters(channelsParameters);
|
||||
if (mode != AtomixReactiveApiMode.SERVER) {
|
||||
EventProducer<OnRequest<?>> tdRequestProducer = ChannelProducerTdlibRequest.create(channelFactory);
|
||||
EventConsumer<OnResponse<Object>> tdResponseConsumer = ChannelConsumerTdlibResponse.create(channelFactory);
|
||||
HashMap<String, EventConsumer<ClientBoundEvent>> clientBoundConsumers = new HashMap<>();
|
||||
for (String lane : channelsParameters.getAllLanes()) {
|
||||
clientBoundConsumers.put(lane, ChannelConsumerClientBoundEvent.create(channelFactory, lane));
|
||||
}
|
||||
var tdClientsChannels = new TdlibChannelsClients(tdRequestProducer,
|
||||
tdResponseConsumer,
|
||||
clientBoundConsumers
|
||||
);
|
||||
this.sharedTdlibClients = new TdlibChannelsSharedReceive(tdClientsChannels);
|
||||
this.client = new LiveAtomixReactiveApiClient(sharedTdlibClients);
|
||||
} else {
|
||||
this.sharedTdlibClients = null;
|
||||
this.client = null;
|
||||
}
|
||||
if (mode != AtomixReactiveApiMode.CLIENT) {
|
||||
EventConsumer<OnRequest<Object>> tdRequestConsumer = ChannelConsumerTdlibRequest.create(channelFactory);
|
||||
EventProducer<OnResponse<Object>> tdResponseProducer = ChannelProducerTdlibResponse.create(channelFactory);
|
||||
var clientBoundProducers = new HashMap<String, EventProducer<ClientBoundEvent>>();
|
||||
for (String lane : channelsParameters.getAllLanes()) {
|
||||
clientBoundProducers.put(lane, ChannelProducerClientBoundEvent.create(channelFactory, lane));
|
||||
}
|
||||
var tdServer = new TdlibChannelsServers(tdRequestConsumer,
|
||||
tdResponseProducer,
|
||||
clientBoundProducers
|
||||
);
|
||||
this.sharedTdlibServers = new TdlibChannelsSharedHost(channelsParameters.getAllLanes(), tdServer);
|
||||
} else {
|
||||
this.sharedTdlibServers = null;
|
||||
}
|
||||
this.resultingEventTransformerSet = resultingEventTransformerSet;
|
||||
|
||||
this.diskSessions = diskSessions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> start() {
|
||||
var idsSavedIntoLocalConfiguration = Mono
|
||||
.<Set<Entry<Long, DiskSession>>>fromCallable(() -> {
|
||||
if (diskSessions == null) {
|
||||
return Set.of();
|
||||
}
|
||||
synchronized (diskSessions) {
|
||||
return diskSessions.getSettings().userIdToSession().entrySet();
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMapIterable(a -> a)
|
||||
.map(a -> new DiskSessionAndId(a.getValue(), a.getKey()));
|
||||
|
||||
var loadSessions = idsSavedIntoLocalConfiguration
|
||||
.filter(diskSessionAndId -> {
|
||||
try {
|
||||
diskSessionAndId.diskSession().validate();
|
||||
} catch (Throwable ex) {
|
||||
LOG.error("Failed to load disk session {}", diskSessionAndId.id, ex);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.flatMap(diskSessionAndId -> {
|
||||
var id = diskSessionAndId.id;
|
||||
var diskSession = diskSessionAndId.diskSession;
|
||||
return createSession(new LoadSessionFromDiskRequest(id,
|
||||
diskSession.token,
|
||||
diskSession.phoneNumber,
|
||||
diskSession.lane,
|
||||
true
|
||||
));
|
||||
})
|
||||
.then()
|
||||
.doOnTerminate(() -> LOG.info("Loaded all saved sessions from disk"));
|
||||
|
||||
return loadSessions.<Void>then(Mono.fromRunnable(() -> {
|
||||
if (sharedTdlibServers != null) {
|
||||
requestsSub = sharedTdlibServers.requests()
|
||||
.doOnNext(req -> {
|
||||
var publisher = localSessions.get(req.data().userId());
|
||||
if (publisher != null) {
|
||||
publisher.handleRequest(req.data());
|
||||
} else {
|
||||
LOG.debug("Dropped request because no session is found: {}", req);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.parallel())
|
||||
.subscribe(n -> {}, ex -> LOG.error("Requests channel broke unexpectedly", ex));
|
||||
}
|
||||
})).transform(ReactorUtils::subscribeOnceUntilUnsubscribe);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CreateSessionResponse> createSession(CreateSessionRequest req) {
|
||||
LOG.debug("Received create session request: {}", req);
|
||||
|
||||
if (mode == AtomixReactiveApiMode.CLIENT) {
|
||||
return Mono.error(new UnsupportedOperationException("This is a client, it can't have own sessions"));
|
||||
}
|
||||
|
||||
// Create the session instance
|
||||
ReactiveApiPublisher reactiveApiPublisher;
|
||||
boolean loadedFromDisk;
|
||||
long userId;
|
||||
String botToken;
|
||||
String lane;
|
||||
Long phoneNumber;
|
||||
if (req instanceof CreateBotSessionRequest createBotSessionRequest) {
|
||||
loadedFromDisk = false;
|
||||
userId = createBotSessionRequest.userId();
|
||||
botToken = createBotSessionRequest.token();
|
||||
phoneNumber = null;
|
||||
lane = createBotSessionRequest.lane();
|
||||
reactiveApiPublisher = ReactiveApiPublisher.fromToken(sharedTdlibServers, resultingEventTransformerSet,
|
||||
userId,
|
||||
botToken,
|
||||
lane
|
||||
);
|
||||
} else if (req instanceof CreateUserSessionRequest createUserSessionRequest) {
|
||||
loadedFromDisk = false;
|
||||
userId = createUserSessionRequest.userId();
|
||||
botToken = null;
|
||||
phoneNumber = createUserSessionRequest.phoneNumber();
|
||||
lane = createUserSessionRequest.lane();
|
||||
reactiveApiPublisher = ReactiveApiPublisher.fromPhoneNumber(sharedTdlibServers, resultingEventTransformerSet,
|
||||
userId,
|
||||
phoneNumber,
|
||||
lane
|
||||
);
|
||||
} else if (req instanceof LoadSessionFromDiskRequest loadSessionFromDiskRequest) {
|
||||
loadedFromDisk = true;
|
||||
userId = loadSessionFromDiskRequest.userId();
|
||||
botToken = loadSessionFromDiskRequest.token();
|
||||
phoneNumber = loadSessionFromDiskRequest.phoneNumber();
|
||||
lane = loadSessionFromDiskRequest.lane();
|
||||
if (loadSessionFromDiskRequest.phoneNumber() != null) {
|
||||
reactiveApiPublisher = ReactiveApiPublisher.fromPhoneNumber(sharedTdlibServers,
|
||||
resultingEventTransformerSet,
|
||||
userId,
|
||||
phoneNumber,
|
||||
lane
|
||||
);
|
||||
} else {
|
||||
reactiveApiPublisher = ReactiveApiPublisher.fromToken(sharedTdlibServers,
|
||||
resultingEventTransformerSet,
|
||||
userId,
|
||||
botToken,
|
||||
lane
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return Mono.error(new UnsupportedOperationException("Unexpected value: " + req));
|
||||
}
|
||||
|
||||
// Register the session instance to the local nodes map
|
||||
var prev = localSessions.put(userId, reactiveApiPublisher);
|
||||
if (prev != null) {
|
||||
LOG.error("User id \"{}\" was already registered locally! {}", userId, prev);
|
||||
}
|
||||
|
||||
var saveToDiskMono = Mono
|
||||
.<Void>fromCallable(() -> {
|
||||
// Save updated sessions configuration to disk
|
||||
try {
|
||||
Objects.requireNonNull(diskSessions);
|
||||
|
||||
synchronized (diskSessions) {
|
||||
diskSessions.save();
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new CompletionException("Failed to save disk sessions configuration", e);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
|
||||
// Start the session instance
|
||||
return Mono
|
||||
.fromCallable(() -> {
|
||||
Objects.requireNonNull(diskSessions);
|
||||
synchronized (diskSessions) {
|
||||
return Objects.requireNonNull(Paths.get(diskSessions.getSettings().path),
|
||||
"Session " + userId + " path is missing");
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMap(baseSessionsPath -> {
|
||||
String diskSessionFolderName = "id" + Long.toUnsignedString(userId);
|
||||
Path sessionPath = baseSessionsPath.resolve(diskSessionFolderName);
|
||||
|
||||
if (!loadedFromDisk) {
|
||||
// Create the disk session configuration
|
||||
var diskSession = new DiskSession(botToken, phoneNumber, lane);
|
||||
return Mono.<Void>fromCallable(() -> {
|
||||
Objects.requireNonNull(diskSessions);
|
||||
synchronized (diskSessions) {
|
||||
diskSessions.getSettings().userIdToSession().put(userId, diskSession);
|
||||
return null;
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic()).then(saveToDiskMono).thenReturn(sessionPath);
|
||||
} else {
|
||||
return Mono.just(sessionPath);
|
||||
}
|
||||
})
|
||||
.doOnNext(path -> reactiveApiPublisher.start(path, () -> {
|
||||
localSessions.remove(userId);
|
||||
LOG.debug("Closed the session for user {} after it was closed itself", userId);
|
||||
}))
|
||||
.thenReturn(new CreateSessionResponse(userId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactiveApiMultiClient client() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> close() {
|
||||
closeRequested = true;
|
||||
Mono<?> serverProducersStopper;
|
||||
if (sharedTdlibServers != null) {
|
||||
serverProducersStopper = Mono.fromRunnable(sharedTdlibServers::close).subscribeOn(Schedulers.boundedElastic());
|
||||
} else {
|
||||
serverProducersStopper = Mono.empty();
|
||||
}
|
||||
Mono<?> clientProducersStopper;
|
||||
if (sharedTdlibClients != null) {
|
||||
clientProducersStopper = Mono
|
||||
.fromRunnable(sharedTdlibClients::close)
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
} else {
|
||||
clientProducersStopper = Mono.empty();
|
||||
}
|
||||
if (requestsSub != null) {
|
||||
requestsSub.dispose();
|
||||
}
|
||||
return Mono.when(serverProducersStopper, clientProducersStopper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void waitForExit() {
|
||||
var nanos = Duration.ofSeconds(1).toNanos();
|
||||
while (!closeRequested && !Thread.interrupted()) {
|
||||
LockSupport.parkNanos(nanos);
|
||||
}
|
||||
}
|
||||
|
||||
private record DiskSessionAndId(DiskSession diskSession, long id) {}
|
||||
|
||||
private Mono<DiskSessionAndId> getLocalDiskSession(Long localUserId) {
|
||||
return Mono.fromCallable(() -> {
|
||||
Objects.requireNonNull(diskSessions);
|
||||
synchronized (diskSessions) {
|
||||
var diskSession = requireNonNull(diskSessions.getSettings().userIdToSession().get(localUserId),
|
||||
"Id not found: " + localUserId
|
||||
);
|
||||
try {
|
||||
diskSession.validate();
|
||||
} catch (Throwable ex) {
|
||||
LOG.error("Failed to load disk session {}", localUserId, ex);
|
||||
return null;
|
||||
}
|
||||
return new DiskSessionAndId(diskSession, localUserId);
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
}
|
13
src/main/java/it/tdlight/reactiveapi/AuthPhase.java
Normal file
13
src/main/java/it/tdlight/reactiveapi/AuthPhase.java
Normal file
@ -0,0 +1,13 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
public enum AuthPhase {
|
||||
LOGGED_OUT,
|
||||
PARAMETERS_PHASE,
|
||||
AUTH_PHASE,
|
||||
LOGGED_IN,
|
||||
LOGGING_OUT,
|
||||
/**
|
||||
* Similar to {@link #LOGGED_OUT}, but it can't be recovered
|
||||
*/
|
||||
BROKEN
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static it.tdlight.reactiveapi.Event.SERIAL_VERSION;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Error;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import it.tdlight.reactiveapi.Event.Ignored;
|
||||
import it.tdlight.reactiveapi.Event.OnBotLoginCodeRequested;
|
||||
import it.tdlight.reactiveapi.Event.OnOtherDeviceLoginRequested;
|
||||
import it.tdlight.reactiveapi.Event.OnPasswordRequested;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest.Request;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse.Response;
|
||||
import it.tdlight.reactiveapi.Event.OnUpdateData;
|
||||
import it.tdlight.reactiveapi.Event.OnUpdateError;
|
||||
import it.tdlight.reactiveapi.Event.OnUserLoginCodeRequested;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInput;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoSink;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.EmitFailureHandler;
|
||||
import reactor.core.publisher.Sinks.Many;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
abstract class BaseAtomixReactiveApiClient implements ReactiveApiMultiClient {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(BaseAtomixReactiveApiClient.class);
|
||||
private static final long EMPTY_USER_ID = 0;
|
||||
|
||||
// Temporary id used to make requests
|
||||
private final long clientId;
|
||||
private final Consumer<OnRequest<?>> requests;
|
||||
private final Map<Long, MonoSink<Timestamped<OnResponse<Object>>>> responses
|
||||
= new ConcurrentHashMap<>();
|
||||
private final AtomicLong requestId = new AtomicLong(0);
|
||||
private final Disposable subscription;
|
||||
|
||||
public BaseAtomixReactiveApiClient(TdlibChannelsSharedReceive sharedTdlibClients) {
|
||||
this.clientId = System.nanoTime();
|
||||
this.requests = sharedTdlibClients::emitRequest;
|
||||
|
||||
this.subscription = sharedTdlibClients.responses().doOnNext(response -> {
|
||||
var responseSink = responses.get(response.data().requestId());
|
||||
if (responseSink == null) {
|
||||
LOG.debug("Bot received a response for an unknown request id: {}", response.data().requestId());
|
||||
return;
|
||||
}
|
||||
responseSink.success(response);
|
||||
}).subscribeOn(Schedulers.parallel())
|
||||
.subscribe(v -> {}, ex -> LOG.error("Reactive api client responses flux has failed unexpectedly!", ex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Object> Mono<T> request(long userId, Function<T> request, Instant timeout) {
|
||||
return Mono.defer(() -> {
|
||||
var requestId = this.requestId.getAndIncrement();
|
||||
var timeoutError = new TdError(408, "Request Timeout");
|
||||
var requestTimestamp = Instant.now();
|
||||
var timeoutDuration = Duration.between(requestTimestamp, timeout);
|
||||
if (timeoutDuration.isNegative() || timeoutDuration.isZero()) {
|
||||
return Mono.error(timeoutError);
|
||||
}
|
||||
Mono<T> response = Mono
|
||||
.<Timestamped<OnResponse<TdApi.Object>>>create(sink -> {
|
||||
sink.onDispose(() -> this.responses.remove(requestId, sink));
|
||||
var prev = this.responses.putIfAbsent(requestId, sink);
|
||||
if (prev != null) {
|
||||
sink.error(new IllegalStateException("Can't call the same request twice: " + requestId));
|
||||
}
|
||||
})
|
||||
.timeout(timeoutDuration, Mono.fromSupplier(() -> new Timestamped<>(requestTimestamp.toEpochMilli(),
|
||||
new Response<>(clientId, requestId, userId, new TdApi.Error(408, "Request Timeout")))))
|
||||
.<T>handle((responseObj, sink) -> {
|
||||
if (Instant.ofEpochMilli(responseObj.timestamp()).compareTo(timeout) > 0) {
|
||||
sink.error(new TdError(408, "Request Timeout"));
|
||||
} else if (responseObj.data() instanceof OnResponse.InvalidResponse<?>) {
|
||||
sink.error(new TdError(400, "Conflicting protocol version"));
|
||||
} else if (responseObj.data() instanceof OnResponse.Response<?> onResponse) {
|
||||
if (onResponse.response().getConstructor() == Error.CONSTRUCTOR) {
|
||||
var tdError = (TdApi.Error) onResponse.response();
|
||||
sink.error(new TdError(tdError.code, tdError.message));
|
||||
} else {
|
||||
//noinspection unchecked
|
||||
var tdResponse = (T) onResponse.response();
|
||||
sink.next(tdResponse);
|
||||
}
|
||||
} else {
|
||||
sink.error(new UnsupportedOperationException("Unknown response type: " + responseObj.data().getClass()));
|
||||
}
|
||||
});
|
||||
requests.accept(new Request<>(userId, clientId, requestId, request, timeout));
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
static ClientBoundEvent deserializeEvent(byte[] bytes) {
|
||||
try (var byteArrayInputStream = new ByteArrayInputStream(bytes)) {
|
||||
try (var is = new DataInputStream(byteArrayInputStream)) {
|
||||
return deserializeEvent(is);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new SerializationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static @NotNull ClientBoundEvent deserializeEvent(DataInput is) throws IOException {
|
||||
var userId = is.readLong();
|
||||
var dataVersion = is.readInt();
|
||||
if (dataVersion != SERIAL_VERSION) {
|
||||
return new Ignored(userId);
|
||||
}
|
||||
return switch (is.readByte()) {
|
||||
case 0x01 -> new OnUpdateData(userId, (TdApi.Update) TdApi.Deserializer.deserialize(is));
|
||||
case 0x02 -> new OnUpdateError(userId, (TdApi.Error) TdApi.Deserializer.deserialize(is));
|
||||
case 0x03 -> new OnUserLoginCodeRequested(userId, is.readLong());
|
||||
case 0x04 -> new OnBotLoginCodeRequested(userId, is.readUTF());
|
||||
case 0x05 -> new OnOtherDeviceLoginRequested(userId, is.readUTF());
|
||||
case 0x06 -> new OnPasswordRequested(userId, is.readUTF(), is.readBoolean(), is.readUTF());
|
||||
case 0x07 -> new Ignored(userId);
|
||||
default -> throw new IllegalStateException("Unexpected value: " + is.readByte());
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> close() {
|
||||
return Mono.fromRunnable(() -> {
|
||||
subscription.dispose();
|
||||
long now = System.currentTimeMillis();
|
||||
responses.forEach((requestId, cf) -> cf.success(new Timestamped<>(now,
|
||||
new Response<>(clientId, requestId, EMPTY_USER_ID, new Error(408, "Request Timeout"))
|
||||
)));
|
||||
responses.clear();
|
||||
});
|
||||
}
|
||||
}
|
22
src/main/java/it/tdlight/reactiveapi/Channel.java
Normal file
22
src/main/java/it/tdlight/reactiveapi/Channel.java
Normal file
@ -0,0 +1,22 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
public enum Channel {
|
||||
CLIENT_BOUND_EVENT("event"),
|
||||
TDLIB_REQUEST("request"),
|
||||
TDLIB_RESPONSE("response");
|
||||
|
||||
private final String name;
|
||||
|
||||
Channel(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getChannelName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
47
src/main/java/it/tdlight/reactiveapi/ChannelCodec.java
Normal file
47
src/main/java/it/tdlight/reactiveapi/ChannelCodec.java
Normal file
@ -0,0 +1,47 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
public class ChannelCodec {
|
||||
public static final ChannelCodec CLIENT_BOUND_EVENT = new ChannelCodec(ClientBoundEventSerializer.class, ClientBoundEventDeserializer.class);
|
||||
public static final ChannelCodec TDLIB_REQUEST = new ChannelCodec(TdlibRequestSerializer.class, TdlibRequestDeserializer.class);
|
||||
public static final ChannelCodec TDLIB_RESPONSE = new ChannelCodec(TdlibResponseSerializer.class, TdlibResponseDeserializer.class);
|
||||
public static final ChannelCodec UTF8_TEST = new ChannelCodec(UtfCodec.class, UtfCodec.class);
|
||||
|
||||
private final Class<?> serializerClass;
|
||||
private final Class<?> deserializerClass;
|
||||
|
||||
public ChannelCodec(Class<?> serializerClass,
|
||||
Class<?> deserializerClass) {
|
||||
this.serializerClass = serializerClass;
|
||||
this.deserializerClass = deserializerClass;
|
||||
}
|
||||
|
||||
public Class<?> getSerializerClass() {
|
||||
return serializerClass;
|
||||
}
|
||||
|
||||
public Class<?> getDeserializerClass() {
|
||||
return deserializerClass;
|
||||
}
|
||||
|
||||
public <K> Deserializer<K> getNewDeserializer() {
|
||||
try {
|
||||
//noinspection unchecked
|
||||
return (Deserializer<K>) deserializerClass.getDeclaredConstructor().newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException |
|
||||
ClassCastException e) {
|
||||
throw new IllegalStateException("Can't instantiate the codec deserializer", e);
|
||||
}
|
||||
}
|
||||
|
||||
public <K> Serializer<K> getNewSerializer() {
|
||||
try {
|
||||
//noinspection unchecked
|
||||
return (Serializer<K>) serializerClass.getDeclaredConstructor().newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException |
|
||||
ClassCastException e) {
|
||||
throw new IllegalStateException("Can't instantiate the codec serializer", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class ChannelConsumerClientBoundEvent {
|
||||
|
||||
private ChannelConsumerClientBoundEvent() {
|
||||
}
|
||||
|
||||
public static EventConsumer<ClientBoundEvent> create(ChannelFactory channelFactory, @NotNull String lane) {
|
||||
String name;
|
||||
if (lane.isEmpty()) {
|
||||
name = Channel.CLIENT_BOUND_EVENT.getChannelName();
|
||||
} else {
|
||||
name = Channel.CLIENT_BOUND_EVENT.getChannelName() + "-" + lane;
|
||||
}
|
||||
return channelFactory.newConsumer(false, ChannelCodec.CLIENT_BOUND_EVENT, name);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest;
|
||||
|
||||
public class ChannelConsumerTdlibRequest {
|
||||
|
||||
private ChannelConsumerTdlibRequest() {
|
||||
}
|
||||
|
||||
public static EventConsumer<OnRequest<Object>> create(ChannelFactory channelFactory) {
|
||||
return channelFactory.newConsumer(true, ChannelCodec.TDLIB_REQUEST, Channel.TDLIB_REQUEST.getChannelName());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse;
|
||||
|
||||
public class ChannelConsumerTdlibResponse {
|
||||
|
||||
private ChannelConsumerTdlibResponse() {
|
||||
}
|
||||
|
||||
public static EventConsumer<OnResponse<Object>> create(ChannelFactory channelFactory) {
|
||||
return channelFactory.newConsumer(true, ChannelCodec.TDLIB_RESPONSE, Channel.TDLIB_RESPONSE.getChannelName());
|
||||
}
|
||||
}
|
76
src/main/java/it/tdlight/reactiveapi/ChannelFactory.java
Normal file
76
src/main/java/it/tdlight/reactiveapi/ChannelFactory.java
Normal file
@ -0,0 +1,76 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.kafka.KafkaConsumer;
|
||||
import it.tdlight.reactiveapi.kafka.KafkaProducer;
|
||||
import it.tdlight.reactiveapi.rsocket.MyRSocketClient;
|
||||
import it.tdlight.reactiveapi.rsocket.MyRSocketServer;
|
||||
import it.tdlight.reactiveapi.rsocket.RSocketChannelManager;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
public interface ChannelFactory {
|
||||
|
||||
static ChannelFactory getFactoryFromParameters(ChannelsParameters channelsParameters) {
|
||||
if (channelsParameters instanceof KafkaParameters kafkaParameters) {
|
||||
return new KafkaChannelFactory(kafkaParameters);
|
||||
} else if (channelsParameters instanceof RSocketParameters socketParameters) {
|
||||
return new RSocketChannelFactory(socketParameters);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unsupported parameters type: " + channelsParameters);
|
||||
}
|
||||
}
|
||||
|
||||
<T> EventConsumer<T> newConsumer(boolean quickResponse, ChannelCodec channelCodec, String channelName);
|
||||
|
||||
<T> EventProducer<T> newProducer(ChannelCodec channelCodec, String channelName);
|
||||
|
||||
class KafkaChannelFactory implements ChannelFactory {
|
||||
|
||||
private final KafkaParameters channelsParameters;
|
||||
|
||||
public KafkaChannelFactory(KafkaParameters channelsParameters) {
|
||||
this.channelsParameters = channelsParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> EventConsumer<T> newConsumer(boolean quickResponse, ChannelCodec channelCodec, String channelName) {
|
||||
return new KafkaConsumer<>(channelsParameters, quickResponse, channelCodec, channelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> EventProducer<T> newProducer(ChannelCodec channelCodec, String channelName) {
|
||||
return new KafkaProducer<>(channelsParameters, channelCodec, channelName);
|
||||
}
|
||||
}
|
||||
|
||||
class RSocketChannelFactory implements ChannelFactory, Closeable {
|
||||
|
||||
private final RSocketParameters channelsParameters;
|
||||
private final RSocketChannelManager manager;
|
||||
|
||||
public RSocketChannelFactory(RSocketParameters channelsParameters) {
|
||||
this.channelsParameters = channelsParameters;
|
||||
if (channelsParameters.isClient()) {
|
||||
this.manager = new MyRSocketClient(channelsParameters.transportFactory());
|
||||
} else {
|
||||
this.manager = new MyRSocketServer(channelsParameters.transportFactory());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> EventConsumer<T> newConsumer(boolean quickResponse, ChannelCodec channelCodec, String channelName) {
|
||||
return manager.registerConsumer(channelCodec, channelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> EventProducer<T> newProducer(ChannelCodec channelCodec, String channelName) {
|
||||
return manager.registerProducer(channelCodec, channelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
manager.dispose();
|
||||
manager.onClose().block();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
|
||||
public class ChannelProducerClientBoundEvent {
|
||||
|
||||
private ChannelProducerClientBoundEvent() {
|
||||
}
|
||||
|
||||
public static EventProducer<ClientBoundEvent> create(ChannelFactory channelFactory, String lane) {
|
||||
String name;
|
||||
if (lane.isBlank()) {
|
||||
name = Channel.CLIENT_BOUND_EVENT.getChannelName();
|
||||
} else {
|
||||
name = Channel.CLIENT_BOUND_EVENT.getChannelName() + "-" + lane;
|
||||
}
|
||||
return channelFactory.newProducer(ChannelCodec.CLIENT_BOUND_EVENT, name);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.Event.OnRequest;
|
||||
|
||||
public class ChannelProducerTdlibRequest {
|
||||
|
||||
private ChannelProducerTdlibRequest() {
|
||||
}
|
||||
|
||||
public static EventProducer<OnRequest<?>> create(ChannelFactory channelFactory) {
|
||||
return channelFactory.newProducer(ChannelCodec.TDLIB_REQUEST, Channel.TDLIB_REQUEST.getChannelName());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse;
|
||||
|
||||
public class ChannelProducerTdlibResponse {
|
||||
|
||||
private ChannelProducerTdlibResponse() {
|
||||
}
|
||||
|
||||
public static EventProducer<OnResponse<Object>> create(ChannelFactory channelFactory) {
|
||||
return channelFactory.newProducer(ChannelCodec.TDLIB_RESPONSE, Channel.TDLIB_RESPONSE.getChannelName());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public interface ChannelsParameters {
|
||||
|
||||
Set<String> getAllLanes();
|
||||
}
|
152
src/main/java/it/tdlight/reactiveapi/Cli.java
Normal file
152
src/main/java/it/tdlight/reactiveapi/Cli.java
Normal file
@ -0,0 +1,152 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static it.tdlight.reactiveapi.Lanes.MAIN_LANE;
|
||||
import static java.util.Collections.unmodifiableSet;
|
||||
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.CreateBotSessionRequest;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.CreateUserSessionRequest;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
import net.minecrell.terminalconsole.SimpleTerminalConsole;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.LineReaderBuilder;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class Cli {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(Cli.class);
|
||||
|
||||
private static final Object parameterLock = new Object();
|
||||
private static boolean askedParameter = false;
|
||||
private static CompletableFuture<String> askedParameterResult = null;
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
var validArgs = Entrypoint.parseArguments(args);
|
||||
var api = (AtomixReactiveApi) Entrypoint.start(validArgs);
|
||||
|
||||
AtomicBoolean alreadyShutDown = new AtomicBoolean(false);
|
||||
AtomicBoolean acceptInputs = new AtomicBoolean(true);
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
acceptInputs.set(false);
|
||||
if (alreadyShutDown.compareAndSet(false, true)) {
|
||||
api.close().subscribeOn(Schedulers.immediate()).subscribe();
|
||||
}
|
||||
}));
|
||||
|
||||
var console = new SimpleTerminalConsole() {
|
||||
|
||||
private static final Set<String> commands = Set.of("exit",
|
||||
"stop",
|
||||
"createsession",
|
||||
"help",
|
||||
"man",
|
||||
"?",
|
||||
"sessions",
|
||||
"localsessions"
|
||||
);
|
||||
|
||||
@Override
|
||||
protected LineReader buildReader(LineReaderBuilder builder) {
|
||||
return super.buildReader(builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isRunning() {
|
||||
return acceptInputs.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void runCommand(String command) {
|
||||
synchronized (parameterLock) {
|
||||
if (askedParameter) {
|
||||
askedParameterResult.complete(command);
|
||||
askedParameterResult = null;
|
||||
askedParameter = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var parts = command.split(" ", 2);
|
||||
var commandName = parts[0].trim().toLowerCase();
|
||||
String commandArgs;
|
||||
if (parts.length > 1) {
|
||||
commandArgs = parts[1].trim();
|
||||
} else {
|
||||
commandArgs = "";
|
||||
}
|
||||
switch (commandName) {
|
||||
case "exit", "stop" -> shutdown();
|
||||
case "createsession" -> createSession(api, commandArgs);
|
||||
case "help", "?", "man" -> LOG.info("Commands: {}", commands);
|
||||
case "sessions" -> printSessions(api, false);
|
||||
case "localsessions" -> printSessions(api, true);
|
||||
default -> LOG.info("Unknown command \"{}\"", command);
|
||||
}
|
||||
}
|
||||
|
||||
private void printSessions(ReactiveApi api, boolean onlyLocal) {
|
||||
LOG.info("Not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void shutdown() {
|
||||
acceptInputs.set(false);
|
||||
if (alreadyShutDown.compareAndSet(false, true)) {
|
||||
Runtime.getRuntime().exit(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
console.start();
|
||||
api.waitForExit();
|
||||
}
|
||||
|
||||
private static void createSession(ReactiveApi api, String commandArgs) {
|
||||
var parts = commandArgs.split(" ");
|
||||
boolean invalid = false;
|
||||
if (parts.length == 4 || parts.length == 3) {
|
||||
String lane;
|
||||
if (parts.length == 4) {
|
||||
lane = parts[3];
|
||||
} else {
|
||||
lane = MAIN_LANE;
|
||||
}
|
||||
CreateSessionRequest request = switch (parts[0]) {
|
||||
case "bot" -> new CreateBotSessionRequest(Long.parseLong(parts[1]), parts[2], lane);
|
||||
case "user" -> new CreateUserSessionRequest(Long.parseLong(parts[1]),
|
||||
Long.parseLong(parts[2]), lane);
|
||||
default -> {
|
||||
invalid = true;
|
||||
yield null;
|
||||
}
|
||||
};
|
||||
if (!invalid) {
|
||||
api
|
||||
.createSession(request)
|
||||
.doOnNext(response -> LOG.info("Created a session with live id \"{}\"", response.sessionId()))
|
||||
.block();
|
||||
}
|
||||
} else {
|
||||
invalid = true;
|
||||
}
|
||||
if (invalid) {
|
||||
LOG.error("Syntax: CreateSession <\"bot\"|\"user\"> <userid> <token|phoneNumber> [lane]");
|
||||
}
|
||||
}
|
||||
|
||||
public static String askParameter(String question) {
|
||||
var cf = new CompletableFuture<String>();
|
||||
synchronized (parameterLock) {
|
||||
LOG.info(question);
|
||||
askedParameter = true;
|
||||
askedParameterResult = cf;
|
||||
}
|
||||
return cf.join();
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import java.io.DataInput;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ClientBoundEventDeserializer implements Deserializer<ClientBoundEvent> {
|
||||
|
||||
@Override
|
||||
public ClientBoundEvent deserialize(byte[] data) {
|
||||
if (data == null || data.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return LiveAtomixReactiveApiClient.deserializeEvent(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientBoundEvent deserialize(int length, DataInput dataInput) throws IOException {
|
||||
if (dataInput == null || length == 0) {
|
||||
return null;
|
||||
}
|
||||
return LiveAtomixReactiveApiClient.deserializeEvent(dataInput);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ClientBoundEventSerializer implements Serializer<ClientBoundEvent> {
|
||||
|
||||
@Override
|
||||
public byte[] serialize(ClientBoundEvent data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return ReactiveApiPublisher.serializeEvent(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(ClientBoundEvent data, DataOutput output) throws IOException {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
ReactiveApiPublisher.writeClientBoundEvent(data, output);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ClientBoundResultingEvent;
|
||||
|
||||
public class ClientBoundResultingEventSerializer implements Serializer<ClientBoundResultingEvent> {
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ClusterBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ResultingEventPublisherClosed;
|
||||
import java.io.DataInput;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ClusterBoundResultingEventDeserializer implements Deserializer<ClusterBoundResultingEvent> {
|
||||
|
||||
@Override
|
||||
public ClusterBoundResultingEvent deserialize(int length, DataInput dataInput) throws IOException {
|
||||
var type = dataInput.readByte();
|
||||
return switch (type) {
|
||||
case 0 -> new ResultingEventPublisherClosed();
|
||||
default -> throw new UnsupportedOperationException("Unsupported type: " + type);
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ClusterBoundResultingEvent;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
|
||||
public class ClusterBoundResultingEventSerializer implements Serializer<ClusterBoundResultingEvent> {
|
||||
|
||||
@Override
|
||||
public void serialize(ClusterBoundResultingEvent data, DataOutput output) throws IOException {
|
||||
if (data instanceof ResultingEvent.ResultingEventPublisherClosed) {
|
||||
output.writeByte(0x0);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unsupported event: " + data);
|
||||
}
|
||||
}
|
||||
}
|
39
src/main/java/it/tdlight/reactiveapi/ClusterSettings.java
Normal file
39
src/main/java/it/tdlight/reactiveapi/ClusterSettings.java
Normal file
@ -0,0 +1,39 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Define the cluster structure
|
||||
*/
|
||||
public class ClusterSettings {
|
||||
|
||||
public String id;
|
||||
public List<String> kafkaBootstrapServers;
|
||||
public String rsocketHost;
|
||||
public List<String> lanes;
|
||||
|
||||
@JsonCreator
|
||||
public ClusterSettings(@JsonProperty(required = true, value = "id") String id,
|
||||
@JsonProperty(value = "kafkaBootstrapServers") List<String> kafkaBootstrapServers,
|
||||
@JsonProperty(value = "rsocketHost") String rsocketHost,
|
||||
@JsonProperty(required = true, value = "lanes") List<String> lanes) {
|
||||
this.id = id;
|
||||
this.kafkaBootstrapServers = kafkaBootstrapServers;
|
||||
this.rsocketHost = rsocketHost;
|
||||
this.lanes = lanes;
|
||||
if ((rsocketHost == null) == (kafkaBootstrapServers == null || kafkaBootstrapServers.isEmpty())) {
|
||||
throw new IllegalArgumentException("Please configure either RSocket or Kafka");
|
||||
}
|
||||
}
|
||||
|
||||
public ChannelsParameters toParameters(String clientId, InstanceType instanceType) {
|
||||
if (rsocketHost != null) {
|
||||
return new RSocketParameters(instanceType != InstanceType.UPDATES_CONSUMER, rsocketHost, lanes);
|
||||
} else {
|
||||
return new KafkaParameters(clientId, clientId, kafkaBootstrapServers, List.copyOf(lanes));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.CreateBotSessionRequest;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.CreateUserSessionRequest;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.LoadSessionFromDiskRequest;
|
||||
|
||||
public sealed interface CreateSessionRequest permits CreateUserSessionRequest, CreateBotSessionRequest,
|
||||
LoadSessionFromDiskRequest {
|
||||
|
||||
long userId();
|
||||
|
||||
record CreateUserSessionRequest(long userId, long phoneNumber, String lane) implements CreateSessionRequest {}
|
||||
|
||||
record CreateBotSessionRequest(long userId, String token, String lane) implements CreateSessionRequest {}
|
||||
|
||||
record LoadSessionFromDiskRequest(long userId, String token, Long phoneNumber, String lane,
|
||||
boolean createNew) implements CreateSessionRequest {
|
||||
|
||||
public LoadSessionFromDiskRequest {
|
||||
if ((token == null) == (phoneNumber == null)) {
|
||||
throw new IllegalArgumentException("This must be either a bot or an user");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
public record CreateSessionResponse(long sessionId) {
|
||||
|
||||
}
|
22
src/main/java/it/tdlight/reactiveapi/Deserializer.java
Normal file
22
src/main/java/it/tdlight/reactiveapi/Deserializer.java
Normal file
@ -0,0 +1,22 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.DataInput;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
public interface Deserializer<T> {
|
||||
|
||||
default T deserialize(byte[] data) throws IOException {
|
||||
var bais = new ByteArrayInputStream(data);
|
||||
return deserialize(data.length, new DataInputStream(bais));
|
||||
}
|
||||
|
||||
default T deserialize(int length, DataInput dataInput) throws IOException {
|
||||
byte[] data = new byte[length];
|
||||
dataInput.readFully(data);
|
||||
return deserialize(data);
|
||||
}
|
||||
}
|
37
src/main/java/it/tdlight/reactiveapi/DiskSession.java
Normal file
37
src/main/java/it/tdlight/reactiveapi/DiskSession.java
Normal file
@ -0,0 +1,37 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static it.tdlight.reactiveapi.Lanes.MAIN_LANE;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Objects;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public class DiskSession {
|
||||
|
||||
@Nullable
|
||||
public String token;
|
||||
@Nullable
|
||||
public Long phoneNumber;
|
||||
@Nullable
|
||||
public String lane;
|
||||
|
||||
@JsonCreator
|
||||
public DiskSession(@JsonProperty("token") @Nullable String token,
|
||||
@JsonProperty("phoneNumber") @Nullable Long phoneNumber,
|
||||
@JsonProperty("lane") @Nullable String lane) {
|
||||
this.token = token;
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.lane = Objects.requireNonNullElse(lane, MAIN_LANE);
|
||||
this.validate();
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
if ((token == null) == (phoneNumber == null)) {
|
||||
throw new UnsupportedOperationException("You must set either a token or a phone number");
|
||||
}
|
||||
}
|
||||
}
|
29
src/main/java/it/tdlight/reactiveapi/DiskSessions.java
Normal file
29
src/main/java/it/tdlight/reactiveapi/DiskSessions.java
Normal file
@ -0,0 +1,29 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Map;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class DiskSessions {
|
||||
|
||||
@NotNull
|
||||
public String path;
|
||||
|
||||
/**
|
||||
* key: session folder name
|
||||
*/
|
||||
@NotNull
|
||||
private Map<Long, DiskSession> sessions;
|
||||
|
||||
@JsonCreator
|
||||
public DiskSessions(@JsonProperty(required = true, value = "path") @NotNull String path,
|
||||
@JsonProperty(required = true, value = "sessions") @NotNull Map<Long, DiskSession> userIdToSession) {
|
||||
this.path = path;
|
||||
this.sessions = userIdToSession;
|
||||
}
|
||||
|
||||
public Map<Long, DiskSession> userIdToSession() {
|
||||
return sessions;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class DiskSessionsManager {
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
private final DiskSessions diskSessionsSettings;
|
||||
private final File file;
|
||||
|
||||
public DiskSessionsManager(ObjectMapper mapper, String diskSessionsConfigPath) throws IOException {
|
||||
this.mapper = mapper;
|
||||
this.file = Paths.get(diskSessionsConfigPath).toFile();
|
||||
diskSessionsSettings = mapper.readValue(file, DiskSessions.class);
|
||||
}
|
||||
|
||||
public DiskSessions getSettings() {
|
||||
return diskSessionsSettings;
|
||||
}
|
||||
|
||||
public synchronized void save() throws IOException {
|
||||
mapper.writeValue(file, diskSessionsSettings);
|
||||
}
|
||||
}
|
123
src/main/java/it/tdlight/reactiveapi/Entrypoint.java
Normal file
123
src/main/java/it/tdlight/reactiveapi/Entrypoint.java
Normal file
@ -0,0 +1,123 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static java.util.Collections.unmodifiableSet;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import it.tdlight.reactiveapi.AtomixReactiveApi.AtomixReactiveApiMode;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class Entrypoint {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(Entrypoint.class);
|
||||
|
||||
public record ValidEntrypointArgs(String clusterPath, String instancePath, String diskSessionsPath) {}
|
||||
|
||||
public static ValidEntrypointArgs parseArguments(String[] args) {
|
||||
// Check arguments validity
|
||||
if (args.length != 3
|
||||
|| args[0].isBlank()
|
||||
|| args[1].isBlank()
|
||||
|| args[2].isBlank()
|
||||
|| !Files.isRegularFile(Paths.get(args[0]))
|
||||
|| !Files.isRegularFile(Paths.get(args[1]))
|
||||
|| !Files.isRegularFile(Paths.get(args[2]))) {
|
||||
System.err.println("Syntax: executable <path/to/cluster.yaml> <path/to/instance.yaml> <path/to/disk-sessions.yaml>");
|
||||
System.exit(1);
|
||||
}
|
||||
return new ValidEntrypointArgs(args[0], args[1], args[2]);
|
||||
}
|
||||
|
||||
public static ReactiveApi start(ValidEntrypointArgs args) throws IOException {
|
||||
// Read settings
|
||||
ClusterSettings clusterSettings;
|
||||
InstanceSettings instanceSettings;
|
||||
DiskSessionsManager diskSessions;
|
||||
{
|
||||
var mapper = new ObjectMapper(new YAMLFactory());
|
||||
mapper.findAndRegisterModules();
|
||||
String clusterConfigPath = args.clusterPath;
|
||||
String instanceConfigPath = args.instancePath;
|
||||
String diskSessionsConfigPath = args.diskSessionsPath;
|
||||
clusterSettings = mapper.readValue(Paths.get(clusterConfigPath).toFile(), ClusterSettings.class);
|
||||
instanceSettings = mapper.readValue(Paths.get(instanceConfigPath).toFile(), InstanceSettings.class);
|
||||
diskSessions = switch (instanceSettings.instanceType) {
|
||||
case TDLIB -> new DiskSessionsManager(mapper, diskSessionsConfigPath);
|
||||
case UPDATES_CONSUMER -> null;
|
||||
};
|
||||
}
|
||||
|
||||
return start(clusterSettings, instanceSettings, diskSessions);
|
||||
}
|
||||
|
||||
public static ReactiveApi start(ClusterSettings clusterSettings,
|
||||
InstanceSettings instanceSettings,
|
||||
@Nullable DiskSessionsManager diskSessions) {
|
||||
|
||||
Set<ResultingEventTransformer> resultingEventTransformerSet;
|
||||
AtomixReactiveApiMode mode = AtomixReactiveApiMode.SERVER;
|
||||
switch (instanceSettings.instanceType) {
|
||||
case UPDATES_CONSUMER -> {
|
||||
if (diskSessions != null) {
|
||||
throw new IllegalArgumentException("An updates-consumer instance can't have a session manager!");
|
||||
}
|
||||
if (instanceSettings.listenAddress == null) {
|
||||
throw new IllegalArgumentException("An updates-consumer instance must have an address (host:port)");
|
||||
}
|
||||
mode = AtomixReactiveApiMode.CLIENT;
|
||||
resultingEventTransformerSet = Set.of();
|
||||
}
|
||||
case TDLIB -> {
|
||||
if (diskSessions == null) {
|
||||
throw new IllegalArgumentException("A tdlib instance must have a session manager!");
|
||||
}
|
||||
|
||||
resultingEventTransformerSet = new HashSet<>();
|
||||
if (instanceSettings.resultingEventTransformers != null) {
|
||||
for (var resultingEventTransformer: instanceSettings.resultingEventTransformers) {
|
||||
try {
|
||||
var instance = resultingEventTransformer.getConstructor().newInstance();
|
||||
resultingEventTransformerSet.add(instance);
|
||||
LOG.info("Loaded and applied resulting event transformer: " + resultingEventTransformer.getName());
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalArgumentException("Failed to load resulting event transformer: "
|
||||
+ resultingEventTransformer.getName());
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IllegalArgumentException("The client transformer must declare an empty constructor: "
|
||||
+ resultingEventTransformer.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resultingEventTransformerSet = unmodifiableSet(resultingEventTransformerSet);
|
||||
}
|
||||
default -> throw new UnsupportedOperationException("Unsupported instance type: " + instanceSettings.instanceType);
|
||||
}
|
||||
|
||||
ChannelsParameters channelsParameters = clusterSettings.toParameters(instanceSettings.id, instanceSettings.instanceType);
|
||||
|
||||
var api = new AtomixReactiveApi(mode, channelsParameters, diskSessions, resultingEventTransformerSet);
|
||||
|
||||
LOG.info("Starting ReactiveApi...");
|
||||
|
||||
api.start().block();
|
||||
|
||||
LOG.info("Started ReactiveApi");
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
var validArgs = parseArguments(args);
|
||||
var api = start(validArgs);
|
||||
api.waitForExit();
|
||||
}
|
||||
}
|
82
src/main/java/it/tdlight/reactiveapi/Event.java
Normal file
82
src/main/java/it/tdlight/reactiveapi/Event.java
Normal file
@ -0,0 +1,82 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.common.utils.LibraryVersion;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Any event received from a session
|
||||
*/
|
||||
public sealed interface Event {
|
||||
|
||||
int SERIAL_VERSION = Arrays.hashCode(LibraryVersion.VERSION.getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
/**
|
||||
* Event received after choosing the user id of the session
|
||||
*/
|
||||
sealed interface ClientBoundEvent extends Event {
|
||||
|
||||
/**
|
||||
*
|
||||
* @return telegram user id of the session
|
||||
*/
|
||||
long userId();
|
||||
}
|
||||
|
||||
sealed interface ServerBoundEvent extends Event {}
|
||||
|
||||
/**
|
||||
* TDLib is asking for an authorization code
|
||||
*/
|
||||
sealed interface OnLoginCodeRequested extends ClientBoundEvent {}
|
||||
|
||||
record OnUserLoginCodeRequested(long userId, long phoneNumber) implements OnLoginCodeRequested {}
|
||||
|
||||
record OnBotLoginCodeRequested(long userId, String token) implements OnLoginCodeRequested {}
|
||||
|
||||
record OnOtherDeviceLoginRequested(long userId, String link) implements ClientBoundEvent {}
|
||||
|
||||
record OnPasswordRequested(long userId, String passwordHint, boolean hasRecoveryEmail,
|
||||
String recoveryEmailPattern) implements ClientBoundEvent {}
|
||||
|
||||
record Ignored(long userId) implements ClientBoundEvent {}
|
||||
|
||||
/**
|
||||
* Event received from TDLib
|
||||
*/
|
||||
sealed interface OnUpdate extends ClientBoundEvent {}
|
||||
|
||||
record OnUpdateData(long userId, TdApi.Update update) implements OnUpdate {}
|
||||
|
||||
record OnUpdateError(long userId, TdApi.Error error) implements OnUpdate {}
|
||||
|
||||
sealed interface OnRequest<T extends TdApi.Object> extends ServerBoundEvent {
|
||||
|
||||
record Request<T extends TdApi.Object>(long userId, long clientId, long requestId, TdApi.Function<T> request,
|
||||
Instant timeout) implements OnRequest<T> {}
|
||||
|
||||
record InvalidRequest<T extends TdApi.Object>(long userId, long clientId, long requestId) implements OnRequest<T> {}
|
||||
|
||||
long userId();
|
||||
|
||||
long clientId();
|
||||
|
||||
long requestId();
|
||||
|
||||
}
|
||||
|
||||
sealed interface OnResponse<T extends TdApi.Object> extends ClientBoundEvent {
|
||||
|
||||
record Response<T extends TdApi.Object>(long clientId, long requestId, long userId,
|
||||
T response) implements OnResponse<T> {}
|
||||
|
||||
record InvalidResponse<T extends TdApi.Object>(long clientId, long requestId, long userId) implements
|
||||
OnResponse<T> {}
|
||||
|
||||
long clientId();
|
||||
|
||||
long requestId();
|
||||
}
|
||||
}
|
9
src/main/java/it/tdlight/reactiveapi/EventConsumer.java
Normal file
9
src/main/java/it/tdlight/reactiveapi/EventConsumer.java
Normal file
@ -0,0 +1,9 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public interface EventConsumer<K> {
|
||||
|
||||
Flux<Timestamped<K>> consumeMessages();
|
||||
}
|
11
src/main/java/it/tdlight/reactiveapi/EventProducer.java
Normal file
11
src/main/java/it/tdlight/reactiveapi/EventProducer.java
Normal file
@ -0,0 +1,11 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface EventProducer<K> {
|
||||
|
||||
Mono<Void> sendMessages(Flux<K> eventsFlux);
|
||||
|
||||
void close();
|
||||
}
|
58
src/main/java/it/tdlight/reactiveapi/InstanceSettings.java
Normal file
58
src/main/java/it/tdlight/reactiveapi/InstanceSettings.java
Normal file
@ -0,0 +1,58 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class InstanceSettings {
|
||||
|
||||
@NotNull
|
||||
public String id;
|
||||
|
||||
public InstanceType instanceType;
|
||||
|
||||
/**
|
||||
* If {@link #instanceType} is true, this will be the address of this client
|
||||
*/
|
||||
public @Nullable String listenAddress;
|
||||
|
||||
/**
|
||||
* If {@link #instanceType} is false, this will transform resulting events <b>before</b> being sent
|
||||
*/
|
||||
public @Nullable List<Class<? extends ResultingEventTransformer>> resultingEventTransformers;
|
||||
|
||||
public InstanceSettings(@NotNull String id,
|
||||
@NotNull InstanceType instanceType,
|
||||
@Nullable String listenAddress,
|
||||
@Nullable List<Class<? extends ResultingEventTransformer>> resultingEventTransformers) {
|
||||
this.id = id;
|
||||
this.instanceType = instanceType;
|
||||
this.listenAddress = listenAddress;
|
||||
this.resultingEventTransformers = resultingEventTransformers;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public InstanceSettings(@JsonProperty(required = true, value = "id") @NotNull String id,
|
||||
@Deprecated @JsonProperty(value = "client", defaultValue = "null") Boolean deprecatedIsClient,
|
||||
@JsonProperty(value = "instanceType", defaultValue = "null") String instanceType,
|
||||
@Deprecated @JsonProperty(value = "clientAddress", defaultValue = "null") @Nullable String deprecatedClientAddress,
|
||||
@JsonProperty(value = "listenAddress", defaultValue = "null") @Nullable String listenAddress,
|
||||
@JsonProperty("resultingEventTransformers")
|
||||
@Nullable List<Class<? extends ResultingEventTransformer>> resultingEventTransformers) {
|
||||
this.id = id;
|
||||
if (deprecatedIsClient != null) {
|
||||
this.instanceType = deprecatedIsClient ? InstanceType.UPDATES_CONSUMER : InstanceType.TDLIB;
|
||||
} else {
|
||||
this.instanceType = InstanceType.valueOf(instanceType.toUpperCase());
|
||||
}
|
||||
if (deprecatedClientAddress != null) {
|
||||
this.listenAddress = deprecatedClientAddress;
|
||||
} else {
|
||||
this.listenAddress = listenAddress;
|
||||
}
|
||||
this.resultingEventTransformers = resultingEventTransformers;
|
||||
}
|
||||
}
|
6
src/main/java/it/tdlight/reactiveapi/InstanceType.java
Normal file
6
src/main/java/it/tdlight/reactiveapi/InstanceType.java
Normal file
@ -0,0 +1,6 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
public enum InstanceType {
|
||||
UPDATES_CONSUMER,
|
||||
TDLIB
|
||||
}
|
21
src/main/java/it/tdlight/reactiveapi/KafkaParameters.java
Normal file
21
src/main/java/it/tdlight/reactiveapi/KafkaParameters.java
Normal file
@ -0,0 +1,21 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public record KafkaParameters(String groupId, String clientId, List<String> bootstrapServers,
|
||||
List<String> lanes) implements ChannelsParameters {
|
||||
|
||||
public String getBootstrapServersString() {
|
||||
return String.join(",", bootstrapServers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAllLanes() {
|
||||
var lanes = new LinkedHashSet<String>(this.lanes.size() + 1);
|
||||
lanes.add("main");
|
||||
lanes.addAll(this.lanes);
|
||||
return lanes;
|
||||
}
|
||||
}
|
5
src/main/java/it/tdlight/reactiveapi/Lanes.java
Normal file
5
src/main/java/it/tdlight/reactiveapi/Lanes.java
Normal file
@ -0,0 +1,5 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
public class Lanes {
|
||||
public static final String MAIN_LANE = "main";
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public class LiveAtomixReactiveApiClient extends BaseAtomixReactiveApiClient {
|
||||
|
||||
private final TdlibChannelsSharedReceive sharedTdlibClients;
|
||||
|
||||
LiveAtomixReactiveApiClient(TdlibChannelsSharedReceive sharedTdlibClients) {
|
||||
super(sharedTdlibClients);
|
||||
this.sharedTdlibClients = sharedTdlibClients;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ClientBoundEvent> clientBoundEvents(String lane) {
|
||||
return sharedTdlibClients.events(lane).map(Timestamped::data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Flux<ClientBoundEvent>> clientBoundEvents() {
|
||||
return sharedTdlibClients.events().entrySet().stream()
|
||||
.collect(Collectors.toUnmodifiableMap(Entry::getKey, e -> e.getValue().map(Timestamped::data)));
|
||||
}
|
||||
}
|
72
src/main/java/it/tdlight/reactiveapi/RSocketParameters.java
Normal file
72
src/main/java/it/tdlight/reactiveapi/RSocketParameters.java
Normal file
@ -0,0 +1,72 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public final class RSocketParameters implements ChannelsParameters {
|
||||
|
||||
private final boolean client;
|
||||
private final TransportFactory transportFactory;
|
||||
private final List<String> lanes;
|
||||
|
||||
public RSocketParameters(boolean client, String host, List<String> lanes) {
|
||||
this.client = client;
|
||||
var hostAndPort = HostAndPort.fromString(host);
|
||||
this.transportFactory = TransportFactory.tcp(hostAndPort);
|
||||
this.lanes = lanes;
|
||||
}
|
||||
|
||||
public RSocketParameters(boolean client, TransportFactory transportFactory, List<String> lanes) {
|
||||
this.client = client;
|
||||
this.transportFactory = transportFactory;
|
||||
this.lanes = lanes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAllLanes() {
|
||||
var lanes = new LinkedHashSet<String>(this.lanes.size() + 1);
|
||||
lanes.add("main");
|
||||
lanes.addAll(this.lanes);
|
||||
return lanes;
|
||||
}
|
||||
|
||||
public boolean isClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
public TransportFactory transportFactory() {
|
||||
return transportFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
RSocketParameters that = (RSocketParameters) o;
|
||||
return client == that.client && Objects.equals(transportFactory, that.transportFactory) && Objects.equals(lanes,
|
||||
that.lanes
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(client, transportFactory, lanes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", RSocketParameters.class.getSimpleName() + "[", "]")
|
||||
.add("client=" + client)
|
||||
.add("transportFactory=" + transportFactory)
|
||||
.add("lanes=" + lanes)
|
||||
.toString();
|
||||
}
|
||||
}
|
19
src/main/java/it/tdlight/reactiveapi/ReactiveApi.java
Normal file
19
src/main/java/it/tdlight/reactiveapi/ReactiveApi.java
Normal file
@ -0,0 +1,19 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ReactiveApi {
|
||||
|
||||
Mono<Void> start();
|
||||
|
||||
Mono<CreateSessionResponse> createSession(CreateSessionRequest req);
|
||||
|
||||
ReactiveApiMultiClient client();
|
||||
|
||||
Mono<Void> close();
|
||||
|
||||
void waitForExit();
|
||||
}
|
14
src/main/java/it/tdlight/reactiveapi/ReactiveApiClient.java
Normal file
14
src/main/java/it/tdlight/reactiveapi/ReactiveApiClient.java
Normal file
@ -0,0 +1,14 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import java.time.Instant;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ReactiveApiClient extends ReactiveApiThinClient {
|
||||
|
||||
Flux<ClientBoundEvent> clientBoundEvents();
|
||||
|
||||
boolean isPullMode();
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ReactiveApiMultiClient {
|
||||
|
||||
Flux<ClientBoundEvent> clientBoundEvents(String lane);
|
||||
|
||||
Map<String, Flux<ClientBoundEvent>> clientBoundEvents();
|
||||
|
||||
<T extends TdApi.Object> Mono<T> request(long userId, TdApi.Function<T> request, Instant timeout);
|
||||
|
||||
Mono<Void> close();
|
||||
|
||||
default ReactiveApiThinClient view(long userId) {
|
||||
return new ReactiveApiThinClient() {
|
||||
@Override
|
||||
public <T extends Object> Mono<T> request(Function<T> request, Instant timeout) {
|
||||
return ReactiveApiMultiClient.this.request(userId, request, timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
656
src/main/java/it/tdlight/reactiveapi/ReactiveApiPublisher.java
Normal file
656
src/main/java/it/tdlight/reactiveapi/ReactiveApiPublisher.java
Normal file
@ -0,0 +1,656 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static it.tdlight.reactiveapi.AuthPhase.LOGGED_IN;
|
||||
import static it.tdlight.reactiveapi.AuthPhase.LOGGED_OUT;
|
||||
import static it.tdlight.reactiveapi.Event.SERIAL_VERSION;
|
||||
import static it.tdlight.reactiveapi.rsocket.FileQueueUtils.convert;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import it.cavallium.filequeue.QueueConsumer;
|
||||
import it.tdlight.common.Init;
|
||||
import it.tdlight.common.ReactiveTelegramClient;
|
||||
import it.tdlight.common.Response;
|
||||
import it.tdlight.common.Signal;
|
||||
import it.tdlight.common.utils.CantLoadLibrary;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateWaitOtherDeviceConfirmation;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateWaitPassword;
|
||||
import it.tdlight.jni.TdApi.CheckAuthenticationBotToken;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.jni.TdApi.PhoneNumberAuthenticationSettings;
|
||||
import it.tdlight.jni.TdApi.SetAuthenticationPhoneNumber;
|
||||
import it.tdlight.jni.TdApi.SetTdlibParameters;
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import it.tdlight.reactiveapi.Event.Ignored;
|
||||
import it.tdlight.reactiveapi.Event.OnBotLoginCodeRequested;
|
||||
import it.tdlight.reactiveapi.Event.OnOtherDeviceLoginRequested;
|
||||
import it.tdlight.reactiveapi.Event.OnPasswordRequested;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest.Request;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse;
|
||||
import it.tdlight.reactiveapi.Event.OnUpdateData;
|
||||
import it.tdlight.reactiveapi.Event.OnUpdateError;
|
||||
import it.tdlight.reactiveapi.Event.OnUserLoginCodeRequested;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ClientBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ClusterBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ResultingEventPublisherClosed;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
import it.tdlight.tdlight.ClientManager;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutput;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
import java.util.function.Consumer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxSink.OverflowStrategy;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks.EmitFailureHandler;
|
||||
import reactor.core.publisher.Sinks.EmitResult;
|
||||
import reactor.core.publisher.Sinks.Many;
|
||||
import reactor.core.scheduler.Scheduler.Worker;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public abstract class ReactiveApiPublisher {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(ReactiveApiPublisher.class);
|
||||
private static final Duration SPECIAL_RAW_TIMEOUT_DURATION = Duration.ofMinutes(5);
|
||||
private final TdlibChannelsSharedHost sharedTdlibServers;
|
||||
private final Set<ResultingEventTransformer> resultingEventTransformerSet;
|
||||
private final ReactiveTelegramClient rawTelegramClient;
|
||||
private final Flux<Signal> telegramClient;
|
||||
|
||||
private final AtomicReference<State> state = new AtomicReference<>(new State(LOGGED_OUT));
|
||||
protected final long userId;
|
||||
protected final String lane;
|
||||
|
||||
private final Many<OnResponse<TdApi.Object>> responses;
|
||||
|
||||
private final AtomicReference<Disposable> disposable = new AtomicReference<>();
|
||||
private final AtomicReference<Path> path = new AtomicReference<>();
|
||||
|
||||
// Debugging variables
|
||||
final LongAdder receivedUpdates = new LongAdder();
|
||||
final LongAdder bufferedUpdates = new LongAdder();
|
||||
final LongAdder processedUpdates = new LongAdder();
|
||||
final LongAdder clientBoundEvents = new LongAdder();
|
||||
final LongAdder sentClientBoundEvents = new LongAdder();
|
||||
|
||||
private ReactiveApiPublisher(TdlibChannelsSharedHost sharedTdlibServers,
|
||||
Set<ResultingEventTransformer> resultingEventTransformerSet,
|
||||
long userId, String lane) {
|
||||
this.sharedTdlibServers = sharedTdlibServers;
|
||||
this.resultingEventTransformerSet = resultingEventTransformerSet;
|
||||
this.userId = userId;
|
||||
this.lane = Objects.requireNonNull(lane);
|
||||
this.responses = this.sharedTdlibServers.responses();
|
||||
this.rawTelegramClient = ClientManager.createReactive();
|
||||
try {
|
||||
Init.start();
|
||||
} catch (CantLoadLibrary e) {
|
||||
throw new RuntimeException("Can't load TDLight", e);
|
||||
}
|
||||
this.telegramClient = Flux.<Signal>create(sink -> {
|
||||
try {
|
||||
rawTelegramClient.createAndRegisterClient();
|
||||
} catch (Throwable ex) {
|
||||
LOG.error("Failed to initialize client {}", userId, ex);
|
||||
sink.error(ex);
|
||||
return;
|
||||
}
|
||||
rawTelegramClient.setListener(t -> {
|
||||
if (!sink.isCancelled()) {
|
||||
this.receivedUpdates.increment();
|
||||
sink.next(t);
|
||||
}
|
||||
});
|
||||
sink.onCancel(rawTelegramClient::cancel);
|
||||
sink.onDispose(rawTelegramClient::dispose);
|
||||
}, OverflowStrategy.BUFFER).doOnNext(next -> bufferedUpdates.increment());
|
||||
|
||||
|
||||
Stats.STATS.add(this);
|
||||
}
|
||||
|
||||
public static ReactiveApiPublisher fromToken(TdlibChannelsSharedHost sharedTdlibServers,
|
||||
Set<ResultingEventTransformer> resultingEventTransformerSet,
|
||||
long userId,
|
||||
String token,
|
||||
String lane) {
|
||||
return new ReactiveApiPublisherToken(sharedTdlibServers, resultingEventTransformerSet, userId, token, lane);
|
||||
}
|
||||
|
||||
public static ReactiveApiPublisher fromPhoneNumber(TdlibChannelsSharedHost sharedTdlibServers,
|
||||
Set<ResultingEventTransformer> resultingEventTransformerSet,
|
||||
long userId,
|
||||
long phoneNumber,
|
||||
String lane) {
|
||||
return new ReactiveApiPublisherPhoneNumber(sharedTdlibServers,
|
||||
resultingEventTransformerSet,
|
||||
userId,
|
||||
phoneNumber,
|
||||
lane
|
||||
);
|
||||
}
|
||||
|
||||
public void start(Path path, @Nullable Runnable onClose) {
|
||||
this.path.set(path);
|
||||
LOG.info("Starting session \"{}\" in path \"{}\"", this, path);
|
||||
var publishedResultingEvents = telegramClient
|
||||
.subscribeOn(Schedulers.parallel())
|
||||
// Handle signals, then return a ResultingEvent
|
||||
.concatMapIterable(this::onSignal)
|
||||
.doFinally(s -> LOG.trace("Finalized telegram client events"))
|
||||
|
||||
// Transform resulting events using all the registered resulting event transformers
|
||||
.transform(flux -> {
|
||||
Flux<ResultingEvent> transformedFlux = flux;
|
||||
for (ResultingEventTransformer resultingEventTransformer : resultingEventTransformerSet) {
|
||||
transformedFlux = resultingEventTransformer.transform(isBot(), transformedFlux);
|
||||
}
|
||||
return transformedFlux;
|
||||
})
|
||||
|
||||
.publish(512);
|
||||
|
||||
publishedResultingEvents
|
||||
// Obtain only TDLib-bound events
|
||||
.filter(s -> s instanceof TDLibBoundResultingEvent<?>)
|
||||
.<TDLibBoundResultingEvent<?>>map(s -> ((TDLibBoundResultingEvent<?>) s))
|
||||
|
||||
// Buffer requests to avoid halting the event loop
|
||||
.onBackpressureBuffer()
|
||||
|
||||
// Send requests to tdlib
|
||||
.flatMap(req -> Mono
|
||||
.from(rawTelegramClient.send(req.action(), SPECIAL_RAW_TIMEOUT_DURATION))
|
||||
.flatMap(result -> fixBrokenKey(req.action(), result))
|
||||
.mapNotNull(resp -> {
|
||||
if (resp.getConstructor() == TdApi.Error.CONSTRUCTOR) {
|
||||
if (req.ignoreFailure()) {
|
||||
LOG.debug("Received error for special request {}", req.action());
|
||||
return null;
|
||||
} else {
|
||||
LOG.error("Received error for special request {}: {}\nThe instance will be closed", req.action(), resp);
|
||||
return new OnUpdateError(userId, (TdApi.Error) resp);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.doOnError(ex -> LOG.error("Failed to receive the response for special request {}\n"
|
||||
+ " The instance will be closed", req.action(), ex))
|
||||
.onErrorResume(ex -> Mono.just(new OnUpdateError(userId, new TdApi.Error(500, ex.getMessage()))))
|
||||
, Integer.MAX_VALUE, Integer.MAX_VALUE)
|
||||
|
||||
.doOnError(ex -> LOG.error("Failed to receive resulting events. The instance will be closed", ex))
|
||||
.onErrorResume(ex -> Mono.just(new OnUpdateError(userId, new TdApi.Error(500, ex.getMessage()))))
|
||||
|
||||
// when an error arrives, close the session
|
||||
.take(1, true)
|
||||
.concatMap(ignored -> Mono
|
||||
.from(rawTelegramClient.send(new TdApi.Close(), SPECIAL_RAW_TIMEOUT_DURATION))
|
||||
.then(Mono.empty())
|
||||
)
|
||||
.subscribeOn(Schedulers.parallel())
|
||||
.subscribe(v -> {}, ex -> LOG.error("Resulting events flux has failed unexpectedly! (1)", ex));
|
||||
|
||||
var messagesToSend = publishedResultingEvents
|
||||
// Obtain only client-bound events
|
||||
.filter(s -> s instanceof ClientBoundResultingEvent)
|
||||
.cast(ClientBoundResultingEvent.class)
|
||||
.map(ClientBoundResultingEvent::event)
|
||||
|
||||
// Buffer requests to avoid halting the event loop
|
||||
.doOnNext(clientBoundEvent -> clientBoundEvents.increment())
|
||||
.transform(ReactorUtils.onBackpressureBufferSubscribe(Paths.get(""),
|
||||
"client-bound-resulting-events-" + userId,
|
||||
false,
|
||||
new ClientBoundEventSerializer(),
|
||||
new ClientBoundEventDeserializer()
|
||||
))
|
||||
.doOnNext(clientBoundEvent -> sentClientBoundEvents.increment())
|
||||
|
||||
.as(ReactorUtils::subscribeOnceUntilUnsubscribe);
|
||||
|
||||
sharedTdlibServers.events(lane, messagesToSend);
|
||||
|
||||
publishedResultingEvents
|
||||
// Obtain only cluster-bound events
|
||||
.filter(s -> s instanceof ClusterBoundResultingEvent)
|
||||
.cast(ClusterBoundResultingEvent.class)
|
||||
|
||||
// Buffer requests to avoid halting the event loop
|
||||
.onBackpressureBuffer()
|
||||
|
||||
// Send events to the cluster
|
||||
.subscribeOn(Schedulers.parallel())
|
||||
.subscribe(clusterBoundEvent -> {
|
||||
if (clusterBoundEvent instanceof ResultingEventPublisherClosed) {
|
||||
if (onClose != null) {
|
||||
onClose.run();
|
||||
}
|
||||
} else {
|
||||
LOG.error("Unknown cluster-bound event: {}", clusterBoundEvent);
|
||||
}
|
||||
}, ex -> LOG.error("Resulting events flux has failed unexpectedly! (2)", ex));
|
||||
|
||||
|
||||
var prev = this.disposable.getAndSet(publishedResultingEvents.connect());
|
||||
if (prev != null) {
|
||||
LOG.error("The API started twice!");
|
||||
prev.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends TdApi.Object> Mono<TdApi.Object> fixBrokenKey(Function<T> function, TdApi.Object result) {
|
||||
if (result.getConstructor() == TdApi.Error.CONSTRUCTOR
|
||||
&& function instanceof TdApi.SetTdlibParameters setTdlibParameters) {
|
||||
// Fix legacy "cucumbers" password
|
||||
if (setTdlibParameters.databaseEncryptionKey == null
|
||||
&& "Wrong password".equals(((TdApi.Error) result).message)) {
|
||||
|
||||
setTdlibParameters.databaseEncryptionKey = "cucumber".getBytes(StandardCharsets.US_ASCII);
|
||||
Mono<TdApi.Object> oldKeyCheckResultMono = Mono
|
||||
.from(rawTelegramClient.send(setTdlibParameters, SPECIAL_RAW_TIMEOUT_DURATION));
|
||||
return oldKeyCheckResultMono.flatMap(oldKeyCheckResult -> {
|
||||
if (oldKeyCheckResult.getConstructor() != TdApi.Error.CONSTRUCTOR) {
|
||||
var fixOldKeyFunction = new TdApi.SetDatabaseEncryptionKey();
|
||||
return Mono
|
||||
.from(rawTelegramClient.send(fixOldKeyFunction, SPECIAL_RAW_TIMEOUT_DURATION));
|
||||
} else {
|
||||
return Mono.just(oldKeyCheckResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return Mono.just(result);
|
||||
}
|
||||
|
||||
protected abstract boolean isBot();
|
||||
|
||||
private ResultingEvent wrapUpdateSignal(Signal signal) {
|
||||
var update = (TdApi.Update) signal.getUpdate();
|
||||
return new ClientBoundResultingEvent(new OnUpdateData(userId, update));
|
||||
}
|
||||
|
||||
private List<ResultingEvent> withUpdateSignal(Signal signal, List<ResultingEvent> list) {
|
||||
var result = new ArrayList<ResultingEvent>(list.size() + 1);
|
||||
result.add(wrapUpdateSignal(signal));
|
||||
result.addAll(list);
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<@NotNull ResultingEvent> onSignal(Signal signal) {
|
||||
// Update the state
|
||||
var state = this.state.updateAndGet(oldState -> oldState.withSignal(signal));
|
||||
|
||||
processedUpdates.increment();
|
||||
if (state.authPhase() == LOGGED_IN) {
|
||||
ResultingEvent resultingEvent = wrapUpdateSignal(signal);
|
||||
return List.of(resultingEvent);
|
||||
} else {
|
||||
LOG.trace("Signal has not been broadcast because the session {} is not logged in: {}", userId, signal);
|
||||
return this.handleSpecialSignal(state, signal);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("SwitchStatementWithTooFewBranches")
|
||||
@NotNull
|
||||
private List<@NotNull ResultingEvent> handleSpecialSignal(State state, Signal signal) {
|
||||
if (signal.isException()) {
|
||||
LOG.error("Received an error signal", signal.getException());
|
||||
return List.of();
|
||||
}
|
||||
if (signal.isClosed()) {
|
||||
signal.getClosed();
|
||||
LOG.info("Received a closed signal");
|
||||
return List.of(new ClientBoundResultingEvent(new OnUpdateData(userId,
|
||||
new TdApi.UpdateAuthorizationState(new AuthorizationStateClosed())
|
||||
)), new ResultingEventPublisherClosed());
|
||||
}
|
||||
if (signal.isUpdate() && signal.getUpdate().getConstructor() == TdApi.Error.CONSTRUCTOR) {
|
||||
var error = ((TdApi.Error) signal.getUpdate());
|
||||
LOG.error("Received a TDLib error signal! Error {}: {}", error.code, error.message);
|
||||
return List.of();
|
||||
}
|
||||
if (!signal.isUpdate()) {
|
||||
LOG.error("Received a signal that's not an update: {}", signal);
|
||||
return List.of();
|
||||
}
|
||||
var update = signal.getUpdate();
|
||||
var updateResult = wrapUpdateSignal(signal);
|
||||
switch (state.authPhase()) {
|
||||
case BROKEN -> {}
|
||||
case PARAMETERS_PHASE -> {
|
||||
switch (update.getConstructor()) {
|
||||
case TdApi.UpdateAuthorizationState.CONSTRUCTOR -> {
|
||||
var updateAuthorizationState = (TdApi.UpdateAuthorizationState) update;
|
||||
switch (updateAuthorizationState.authorizationState.getConstructor()) {
|
||||
case TdApi.AuthorizationStateWaitTdlibParameters.CONSTRUCTOR -> {
|
||||
SetTdlibParameters parameters = generateTDLibParameters();
|
||||
return List.of(updateResult, new TDLibBoundResultingEvent<>(parameters));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case AUTH_PHASE -> {
|
||||
switch (update.getConstructor()) {
|
||||
case TdApi.UpdateAuthorizationState.CONSTRUCTOR -> {
|
||||
var updateAuthorizationState = (TdApi.UpdateAuthorizationState) update;
|
||||
switch (updateAuthorizationState.authorizationState.getConstructor()) {
|
||||
case TdApi.AuthorizationStateWaitCode.CONSTRUCTOR -> {
|
||||
return withUpdateSignal(signal, onWaitCode());
|
||||
}
|
||||
case TdApi.AuthorizationStateWaitOtherDeviceConfirmation.CONSTRUCTOR -> {
|
||||
var link = ((AuthorizationStateWaitOtherDeviceConfirmation) updateAuthorizationState.authorizationState).link;
|
||||
return List.of(updateResult,
|
||||
new ClientBoundResultingEvent(new OnOtherDeviceLoginRequested(userId, link)));
|
||||
}
|
||||
case TdApi.AuthorizationStateWaitPassword.CONSTRUCTOR -> {
|
||||
var authorizationStateWaitPassword = ((AuthorizationStateWaitPassword) updateAuthorizationState.authorizationState);
|
||||
return List.of(updateResult,
|
||||
new ClientBoundResultingEvent(new OnPasswordRequested(userId,
|
||||
authorizationStateWaitPassword.passwordHint,
|
||||
authorizationStateWaitPassword.hasRecoveryEmailAddress,
|
||||
authorizationStateWaitPassword.recoveryEmailAddressPattern
|
||||
))
|
||||
);
|
||||
}
|
||||
case TdApi.AuthorizationStateWaitPhoneNumber.CONSTRUCTOR -> {
|
||||
return withUpdateSignal(signal, onWaitToken());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private SetTdlibParameters generateTDLibParameters() {
|
||||
var tdlibParameters = new SetTdlibParameters();
|
||||
var path = requireNonNull(this.path.get(), "Path must not be null");
|
||||
tdlibParameters.databaseDirectory = path + "?use_custom_database_format=true";
|
||||
tdlibParameters.apiId = 376588;
|
||||
tdlibParameters.apiHash = "2143fdfc2bbba3ec723228d2f81336c9";
|
||||
tdlibParameters.filesDirectory = path.resolve("user_storage").toString();
|
||||
tdlibParameters.applicationVersion = it.tdlight.reactiveapi.generated.LibraryVersion.VERSION;
|
||||
tdlibParameters.deviceModel = System.getProperty("os.name");
|
||||
tdlibParameters.systemVersion = System.getProperty("os.version");
|
||||
tdlibParameters.enableStorageOptimizer = true;
|
||||
tdlibParameters.ignoreFileNames = true;
|
||||
tdlibParameters.useTestDc = false;
|
||||
tdlibParameters.useSecretChats = false;
|
||||
|
||||
tdlibParameters.useMessageDatabase = true;
|
||||
tdlibParameters.useFileDatabase = true;
|
||||
tdlibParameters.useChatInfoDatabase = true;
|
||||
tdlibParameters.systemLanguageCode = System.getProperty("user.language", "en");
|
||||
return tdlibParameters;
|
||||
}
|
||||
|
||||
protected abstract List<ResultingEvent> onWaitToken();
|
||||
|
||||
protected List<ResultingEvent> onWaitCode() {
|
||||
LOG.error("Wait code event is not supported");
|
||||
return List.of();
|
||||
}
|
||||
|
||||
public static byte[] serializeEvents(List<ClientBoundEvent> clientBoundEvents) {
|
||||
try (var byteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
try (var dataOutputStream = new DataOutputStream(byteArrayOutputStream)) {
|
||||
dataOutputStream.writeInt(clientBoundEvents.size());
|
||||
for (ClientBoundEvent clientBoundEvent : clientBoundEvents) {
|
||||
writeClientBoundEvent(clientBoundEvent, dataOutputStream);
|
||||
}
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new SerializationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] serializeEvent(ClientBoundEvent clientBoundEvent) {
|
||||
try (var byteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
try (var dataOutputStream = new DataOutputStream(byteArrayOutputStream)) {
|
||||
writeClientBoundEvent(clientBoundEvent, dataOutputStream);
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new SerializationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeClientBoundEvent(ClientBoundEvent clientBoundEvent, DataOutput dataOutputStream)
|
||||
throws IOException {
|
||||
dataOutputStream.writeLong(clientBoundEvent.userId());
|
||||
dataOutputStream.writeInt(SERIAL_VERSION);
|
||||
if (clientBoundEvent instanceof OnUpdateData onUpdateData) {
|
||||
dataOutputStream.writeByte(0x1);
|
||||
onUpdateData.update().serialize(dataOutputStream);
|
||||
} else if (clientBoundEvent instanceof OnUpdateError onUpdateError) {
|
||||
dataOutputStream.writeByte(0x2);
|
||||
onUpdateError.error().serialize(dataOutputStream);
|
||||
} else if (clientBoundEvent instanceof OnUserLoginCodeRequested onUserLoginCodeRequested) {
|
||||
dataOutputStream.writeByte(0x3);
|
||||
dataOutputStream.writeLong(onUserLoginCodeRequested.phoneNumber());
|
||||
} else if (clientBoundEvent instanceof OnBotLoginCodeRequested onBotLoginCodeRequested) {
|
||||
dataOutputStream.writeByte(0x4);
|
||||
dataOutputStream.writeUTF(onBotLoginCodeRequested.token());
|
||||
} else if (clientBoundEvent instanceof OnOtherDeviceLoginRequested onOtherDeviceLoginRequested) {
|
||||
dataOutputStream.writeByte(0x5);
|
||||
dataOutputStream.writeUTF(onOtherDeviceLoginRequested.link());
|
||||
} else if (clientBoundEvent instanceof OnPasswordRequested onPasswordRequested) {
|
||||
dataOutputStream.writeByte(0x6);
|
||||
dataOutputStream.writeUTF(onPasswordRequested.passwordHint());
|
||||
dataOutputStream.writeBoolean(onPasswordRequested.hasRecoveryEmail());
|
||||
dataOutputStream.writeUTF(onPasswordRequested.recoveryEmailPattern());
|
||||
} else if (clientBoundEvent instanceof Ignored) {
|
||||
dataOutputStream.writeByte(0x7);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unexpected value: " + clientBoundEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] serializeResponse(Response response) {
|
||||
if (response == null) return null;
|
||||
var id = response.getId();
|
||||
var object = response.getObject();
|
||||
try (var byteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
try (var dataOutputStream = new DataOutputStream(byteArrayOutputStream)) {
|
||||
dataOutputStream.writeInt(SERIAL_VERSION);
|
||||
//dataOutputStream.writeLong(id);
|
||||
object.serialize(dataOutputStream);
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new SerializationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void handleRequest(OnRequest<TdApi.Object> onRequestObj) {
|
||||
handleRequestInternal(onRequestObj, response -> {
|
||||
EmitResult status;
|
||||
synchronized (this.responses) {
|
||||
status = this.responses.tryEmitNext(response);
|
||||
}
|
||||
if (status.isFailure()) {
|
||||
switch (status) {
|
||||
case FAIL_ZERO_SUBSCRIBER ->
|
||||
LOG.warn("Failed to send response of request {}, user {}, client {}: no subscribers",
|
||||
onRequestObj.userId(), onRequestObj.userId(), onRequestObj.clientId());
|
||||
case FAIL_OVERFLOW ->
|
||||
LOG.warn("Failed to send response of request {}, user {}, client {}: too many unsent responses",
|
||||
onRequestObj.userId(), onRequestObj.userId(), onRequestObj.clientId());
|
||||
default -> LOG.error("Failed to send response of request {}, user {}, client {}: {}",
|
||||
onRequestObj.userId(), onRequestObj.userId(), onRequestObj.clientId(), status);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleRequestInternal(OnRequest<TdApi.Object> onRequestObj, Consumer<Event.OnResponse.Response<TdApi.Object>> r) {
|
||||
if (onRequestObj instanceof OnRequest.InvalidRequest invalidRequest) {
|
||||
r.accept(new Event.OnResponse.Response<>(invalidRequest.clientId(),
|
||||
invalidRequest.requestId(),
|
||||
userId,
|
||||
new TdApi.Error(400, "Conflicting protocol version")
|
||||
));
|
||||
return;
|
||||
}
|
||||
var requestObj = (Request<Object>) onRequestObj;
|
||||
var requestWithTimeoutInstant = new RequestWithTimeoutInstant<>(requestObj.request(), requestObj.timeout());
|
||||
var state = this.state.get();
|
||||
if (state.authPhase() == LOGGED_IN) {
|
||||
var request = requestWithTimeoutInstant.request();
|
||||
var timeoutDuration = Duration.between(Instant.now(), requestWithTimeoutInstant.timeout());
|
||||
if (timeoutDuration.isZero() || timeoutDuration.isNegative()) {
|
||||
LOG.warn("Received an expired request. Expiration: {}", requestWithTimeoutInstant.timeout());
|
||||
}
|
||||
|
||||
rawTelegramClient.send(request, timeoutDuration).subscribe(new Subscriber<Object>() {
|
||||
@Override
|
||||
public void onSubscribe(Subscription subscription) {
|
||||
subscription.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(Object responseObj) {
|
||||
try {
|
||||
r.accept(new Event.OnResponse.Response<>(onRequestObj.clientId(),
|
||||
onRequestObj.requestId(),
|
||||
userId, responseObj));
|
||||
} catch (Throwable ex) {
|
||||
onError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
LOG.error("Unexpected error while processing response for update {}, user {}, client {}",
|
||||
onRequestObj.requestId(),
|
||||
onRequestObj.userId(),
|
||||
onRequestObj.clientId()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
|
||||
}
|
||||
});
|
||||
} else {
|
||||
LOG.error("Ignored a request to {} because the current state is {}. Request: {}", userId, state, requestObj);
|
||||
r.accept(new Event.OnResponse.Response<>(onRequestObj.clientId(),
|
||||
onRequestObj.requestId(),
|
||||
userId, new TdApi.Error(503, "Service Unavailable: " + state)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", ReactiveApiPublisher.class.getSimpleName() + "[", "]")
|
||||
.add("userId=" + userId)
|
||||
.toString();
|
||||
}
|
||||
|
||||
private record RequestWithTimeoutInstant<T extends TdApi.Object>(TdApi.Function<T> request, Instant timeout) {}
|
||||
|
||||
private static class ReactiveApiPublisherToken extends ReactiveApiPublisher {
|
||||
|
||||
private final String botToken;
|
||||
|
||||
public ReactiveApiPublisherToken(TdlibChannelsSharedHost sharedTdlibServers,
|
||||
Set<ResultingEventTransformer> resultingEventTransformerSet,
|
||||
long userId,
|
||||
String botToken,
|
||||
String lane) {
|
||||
super(sharedTdlibServers, resultingEventTransformerSet, userId, lane);
|
||||
this.botToken = botToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isBot() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ResultingEvent> onWaitToken() {
|
||||
return List.of(new TDLibBoundResultingEvent<>(new CheckAuthenticationBotToken(botToken)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", ReactiveApiPublisherToken.class.getSimpleName() + "[", "]")
|
||||
.add("userId=" + userId)
|
||||
.add("token='" + botToken + "'")
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReactiveApiPublisherPhoneNumber extends ReactiveApiPublisher {
|
||||
|
||||
private final long phoneNumber;
|
||||
|
||||
public ReactiveApiPublisherPhoneNumber(TdlibChannelsSharedHost sharedTdlibServers,
|
||||
Set<ResultingEventTransformer> resultingEventTransformerSet,
|
||||
long userId,
|
||||
long phoneNumber,
|
||||
String lane) {
|
||||
super(sharedTdlibServers, resultingEventTransformerSet, userId, lane);
|
||||
this.phoneNumber = phoneNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isBot() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ResultingEvent> onWaitToken() {
|
||||
var authSettings = new PhoneNumberAuthenticationSettings();
|
||||
authSettings.allowFlashCall = false;
|
||||
authSettings.allowSmsRetrieverApi = false;
|
||||
authSettings.isCurrentPhoneNumber = false;
|
||||
return List.of(new TDLibBoundResultingEvent<>(new SetAuthenticationPhoneNumber("+" + phoneNumber,
|
||||
authSettings
|
||||
)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResultingEvent> onWaitCode() {
|
||||
return List.of(new ClientBoundResultingEvent(new OnUserLoginCodeRequested(userId, phoneNumber)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", ReactiveApiPublisherPhoneNumber.class.getSimpleName() + "[", "]")
|
||||
.add("userId=" + userId)
|
||||
.add("phoneNumber=" + phoneNumber)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import java.time.Instant;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ReactiveApiThinClient {
|
||||
|
||||
<T extends TdApi.Object> Mono<T> request(TdApi.Function<T> request, Instant timeout);
|
||||
|
||||
long getUserId();
|
||||
}
|
317
src/main/java/it/tdlight/reactiveapi/ReactorUtils.java
Normal file
317
src/main/java/it/tdlight/reactiveapi/ReactorUtils.java
Normal file
@ -0,0 +1,317 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.cavallium.filequeue.IQueueToConsumer;
|
||||
import it.cavallium.filequeue.LMDBQueueToConsumer;
|
||||
import it.tdlight.reactiveapi.rsocket.FileQueueUtils;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.LongConsumer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.reactivestreams.Subscription;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxSink;
|
||||
import reactor.core.publisher.FluxSink.OverflowStrategy;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Signal;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
public class ReactorUtils {
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static final WaitingSink WAITING_SINK = new WaitingSink<>();
|
||||
|
||||
public static <V> Flux<V> subscribeOnce(Flux<V> f) {
|
||||
AtomicBoolean subscribed = new AtomicBoolean();
|
||||
return f.doOnSubscribe(s -> {
|
||||
if (!subscribed.compareAndSet(false, true)) {
|
||||
throw new UnsupportedOperationException("Can't subscribe more than once!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static <V> Flux<V> subscribeOnceUntilUnsubscribe(Flux<V> f) {
|
||||
AtomicBoolean subscribed = new AtomicBoolean();
|
||||
return f.doOnSubscribe(s -> {
|
||||
if (!subscribed.compareAndSet(false, true)) {
|
||||
throw new UnsupportedOperationException("Can't subscribe more than once!");
|
||||
}
|
||||
}).doFinally(s -> subscribed.set(false));
|
||||
}
|
||||
|
||||
public static <V> Mono<V> subscribeOnce(Mono<V> f) {
|
||||
AtomicBoolean subscribed = new AtomicBoolean();
|
||||
return f.doOnSubscribe(s -> {
|
||||
if (!subscribed.compareAndSet(false, true)) {
|
||||
throw new UnsupportedOperationException("Can't subscribe more than once!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static <V> Mono<V> subscribeOnceUntilUnsubscribe(Mono<V> f) {
|
||||
AtomicBoolean subscribed = new AtomicBoolean();
|
||||
return f.doOnSubscribe(s -> {
|
||||
if (!subscribed.compareAndSet(false, true)) {
|
||||
throw new UnsupportedOperationException("Can't subscribe more than once!");
|
||||
}
|
||||
}).doFinally(s -> subscribed.set(false));
|
||||
}
|
||||
|
||||
public static <K> Flux<K> createLastestSubscriptionFlux(Flux<K> upstream, int maxBufferSize) {
|
||||
return upstream.transform(parent -> {
|
||||
AtomicReference<Subscription> subscriptionAtomicReference = new AtomicReference<>();
|
||||
AtomicReference<FluxSink<K>> prevEmitterRef = new AtomicReference<>();
|
||||
Deque<Signal<K>> queue = new ArrayDeque<>(maxBufferSize);
|
||||
|
||||
return Flux.<K>create(emitter -> {
|
||||
var prevEmitter = prevEmitterRef.getAndSet(emitter);
|
||||
|
||||
if (prevEmitter != null) {
|
||||
if (prevEmitter != WAITING_SINK) {
|
||||
prevEmitter.error(new CancellationException());
|
||||
}
|
||||
synchronized (queue) {
|
||||
Signal<K> next;
|
||||
while (!emitter.isCancelled() && (next = queue.peek()) != null) {
|
||||
if (next.isOnNext()) {
|
||||
queue.poll();
|
||||
var nextVal = next.get();
|
||||
assert nextVal != null;
|
||||
emitter.next(nextVal);
|
||||
} else if (next.isOnError()) {
|
||||
var throwable = next.getThrowable();
|
||||
assert throwable != null;
|
||||
emitter.error(throwable);
|
||||
break;
|
||||
} else if (next.isOnComplete()) {
|
||||
emitter.complete();
|
||||
break;
|
||||
} else {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parent.subscribe(new CoreSubscriber<>() {
|
||||
@Override
|
||||
public void onSubscribe(@NotNull Subscription s) {
|
||||
subscriptionAtomicReference.set(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(K payload) {
|
||||
FluxSink<K> prevEmitter = prevEmitterRef.get();
|
||||
if (prevEmitter != WAITING_SINK) {
|
||||
prevEmitter.next(payload);
|
||||
} else {
|
||||
synchronized (queue) {
|
||||
queue.add(Signal.next(payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
FluxSink<K> prevEmitter = prevEmitterRef.get();
|
||||
synchronized (queue) {
|
||||
queue.add(Signal.error(throwable));
|
||||
}
|
||||
if (prevEmitter != WAITING_SINK) {
|
||||
prevEmitter.error(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
FluxSink<K> prevEmitter = prevEmitterRef.get();
|
||||
synchronized (queue) {
|
||||
queue.add(Signal.complete());
|
||||
}
|
||||
if (prevEmitter != WAITING_SINK) {
|
||||
prevEmitter.complete();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
var s = subscriptionAtomicReference.get();
|
||||
emitter.onRequest(n -> {
|
||||
if (n > maxBufferSize) {
|
||||
emitter.error(new UnsupportedOperationException(
|
||||
"Requests count is bigger than max buffer size! " + n + " > " + maxBufferSize));
|
||||
} else {
|
||||
s.request(n);
|
||||
}
|
||||
});
|
||||
//noinspection unchecked
|
||||
emitter.onCancel(() -> prevEmitterRef.compareAndSet(emitter, WAITING_SINK));
|
||||
//noinspection unchecked
|
||||
emitter.onDispose(() -> prevEmitterRef.compareAndSet(emitter, WAITING_SINK));
|
||||
}, OverflowStrategy.BUFFER);
|
||||
});
|
||||
}
|
||||
|
||||
public static <T> Function<Flux<T>, Flux<T>> onBackpressureBufferSubscribe(Path path,
|
||||
String name,
|
||||
boolean persistent,
|
||||
Serializer<T> serializer,
|
||||
Deserializer<T> deserializer) {
|
||||
return flux -> {
|
||||
AtomicReference<FluxSink<T>> ref = new AtomicReference<>();
|
||||
var queuePath = path.resolve(".tdlib-queue");
|
||||
IQueueToConsumer<T> queue = new LMDBQueueToConsumer<>(queuePath,
|
||||
name,
|
||||
!persistent,
|
||||
FileQueueUtils.convert(serializer),
|
||||
FileQueueUtils.convert(deserializer),
|
||||
signal -> {
|
||||
var sink = ref.get();
|
||||
if (sink != null && !sink.isCancelled() && sink.requestedFromDownstream() > 0) {
|
||||
if (signal != null) {
|
||||
sink.next(signal);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
AtomicReference<Throwable> startEx = new AtomicReference<>();
|
||||
var disposable = flux
|
||||
.subscribeOn(Schedulers.parallel())
|
||||
.publishOn(Schedulers.boundedElastic())
|
||||
.subscribe(queue::add, ex -> {
|
||||
startEx.set(ex);
|
||||
var refVal = ref.get();
|
||||
if (refVal != null) {
|
||||
refVal.error(ex);
|
||||
}
|
||||
});
|
||||
queue.startQueue();
|
||||
return Flux.create(sink -> {
|
||||
sink.onDispose(() -> {
|
||||
disposable.dispose();
|
||||
queue.close();
|
||||
});
|
||||
var startExVal = startEx.get();
|
||||
if (startExVal != null) {
|
||||
sink.error(startExVal);
|
||||
return;
|
||||
}
|
||||
ref.set(sink);
|
||||
sink.onCancel(() -> ref.set(null));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public static <T> Function<Flux<T>, Flux<T>> onBackpressureBuffer(Path path,
|
||||
String name,
|
||||
boolean persistent,
|
||||
Serializer<T> serializer,
|
||||
Deserializer<T> deserializer) {
|
||||
return flux -> Flux.<T>create(sink -> {
|
||||
var queuePath = path.resolve(".tdlib-queue");
|
||||
var queue = new LMDBQueueToConsumer<>(queuePath,
|
||||
name,
|
||||
!persistent,
|
||||
FileQueueUtils.convert(serializer),
|
||||
FileQueueUtils.convert(deserializer),
|
||||
signal -> {
|
||||
if (sink.requestedFromDownstream() > 0 && !sink.isCancelled()) {
|
||||
if (signal != null) {
|
||||
sink.next(signal);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
sink.onDispose(queue::close);
|
||||
flux
|
||||
.subscribeOn(Schedulers.parallel())
|
||||
.publishOn(Schedulers.boundedElastic())
|
||||
.subscribe(new CoreSubscriber<>() {
|
||||
@Override
|
||||
public void onSubscribe(@NotNull Subscription s) {
|
||||
sink.onCancel(s::cancel);
|
||||
s.request(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(T element) {
|
||||
if (!sink.isCancelled()) {
|
||||
queue.add(element);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
sink.error(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
}
|
||||
});
|
||||
queue.startQueue();
|
||||
}, OverflowStrategy.ERROR).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
private static class WaitingSink<T> implements FluxSink<T> {
|
||||
|
||||
@Override
|
||||
public @NotNull FluxSink<T> next(@NotNull T t) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void complete() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(@NotNull Throwable e) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Context currentContext() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long requestedFromDownstream() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull FluxSink<T> onRequest(@NotNull LongConsumer consumer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull FluxSink<T> onCancel(@NotNull Disposable d) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull FluxSink<T> onDispose(@NotNull Disposable d) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
25
src/main/java/it/tdlight/reactiveapi/ResultingEvent.java
Normal file
25
src/main/java/it/tdlight/reactiveapi/ResultingEvent.java
Normal file
@ -0,0 +1,25 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ClientBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ClusterBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
|
||||
public sealed interface ResultingEvent permits ClientBoundResultingEvent, TDLibBoundResultingEvent,
|
||||
ClusterBoundResultingEvent {
|
||||
|
||||
record ClientBoundResultingEvent(ClientBoundEvent event) implements ResultingEvent {}
|
||||
|
||||
record TDLibBoundResultingEvent<T extends TdApi.Object>(TdApi.Function<T> action, boolean ignoreFailure) implements
|
||||
ResultingEvent {
|
||||
|
||||
public TDLibBoundResultingEvent(TdApi.Function<T> action) {
|
||||
this(action, false);
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface ClusterBoundResultingEvent extends ResultingEvent permits ResultingEventPublisherClosed {}
|
||||
|
||||
record ResultingEventPublisherClosed() implements ClusterBoundResultingEvent {}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
|
||||
public interface ResultingEventTransformer {
|
||||
|
||||
Flux<ResultingEvent> transform(boolean isBot, Flux<ResultingEvent> events);
|
||||
}
|
20
src/main/java/it/tdlight/reactiveapi/SchedulerExecutor.java
Normal file
20
src/main/java/it/tdlight/reactiveapi/SchedulerExecutor.java
Normal file
@ -0,0 +1,20 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
|
||||
@SuppressWarnings("ClassCanBeRecord")
|
||||
public class SchedulerExecutor implements Executor {
|
||||
|
||||
private final Scheduler scheduler;
|
||||
|
||||
public SchedulerExecutor(Scheduler scheduler) {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(@NotNull Runnable command) {
|
||||
scheduler.schedule(command);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
/**
|
||||
* Any exception during serialization in the producer
|
||||
*/
|
||||
public class SerializationException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public SerializationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public SerializationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SerializationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public SerializationException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/* avoid the expensive and useless stack trace for serialization exceptions */
|
||||
@Override
|
||||
public Throwable fillInStackTrace() {
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
27
src/main/java/it/tdlight/reactiveapi/Serializer.java
Normal file
27
src/main/java/it/tdlight/reactiveapi/Serializer.java
Normal file
@ -0,0 +1,27 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutput;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.util.Map;
|
||||
|
||||
public interface Serializer<T> {
|
||||
|
||||
default byte[] serialize(T data) throws IOException {
|
||||
try (var baos = new FastByteArrayOutputStream()) {
|
||||
try (var daos = new DataOutputStream(baos)) {
|
||||
serialize(data, daos);
|
||||
baos.trim();
|
||||
return baos.array;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default void serialize(T data, DataOutput output) throws IOException {
|
||||
output.write(serialize(data));
|
||||
}
|
||||
}
|
76
src/main/java/it/tdlight/reactiveapi/SignalUtils.java
Normal file
76
src/main/java/it/tdlight/reactiveapi/SignalUtils.java
Normal file
@ -0,0 +1,76 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.common.Signal;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream;
|
||||
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class SignalUtils {
|
||||
|
||||
private static final Serializer<Signal> SERIALIZER = new Serializer<>() {
|
||||
@Override
|
||||
public byte[] serialize(Signal data) {
|
||||
return SignalUtils.serialize(data);
|
||||
}
|
||||
};
|
||||
|
||||
private static final Deserializer<Signal> DESERIALIZER = new Deserializer<>() {
|
||||
@Override
|
||||
public Signal deserialize(byte[] data) {
|
||||
return SignalUtils.deserialize(data);
|
||||
}
|
||||
};
|
||||
|
||||
public static Serializer<Signal> serializer() {
|
||||
return SERIALIZER;
|
||||
}
|
||||
|
||||
public static Deserializer<Signal> deserializer() {
|
||||
return DESERIALIZER;
|
||||
}
|
||||
|
||||
public static Signal deserialize(byte[] bytes) {
|
||||
var dis = new DataInputStream(new FastByteArrayInputStream(bytes));
|
||||
try {
|
||||
byte type = dis.readByte();
|
||||
return switch (type) {
|
||||
case 0 -> Signal.ofUpdate(TdApi.Deserializer.deserialize(dis));
|
||||
case 1 -> Signal.ofUpdateException(new Exception(dis.readUTF()));
|
||||
case 2 -> Signal.ofClosed();
|
||||
default -> throw new IllegalStateException("Unexpected value: " + type);
|
||||
};
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] serialize(Signal signal) {
|
||||
var baos = new FastByteArrayOutputStream();
|
||||
try (var daos = new DataOutputStream(baos)) {
|
||||
if (signal.isUpdate()) {
|
||||
daos.writeByte(0);
|
||||
var up = signal.getUpdate();
|
||||
up.serialize(daos);
|
||||
} else if (signal.isException()) {
|
||||
daos.writeByte(1);
|
||||
var ex = signal.getException();
|
||||
var exMsg = ex.getMessage();
|
||||
daos.writeUTF(exMsg);
|
||||
} else if (signal.isClosed()) {
|
||||
daos.writeByte(2);
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
baos.trim();
|
||||
return baos.array;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.reactivestreams.Subscription;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.EmitFailureHandler;
|
||||
import reactor.core.publisher.Sinks.Empty;
|
||||
|
||||
public abstract class SimpleEventProducer<K> implements EventProducer<K> {
|
||||
|
||||
private AtomicReference<Subscription> closeRequest = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public final Mono<Void> sendMessages(Flux<K> eventsFlux) {
|
||||
return handleSendMessages(eventsFlux).doOnSubscribe(s -> closeRequest.set(s));
|
||||
}
|
||||
|
||||
public abstract Mono<Void> handleSendMessages(Flux<K> eventsFlux);
|
||||
|
||||
@Override
|
||||
public final void close() {
|
||||
var s = closeRequest.get();
|
||||
if (s != null) {
|
||||
s.cancel();
|
||||
}
|
||||
}
|
||||
}
|
128
src/main/java/it/tdlight/reactiveapi/State.java
Normal file
128
src/main/java/it/tdlight/reactiveapi/State.java
Normal file
@ -0,0 +1,128 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static it.tdlight.reactiveapi.AuthPhase.AUTH_PHASE;
|
||||
import static it.tdlight.reactiveapi.AuthPhase.BROKEN;
|
||||
import static it.tdlight.reactiveapi.AuthPhase.LOGGED_IN;
|
||||
import static it.tdlight.reactiveapi.AuthPhase.LOGGED_OUT;
|
||||
import static it.tdlight.reactiveapi.AuthPhase.LOGGING_OUT;
|
||||
import static it.tdlight.reactiveapi.AuthPhase.PARAMETERS_PHASE;
|
||||
|
||||
import io.soabase.recordbuilder.core.RecordBuilder;
|
||||
import it.tdlight.common.Signal;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import java.util.Set;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@RecordBuilder
|
||||
public record State(AuthPhase authPhase) implements StateBuilder.With {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(State.class);
|
||||
|
||||
public State withSignal(Signal signal) {
|
||||
var newState = this;
|
||||
|
||||
// Mark state as broken if the connection is errored unexpectedly
|
||||
if (signal.isException()) {
|
||||
newState = newState.withAuthPhase(BROKEN);
|
||||
}
|
||||
|
||||
newState = switch (newState.authPhase) {
|
||||
// Mark state as broken if the connection is terminated unexpectedly
|
||||
case PARAMETERS_PHASE, AUTH_PHASE, LOGGED_IN -> {
|
||||
if (signal.isClosed()) {
|
||||
yield newState.withAuthPhase(BROKEN);
|
||||
} else {
|
||||
yield newState;
|
||||
}
|
||||
}
|
||||
case LOGGING_OUT -> {
|
||||
// Mark state as logged out if the connection is terminated successfully
|
||||
if (signal.isClosed()) {
|
||||
yield newState.withAuthPhase(LOGGED_OUT);
|
||||
} else {
|
||||
yield newState;
|
||||
}
|
||||
}
|
||||
default -> newState;
|
||||
};
|
||||
|
||||
if (newState.authPhase != BROKEN && signal.isUpdate()) {
|
||||
var update = signal.getUpdate();
|
||||
newState = switch (update.getConstructor()) {
|
||||
// Forcefully logout if the update stream fails
|
||||
case TdApi.Error.CONSTRUCTOR -> newState.withAuthPhase(LOGGED_OUT);
|
||||
case TdApi.UpdateAuthorizationState.CONSTRUCTOR -> {
|
||||
var updateAuthState = (TdApi.UpdateAuthorizationState) update;
|
||||
yield switch (updateAuthState.authorizationState.getConstructor()) {
|
||||
case TdApi.AuthorizationStateClosing.CONSTRUCTOR -> {
|
||||
if (newState.authPhase != LOGGED_IN) {
|
||||
LOG.warn("Logging out, but the current auth phase is {} instead of {}",
|
||||
newState.authPhase,
|
||||
Set.of(LOGGED_IN)
|
||||
);
|
||||
}
|
||||
yield newState.withAuthPhase(LOGGING_OUT);
|
||||
}
|
||||
case TdApi.AuthorizationStateClosed.CONSTRUCTOR -> {
|
||||
if (newState.authPhase != LOGGING_OUT) {
|
||||
LOG.warn("Logged out, but the current auth phase is {} instead of {}",
|
||||
newState.authPhase,
|
||||
Set.of(LOGGING_OUT)
|
||||
);
|
||||
}
|
||||
yield newState.withAuthPhase(LOGGED_OUT);
|
||||
}
|
||||
case TdApi.AuthorizationStateWaitTdlibParameters.CONSTRUCTOR -> {
|
||||
if (newState.authPhase != LOGGED_OUT) {
|
||||
LOG.warn("Waiting parameters, but the current auth phase is {} instead of {}",
|
||||
newState.authPhase,
|
||||
Set.of(LOGGED_OUT)
|
||||
);
|
||||
}
|
||||
yield newState.withAuthPhase(PARAMETERS_PHASE);
|
||||
}
|
||||
case TdApi.AuthorizationStateWaitPhoneNumber.CONSTRUCTOR,
|
||||
TdApi.AuthorizationStateWaitRegistration.CONSTRUCTOR,
|
||||
TdApi.AuthorizationStateWaitCode.CONSTRUCTOR,
|
||||
TdApi.AuthorizationStateWaitPassword.CONSTRUCTOR,
|
||||
TdApi.AuthorizationStateWaitOtherDeviceConfirmation.CONSTRUCTOR -> {
|
||||
if (newState.authPhase != PARAMETERS_PHASE && newState.authPhase != AUTH_PHASE) {
|
||||
LOG.warn(
|
||||
"Waiting for authentication, but the current auth phase is {} instead of {}",
|
||||
newState.authPhase,
|
||||
Set.of(PARAMETERS_PHASE, AUTH_PHASE)
|
||||
);
|
||||
}
|
||||
yield newState.withAuthPhase(AUTH_PHASE);
|
||||
}
|
||||
case TdApi.AuthorizationStateReady.CONSTRUCTOR -> {
|
||||
if (newState.authPhase != PARAMETERS_PHASE && newState.authPhase != AUTH_PHASE) {
|
||||
LOG.warn("Logged in, but the current auth phase is {} instead of {}",
|
||||
newState.authPhase,
|
||||
Set.of(PARAMETERS_PHASE, AUTH_PHASE)
|
||||
);
|
||||
}
|
||||
yield newState.withAuthPhase(LOGGED_IN);
|
||||
}
|
||||
case TdApi.AuthorizationStateLoggingOut.CONSTRUCTOR -> {
|
||||
if (newState.authPhase != LOGGED_IN) {
|
||||
LOG.warn("Logged in, but the current auth phase is {} instead of {}",
|
||||
newState.authPhase,
|
||||
Set.of(LOGGED_IN)
|
||||
);
|
||||
}
|
||||
yield newState.withAuthPhase(LOGGING_OUT);
|
||||
}
|
||||
default -> {
|
||||
LOG.error("Unknown authorization state: {}", updateAuthState.authorizationState);
|
||||
yield newState;
|
||||
}
|
||||
};
|
||||
}
|
||||
default -> newState;
|
||||
};
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
}
|
145
src/main/java/it/tdlight/reactiveapi/Stats.java
Normal file
145
src/main/java/it/tdlight/reactiveapi/Stats.java
Normal file
@ -0,0 +1,145 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class Stats extends Thread {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(Stats.class);
|
||||
public static final List<ReactiveApiPublisher> STATS = new CopyOnWriteArrayList<>();
|
||||
public static final long SLEEP_INTERVAL = Duration.ofSeconds(10).toMillis();
|
||||
|
||||
static {
|
||||
var stats = new Stats();
|
||||
stats.setName("Stats");
|
||||
stats.setDaemon(true);
|
||||
stats.start();
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
var prev = System.currentTimeMillis();
|
||||
var prevClients = 0;
|
||||
var prevReceivedUpdates = new LongArrayList();
|
||||
var prevBufferedUpdates = new LongArrayList();
|
||||
var prevProcessedUpdates = new LongArrayList();
|
||||
var prevClientBoundEvents = new LongArrayList();
|
||||
var prevSentClientBoundEvents = new LongArrayList();
|
||||
while (!Thread.interrupted()) {
|
||||
//noinspection BusyWait
|
||||
Thread.sleep(SLEEP_INTERVAL);
|
||||
var now = System.currentTimeMillis();
|
||||
var timeDiffSeconds = (now - prev) / 1000d;
|
||||
StringBuilder out = new StringBuilder();
|
||||
out.append("Statistics. Time delta: %03.2fs%n".formatted(timeDiffSeconds));
|
||||
var currentClients = STATS.size();
|
||||
var clientIds = new LongArrayList();
|
||||
var receivedUpdates = new LongArrayList();
|
||||
var bufferedUpdates = new LongArrayList();
|
||||
var processedUpdates = new LongArrayList();
|
||||
var clientBoundEvents = new LongArrayList();
|
||||
var sentClientBoundEvents = new LongArrayList();
|
||||
for (ReactiveApiPublisher stat : STATS) {
|
||||
clientIds.add(stat.userId);
|
||||
receivedUpdates.add(stat.receivedUpdates.longValue());
|
||||
bufferedUpdates.add(stat.bufferedUpdates.longValue());
|
||||
processedUpdates.add(stat.processedUpdates.longValue());
|
||||
clientBoundEvents.add(stat.clientBoundEvents.longValue());
|
||||
sentClientBoundEvents.add(stat.sentClientBoundEvents.longValue());
|
||||
}
|
||||
while (currentClients > prevClients) {
|
||||
prevClients++;
|
||||
prevReceivedUpdates.add(0);
|
||||
prevBufferedUpdates.add(0);
|
||||
prevProcessedUpdates.add(0);
|
||||
prevClientBoundEvents.add(0);
|
||||
prevSentClientBoundEvents.add(0);
|
||||
}
|
||||
double receivedUpdatesRateSum = 0;
|
||||
long ramBufferedSum = 0;
|
||||
long diskBufferedSum = 0;
|
||||
double bufferedUpdatesRateSum = 0;
|
||||
double processedUpdatesRateSum = 0;
|
||||
double clientBoundEventsRateSum = 0;
|
||||
double sentClientBoundEventsRateSum = 0;
|
||||
for (int i = 0; i <= currentClients; i++) {
|
||||
double receivedUpdatesRate;
|
||||
long ramBuffered;
|
||||
long diskBuffered;
|
||||
double bufferedUpdatesRate;
|
||||
double processedUpdatesRate;
|
||||
double clientBoundEventsRate;
|
||||
double sentClientBoundEventsRate;
|
||||
if (i != currentClients) {
|
||||
receivedUpdatesRate = (receivedUpdates.getLong(i) - prevReceivedUpdates.getLong(i)) / timeDiffSeconds;
|
||||
diskBuffered = bufferedUpdates.getLong(i) - processedUpdates.getLong(i);
|
||||
ramBuffered = receivedUpdates.getLong(i) - bufferedUpdates.getLong(i);
|
||||
bufferedUpdatesRate = (bufferedUpdates.getLong(i) - prevBufferedUpdates.getLong(i)) / timeDiffSeconds;
|
||||
processedUpdatesRate = (processedUpdates.getLong(i) - prevProcessedUpdates.getLong(i)) / timeDiffSeconds;
|
||||
clientBoundEventsRate = (clientBoundEvents.getLong(i) - prevClientBoundEvents.getLong(i)) / timeDiffSeconds;
|
||||
sentClientBoundEventsRate =
|
||||
(sentClientBoundEvents.getLong(i) - prevSentClientBoundEvents.getLong(i)) / timeDiffSeconds;
|
||||
receivedUpdatesRateSum += receivedUpdatesRate;
|
||||
diskBufferedSum += diskBuffered;
|
||||
ramBufferedSum += ramBuffered;
|
||||
bufferedUpdatesRateSum += bufferedUpdatesRate;
|
||||
processedUpdatesRateSum += processedUpdatesRate;
|
||||
clientBoundEventsRateSum += clientBoundEventsRate;
|
||||
sentClientBoundEventsRateSum += sentClientBoundEventsRate;
|
||||
if (LOG.isTraceEnabled()) {
|
||||
out.append(String.format("%d:\t", clientIds.getLong(i)));
|
||||
}
|
||||
} else {
|
||||
receivedUpdatesRate = receivedUpdatesRateSum;
|
||||
diskBuffered = diskBufferedSum;
|
||||
ramBuffered = ramBufferedSum;
|
||||
bufferedUpdatesRate = bufferedUpdatesRateSum;
|
||||
processedUpdatesRate = processedUpdatesRateSum;
|
||||
clientBoundEventsRate = clientBoundEventsRateSum;
|
||||
sentClientBoundEventsRate = sentClientBoundEventsRateSum;
|
||||
out.append("Total:\t");
|
||||
}
|
||||
if (i == currentClients || LOG.isTraceEnabled()) {
|
||||
out.append(String.format(
|
||||
"\tUpdates:\t[received %03.2fHz\tbuffered: %03.2fHz (RAM: %d HDD: %d)\tprocessed: %03.2fHz]\tClient bound events: %03.2fHz\tProcessed events: %03.2fHz\t%n",
|
||||
receivedUpdatesRate,
|
||||
bufferedUpdatesRate,
|
||||
ramBuffered,
|
||||
diskBuffered,
|
||||
processedUpdatesRate,
|
||||
clientBoundEventsRate,
|
||||
sentClientBoundEventsRate
|
||||
));
|
||||
}
|
||||
}
|
||||
out.append(String.format("%n"));
|
||||
|
||||
for (int i = 0; i < currentClients; i++) {
|
||||
prevReceivedUpdates = receivedUpdates;
|
||||
prevBufferedUpdates = bufferedUpdates;
|
||||
prevProcessedUpdates = processedUpdates;
|
||||
prevClientBoundEvents = clientBoundEvents;
|
||||
prevSentClientBoundEvents = sentClientBoundEvents;
|
||||
}
|
||||
LOG.debug(out.toString());
|
||||
|
||||
prev = now;
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package it.tdlight.tdlibsession.td;
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Error;
|
||||
|
||||
public class TdError extends RuntimeException {
|
||||
public class TdError extends Exception {
|
||||
|
||||
private final int code;
|
||||
private final String message;
|
@ -0,0 +1,16 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
import java.io.DataInput;
|
||||
import java.io.IOException;
|
||||
|
||||
public class TdlibBoundResultingEventDeserializer implements Deserializer<TDLibBoundResultingEvent<?>> {
|
||||
|
||||
@Override
|
||||
public TDLibBoundResultingEvent<?> deserialize(int length, DataInput dataInput) throws IOException {
|
||||
Function<?> action = (Function<?>) TdApi.Deserializer.deserialize(dataInput);
|
||||
return new TDLibBoundResultingEvent<>(action, dataInput.readBoolean());
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
|
||||
public class TdlibBoundResultingEventSerializer implements Serializer<TDLibBoundResultingEvent<?>> {
|
||||
|
||||
@Override
|
||||
public void serialize(TDLibBoundResultingEvent<?> data, DataOutput output) throws IOException {
|
||||
data.action().serialize(output);
|
||||
output.writeBoolean(data.ignoreFailure());
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse;
|
||||
import java.io.Closeable;
|
||||
import java.util.Map;
|
||||
|
||||
public record TdlibChannelsClients(EventProducer<OnRequest<?>> request,
|
||||
EventConsumer<OnResponse<Object>> response,
|
||||
Map<String, EventConsumer<ClientBoundEvent>> events) implements Closeable {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
request.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import java.io.Closeable;
|
||||
import java.util.Map;
|
||||
|
||||
public record TdlibChannelsServers(EventConsumer<Event.OnRequest<it.tdlight.jni.TdApi.Object>> request,
|
||||
EventProducer<Event.OnResponse<it.tdlight.jni.TdApi.Object>> response,
|
||||
Map<String, EventProducer<ClientBoundEvent>> events) implements Closeable {
|
||||
|
||||
public EventProducer<ClientBoundEvent> events(String lane) {
|
||||
var p = events.get(lane);
|
||||
if (p == null) {
|
||||
throw new IllegalArgumentException("No lane " + lane);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
response.close();
|
||||
events.values().forEach(EventProducer::close);
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse;
|
||||
import java.io.Closeable;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.SignalType;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.EmitFailureHandler;
|
||||
import reactor.core.publisher.Sinks.Empty;
|
||||
import reactor.core.publisher.Sinks.Many;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.concurrent.Queues;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
public class TdlibChannelsSharedHost implements Closeable {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(TdlibChannelsSharedHost.class);
|
||||
public static final RetryBackoffSpec RETRY_STRATEGY = Retry
|
||||
.backoff(Long.MAX_VALUE, Duration.ofSeconds(1))
|
||||
.maxBackoff(Duration.ofSeconds(16))
|
||||
.jitter(1.0)
|
||||
.doBeforeRetry(signal -> LogManager.getLogger("Channels").warn("Retrying channel with signal {}", signal));
|
||||
|
||||
public static final Function<Flux<Long>, Flux<Long>> REPEAT_STRATEGY = n -> n
|
||||
.doOnNext(i -> LogManager.getLogger("Channels").debug("Resubscribing to channel"))
|
||||
.delayElements(Duration.ofSeconds(5));
|
||||
|
||||
private final TdlibChannelsServers tdServersChannels;
|
||||
private final Disposable responsesSub;
|
||||
private final AtomicReference<Disposable> requestsSub = new AtomicReference<>();
|
||||
private final Many<OnResponse<TdApi.Object>> responses = Sinks.many().multicast().onBackpressureBuffer(65_535);
|
||||
private final Map<String, Many<Flux<ClientBoundEvent>>> events;
|
||||
private final Flux<Timestamped<OnRequest<Object>>> requests;
|
||||
|
||||
public TdlibChannelsSharedHost(Set<String> allLanes, TdlibChannelsServers tdServersChannels) {
|
||||
this.tdServersChannels = tdServersChannels;
|
||||
this.responsesSub = tdServersChannels.response().sendMessages(responses.asFlux()/*.log("responses", Level.FINE)*/)
|
||||
.repeatWhen(REPEAT_STRATEGY)
|
||||
.retryWhen(RETRY_STRATEGY)
|
||||
.subscribeOn(Schedulers.parallel())
|
||||
.subscribe(n -> {}, ex -> LOG.error("Unexpected error when sending responses", ex));
|
||||
events = allLanes.stream().collect(Collectors.toUnmodifiableMap(Function.identity(), lane -> {
|
||||
Many<Flux<ClientBoundEvent>> sink = Sinks.many().replay().all();
|
||||
Flux<ClientBoundEvent> outputEventsFlux = Flux
|
||||
.merge(sink.asFlux().map(flux -> flux.publishOn(Schedulers.parallel())), Integer.MAX_VALUE, 256)
|
||||
.doFinally(s -> LOG.debug("Output events flux of lane \"{}\" terminated with signal {}", lane, s));
|
||||
tdServersChannels.events(lane).sendMessages(outputEventsFlux)
|
||||
.repeatWhen(REPEAT_STRATEGY)
|
||||
.retryWhen(RETRY_STRATEGY)
|
||||
.subscribeOn(Schedulers.parallel())
|
||||
.subscribe(n -> {}, ex -> LOG.error("Unexpected error when sending events to lane {}", lane, ex));
|
||||
return sink;
|
||||
}));
|
||||
this.requests = tdServersChannels.request().consumeMessages()
|
||||
.doFinally(s -> LOG.debug("Input requests consumer terminated with signal {}", s))
|
||||
.repeatWhen(REPEAT_STRATEGY)
|
||||
.retryWhen(RETRY_STRATEGY)
|
||||
.doOnError(ex -> LOG.error("Unexpected error when receiving requests", ex))
|
||||
.doFinally(s -> LOG.debug("Input requests flux terminated with signal {}", s));
|
||||
}
|
||||
|
||||
public Flux<Timestamped<OnRequest<Object>>> requests() {
|
||||
return requests
|
||||
//.onBackpressureBuffer(8192, BufferOverflowStrategy.DROP_OLDEST)
|
||||
.log("requests", Level.FINEST, SignalType.REQUEST, SignalType.ON_NEXT);
|
||||
}
|
||||
|
||||
public Disposable events(String lane, Flux<ClientBoundEvent> eventFlux) {
|
||||
Empty<Void> canceller = Sinks.empty();
|
||||
var eventsSink = events.get(lane);
|
||||
if (eventsSink == null) {
|
||||
throw new IllegalArgumentException("Lane " + lane + " does not exist");
|
||||
}
|
||||
synchronized (events) {
|
||||
eventsSink.emitNext(eventFlux.takeUntilOther(canceller.asMono()), EmitFailureHandler.FAIL_FAST);
|
||||
}
|
||||
return () -> canceller.tryEmitEmpty();
|
||||
}
|
||||
|
||||
public Many<OnResponse<TdApi.Object>> responses() {
|
||||
return responses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
responsesSub.dispose();
|
||||
var requestsSub = this.requestsSub.get();
|
||||
if (requestsSub != null) {
|
||||
requestsSub.dispose();
|
||||
}
|
||||
tdServersChannels.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static it.tdlight.reactiveapi.TdlibChannelsSharedHost.REPEAT_STRATEGY;
|
||||
import static it.tdlight.reactiveapi.TdlibChannelsSharedHost.RETRY_STRATEGY;
|
||||
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.reactiveapi.Event.ClientBoundEvent;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse;
|
||||
import java.io.Closeable;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.EmitFailureHandler;
|
||||
import reactor.core.publisher.Sinks.Many;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.concurrent.Queues;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
public class TdlibChannelsSharedReceive implements Closeable {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(TdlibChannelsSharedReceive.class);
|
||||
|
||||
private final TdlibChannelsClients tdClientsChannels;
|
||||
private final AtomicReference<Disposable> responsesSub = new AtomicReference<>();
|
||||
private final Disposable requestsSub;
|
||||
private final AtomicReference<Disposable> eventsSub = new AtomicReference<>();
|
||||
private final Flux<Timestamped<OnResponse<Object>>> responses;
|
||||
private final Map<String, Flux<Timestamped<ClientBoundEvent>>> events;
|
||||
private final Many<OnRequest<?>> requests = Sinks.many().multicast().directAllOrNothing();
|
||||
|
||||
public TdlibChannelsSharedReceive(TdlibChannelsClients tdClientsChannels) {
|
||||
this.tdClientsChannels = tdClientsChannels;
|
||||
this.responses = Flux
|
||||
.defer(() -> tdClientsChannels.response().consumeMessages())
|
||||
//.log("responses", Level.FINE)
|
||||
.repeatWhen(REPEAT_STRATEGY)
|
||||
.retryWhen(RETRY_STRATEGY)
|
||||
.doFinally(s -> LOG.debug("Input responses flux terminated with signal {}", s));
|
||||
this.events = tdClientsChannels.events().entrySet().stream()
|
||||
.collect(Collectors.toUnmodifiableMap(Entry::getKey,
|
||||
e -> Flux
|
||||
.defer(() -> e.getValue().consumeMessages())
|
||||
.repeatWhen(REPEAT_STRATEGY)
|
||||
.retryWhen(RETRY_STRATEGY)
|
||||
.doFinally(s -> LOG.debug("Input events flux of lane \"{}\" terminated with signal {}", e.getKey(), s))
|
||||
));
|
||||
this.requestsSub = tdClientsChannels
|
||||
.request()
|
||||
.sendMessages(Flux.defer(() -> requests.asFlux().doFinally(s -> LOG.debug("Output requests flux terminated with signal {}", s))))
|
||||
.doFinally(s -> LOG.debug("Output requests sender terminated with signal {}", s))
|
||||
.repeatWhen(REPEAT_STRATEGY)
|
||||
.retryWhen(RETRY_STRATEGY)
|
||||
.subscribeOn(Schedulers.parallel())
|
||||
.subscribe(n -> {}, ex -> {
|
||||
LOG.error("An error when handling requests killed the requests subscriber!", ex);
|
||||
synchronized (requests) {
|
||||
requests.emitError(ex, EmitFailureHandler.FAIL_FAST);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Flux<Timestamped<OnResponse<Object>>> responses() {
|
||||
return responses;
|
||||
}
|
||||
|
||||
public Flux<Timestamped<ClientBoundEvent>> events(String lane) {
|
||||
var result = events.get(lane);
|
||||
if (result == null) {
|
||||
throw new IllegalArgumentException("No lane " + lane);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, Flux<Timestamped<ClientBoundEvent>>> events() {
|
||||
return events;
|
||||
}
|
||||
|
||||
public void emitRequest(OnRequest<?> request) {
|
||||
synchronized (requests) {
|
||||
requests.emitNext(request, EmitFailureHandler.FAIL_FAST);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
requestsSub.dispose();
|
||||
var responsesSub = this.responsesSub.get();
|
||||
if (responsesSub != null) {
|
||||
responsesSub.dispose();
|
||||
}
|
||||
var eventsSub = this.eventsSub.get();
|
||||
if (eventsSub != null) {
|
||||
eventsSub.dispose();
|
||||
}
|
||||
tdClientsChannels.close();
|
||||
}
|
||||
}
|
35
src/main/java/it/tdlight/reactiveapi/TdlibDeserializer.java
Normal file
35
src/main/java/it/tdlight/reactiveapi/TdlibDeserializer.java
Normal file
@ -0,0 +1,35 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static it.tdlight.reactiveapi.Event.SERIAL_VERSION;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInput;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class TdlibDeserializer implements Deserializer<Object> {
|
||||
|
||||
@Override
|
||||
public Object deserialize(byte[] data) {
|
||||
if (data.length == 0) {
|
||||
return null;
|
||||
}
|
||||
var bais = new ByteArrayInputStream(data);
|
||||
var dais = new DataInputStream(bais);
|
||||
return deserialize(-1, dais);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object deserialize(int length, DataInput dataInput) {
|
||||
try {
|
||||
if (dataInput.readInt() != SERIAL_VERSION) {
|
||||
return new TdApi.Error(400, "Conflicting protocol version");
|
||||
}
|
||||
return TdApi.Deserializer.deserialize(dataInput);
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException("Failed to deserialize TDLib object", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static it.tdlight.reactiveapi.Event.SERIAL_VERSION;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest.InvalidRequest;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest.Request;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInput;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
public class TdlibRequestDeserializer<T extends TdApi.Object> implements Deserializer<OnRequest<T>> {
|
||||
|
||||
@Override
|
||||
public OnRequest<T> deserialize(byte[] data) {
|
||||
if (data.length == 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
var bais = new ByteArrayInputStream(data);
|
||||
var dais = new DataInputStream(bais);
|
||||
return deserialize(-1, dais);
|
||||
} catch (UnsupportedOperationException | IOException e) {
|
||||
throw new SerializationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OnRequest<T> deserialize(int length, DataInput dataInput) throws IOException {
|
||||
var userId = dataInput.readLong();
|
||||
var clientId = dataInput.readLong();
|
||||
var requestId = dataInput.readLong();
|
||||
if (dataInput.readInt() != SERIAL_VERSION) {
|
||||
// Deprecated request
|
||||
return new InvalidRequest<>(userId, clientId, requestId);
|
||||
} else {
|
||||
long millis = dataInput.readLong();
|
||||
Instant timeout;
|
||||
if (millis == -1) {
|
||||
timeout = Instant.ofEpochMilli(Long.MAX_VALUE);
|
||||
} else {
|
||||
timeout = Instant.ofEpochMilli(millis);
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
TdApi.Function<T> request = (TdApi.Function<T>) TdApi.Deserializer.deserialize(dataInput);
|
||||
return new Request<>(userId, clientId, requestId, request, timeout);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static it.tdlight.reactiveapi.Event.SERIAL_VERSION;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest;
|
||||
import it.tdlight.reactiveapi.Event.OnRequest.Request;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutput;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
public class TdlibRequestSerializer<T extends TdApi.Object> implements Serializer<OnRequest<T>> {
|
||||
|
||||
private static final Instant INFINITE_TIMEOUT = Instant.now().plus(100_000, ChronoUnit.DAYS);
|
||||
|
||||
@Override
|
||||
public byte[] serialize(OnRequest<T> data) {
|
||||
try {
|
||||
if (data == null) {
|
||||
return new byte[0];
|
||||
} else {
|
||||
try(var baos = new ByteArrayOutputStream()) {
|
||||
try (var daos = new DataOutputStream(baos)) {
|
||||
serialize(data, daos);
|
||||
daos.flush();
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException("Failed to serialize TDLib object", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(OnRequest<T> data, DataOutput dataOutput) throws IOException {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
dataOutput.writeLong(data.userId());
|
||||
dataOutput.writeLong(data.clientId());
|
||||
dataOutput.writeLong(data.requestId());
|
||||
dataOutput.writeInt(SERIAL_VERSION);
|
||||
if (data instanceof OnRequest.Request<?> request) {
|
||||
if (request.timeout() == Instant.MAX || request.timeout().compareTo(INFINITE_TIMEOUT) >= 0) {
|
||||
dataOutput.writeLong(-1);
|
||||
} else {
|
||||
dataOutput.writeLong(request.timeout().toEpochMilli());
|
||||
}
|
||||
request.request().serialize(dataOutput);
|
||||
} else if (data instanceof OnRequest.InvalidRequest<?>) {
|
||||
dataOutput.writeLong(-2);
|
||||
} else {
|
||||
throw new SerializationException("Unknown request type: " + dataOutput.getClass());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static it.tdlight.reactiveapi.Event.SERIAL_VERSION;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse.InvalidResponse;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse.Response;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInput;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
|
||||
public class TdlibResponseDeserializer<T extends TdApi.Object> implements Deserializer<OnResponse<T>> {
|
||||
|
||||
@Override
|
||||
public OnResponse<T> deserialize(byte[] data) {
|
||||
if (data.length == 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
var bais = new ByteArrayInputStream(data);
|
||||
var dais = new DataInputStream(bais);
|
||||
return deserialize(-1, dais);
|
||||
} catch (UnsupportedOperationException | IOException e) {
|
||||
throw new SerializationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OnResponse<T> deserialize(int length, DataInput dataInput) throws IOException {
|
||||
var clientId = dataInput.readLong();
|
||||
var requestId = dataInput.readLong();
|
||||
var userId = dataInput.readLong();
|
||||
if (dataInput.readInt() != SERIAL_VERSION) {
|
||||
// Deprecated response
|
||||
return new InvalidResponse<>(clientId, requestId, userId);
|
||||
} else {
|
||||
@SuppressWarnings("unchecked")
|
||||
T response = (T) TdApi.Deserializer.deserialize(dataInput);
|
||||
return new Response<>(clientId, requestId, userId, response);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static it.tdlight.reactiveapi.Event.SERIAL_VERSION;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse;
|
||||
import it.tdlight.reactiveapi.Event.OnResponse.Response;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutput;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
public class TdlibResponseSerializer<T extends TdApi.Object> implements Serializer<OnResponse<T>> {
|
||||
|
||||
@Override
|
||||
public byte[] serialize(OnResponse<T> data) {
|
||||
try {
|
||||
if (data == null) {
|
||||
return new byte[0];
|
||||
} else {
|
||||
try(var baos = new ByteArrayOutputStream()) {
|
||||
try (var daos = new DataOutputStream(baos)) {
|
||||
serialize(data, daos);
|
||||
daos.flush();
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException("Failed to serialize TDLib object", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(OnResponse<T> data, DataOutput dataOutput) throws IOException {
|
||||
dataOutput.writeLong(data.clientId());
|
||||
dataOutput.writeLong(data.requestId());
|
||||
dataOutput.writeLong(data.userId());
|
||||
dataOutput.writeInt(SERIAL_VERSION);
|
||||
if (data instanceof Response<?> response) {
|
||||
response.response().serialize(dataOutput);
|
||||
} else if (data instanceof OnResponse.InvalidResponse<T>) {
|
||||
dataOutput.writeLong(-2);
|
||||
} else {
|
||||
throw new SerializationException("Unknown response type: " + dataOutput.getClass());
|
||||
}
|
||||
}
|
||||
}
|
43
src/main/java/it/tdlight/reactiveapi/TdlibSerializer.java
Normal file
43
src/main/java/it/tdlight/reactiveapi/TdlibSerializer.java
Normal file
@ -0,0 +1,43 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static it.tdlight.reactiveapi.Event.SERIAL_VERSION;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutput;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class TdlibSerializer implements Serializer<TdApi.Object> {
|
||||
|
||||
@Override
|
||||
public byte[] serialize(TdApi.Object data) {
|
||||
try {
|
||||
if (data == null) {
|
||||
return new byte[0];
|
||||
} else {
|
||||
try(var baos = new ByteArrayOutputStream()) {
|
||||
try (var daos = new DataOutputStream(baos)) {
|
||||
serialize(data, daos);
|
||||
daos.flush();
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException("Failed to serialize TDLib object", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Object data, DataOutput output) throws IOException {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
output.writeInt(SERIAL_VERSION);
|
||||
data.serialize(output);
|
||||
}
|
||||
}
|
3
src/main/java/it/tdlight/reactiveapi/Timestamped.java
Normal file
3
src/main/java/it/tdlight/reactiveapi/Timestamped.java
Normal file
@ -0,0 +1,3 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
public record Timestamped<T>(long timestamp, T data) {}
|
20
src/main/java/it/tdlight/reactiveapi/TransportFactory.java
Normal file
20
src/main/java/it/tdlight/reactiveapi/TransportFactory.java
Normal file
@ -0,0 +1,20 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import io.rsocket.transport.ClientTransport;
|
||||
import io.rsocket.transport.ServerTransport;
|
||||
|
||||
public interface TransportFactory {
|
||||
|
||||
ClientTransport getClientTransport(int index);
|
||||
|
||||
ServerTransport<?> getServerTransport(int index);
|
||||
|
||||
static TransportFactory tcp(HostAndPort baseHost) {
|
||||
return new TransportFactoryTcp(baseHost);
|
||||
}
|
||||
|
||||
static TransportFactory local(String prefix) {
|
||||
return new TransportFactoryLocal(prefix);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import io.rsocket.transport.ClientTransport;
|
||||
import io.rsocket.transport.ServerTransport;
|
||||
import io.rsocket.transport.local.LocalClientTransport;
|
||||
import io.rsocket.transport.local.LocalServerTransport;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
class TransportFactoryLocal implements TransportFactory {
|
||||
|
||||
private final String prefix;
|
||||
|
||||
TransportFactoryLocal(String prefix) {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientTransport getClientTransport(int index) {
|
||||
return LocalClientTransport.create(getLabel(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerTransport<?> getServerTransport(int index) {
|
||||
return LocalServerTransport.create(getLabel(index));
|
||||
}
|
||||
|
||||
private String getLabel(int index) {
|
||||
return prefix + "-" + index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
TransportFactoryLocal that = (TransportFactoryLocal) o;
|
||||
return Objects.equals(prefix, that.prefix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(prefix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", TransportFactoryLocal.class.getSimpleName() + "[", "]")
|
||||
.add("prefix='" + prefix + "'")
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import io.rsocket.transport.ClientTransport;
|
||||
import io.rsocket.transport.ServerTransport;
|
||||
import io.rsocket.transport.netty.client.TcpClientTransport;
|
||||
import io.rsocket.transport.netty.server.TcpServerTransport;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
class TransportFactoryTcp implements TransportFactory {
|
||||
|
||||
private final HostAndPort baseHost;
|
||||
|
||||
TransportFactoryTcp(HostAndPort baseHost) {
|
||||
this.baseHost = baseHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientTransport getClientTransport(int index) {
|
||||
return TcpClientTransport.create(baseHost.getHost(), getPort(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerTransport<?> getServerTransport(int index) {
|
||||
return TcpServerTransport.create(baseHost.getHost(), getPort(index));
|
||||
}
|
||||
|
||||
private int getPort(int index) {
|
||||
return baseHost.getPort() + index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
TransportFactoryTcp that = (TransportFactoryTcp) o;
|
||||
return Objects.equals(baseHost, that.baseHost);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(baseHost);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", TransportFactoryTcp.class.getSimpleName() + "[", "]")
|
||||
.add("baseHost=" + baseHost)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
public record UserIdAndLiveId(long userId, long liveId) {}
|
20
src/main/java/it/tdlight/reactiveapi/UtfCodec.java
Normal file
20
src/main/java/it/tdlight/reactiveapi/UtfCodec.java
Normal file
@ -0,0 +1,20 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
public class UtfCodec implements Serializer<String>, Deserializer<String> {
|
||||
|
||||
@Override
|
||||
public String deserialize(byte[] data) {
|
||||
return new String(data, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(String data) {
|
||||
return data.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
147
src/main/java/it/tdlight/reactiveapi/kafka/KafkaConsumer.java
Normal file
147
src/main/java/it/tdlight/reactiveapi/kafka/KafkaConsumer.java
Normal file
@ -0,0 +1,147 @@
|
||||
package it.tdlight.reactiveapi.kafka;
|
||||
|
||||
import static java.lang.Math.toIntExact;
|
||||
|
||||
import it.tdlight.common.Init;
|
||||
import it.tdlight.common.utils.CantLoadLibrary;
|
||||
import it.tdlight.reactiveapi.EventConsumer;
|
||||
import it.tdlight.reactiveapi.ChannelCodec;
|
||||
import it.tdlight.reactiveapi.KafkaParameters;
|
||||
import it.tdlight.reactiveapi.ReactorUtils;
|
||||
import it.tdlight.reactiveapi.Timestamped;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
import org.apache.kafka.clients.consumer.CommitFailedException;
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||
import org.apache.kafka.common.errors.RebalanceInProgressException;
|
||||
import org.apache.kafka.common.record.TimestampType;
|
||||
import org.apache.kafka.common.serialization.IntegerDeserializer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.SignalType;
|
||||
import reactor.kafka.receiver.KafkaReceiver;
|
||||
import reactor.kafka.receiver.ReceiverOptions;
|
||||
import reactor.util.retry.Retry;
|
||||
|
||||
public final class KafkaConsumer<K> implements EventConsumer<K> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(KafkaConsumer.class);
|
||||
|
||||
private final KafkaParameters kafkaParameters;
|
||||
private final boolean quickResponse;
|
||||
private final ChannelCodec channelCodec;
|
||||
private final String channelName;
|
||||
|
||||
public KafkaConsumer(KafkaParameters kafkaParameters,
|
||||
boolean quickResponse,
|
||||
ChannelCodec channelCodec,
|
||||
String channelName) {
|
||||
this.kafkaParameters = kafkaParameters;
|
||||
this.quickResponse = quickResponse;
|
||||
this.channelCodec = channelCodec;
|
||||
this.channelName = channelName;
|
||||
}
|
||||
|
||||
public KafkaReceiver<Integer, K> createReceiver(@NotNull String kafkaGroupId) {
|
||||
try {
|
||||
Init.start();
|
||||
} catch (CantLoadLibrary e) {
|
||||
LOG.error("Can't load TDLight library", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
Map<String, Object> props = new HashMap<>();
|
||||
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaParameters.getBootstrapServersString());
|
||||
props.put(ConsumerConfig.CLIENT_ID_CONFIG, kafkaParameters.clientId());
|
||||
props.put(ConsumerConfig.GROUP_ID_CONFIG, kafkaGroupId);
|
||||
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
|
||||
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, KafkaDeserializer.class);
|
||||
props.put("custom.deserializer.class", getChannelCodec().getDeserializerClass());
|
||||
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
|
||||
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, toIntExact(Duration.ofMinutes(5).toMillis()));
|
||||
if (!isQuickResponse()) {
|
||||
props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, "10000");
|
||||
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000");
|
||||
props.put(ConsumerConfig.FETCH_MIN_BYTES_CONFIG, "1048576");
|
||||
props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, "100");
|
||||
}
|
||||
ReceiverOptions<Integer, K> receiverOptions = ReceiverOptions.<Integer, K>create(props)
|
||||
.commitInterval(Duration.ofSeconds(10))
|
||||
.commitBatchSize(isQuickResponse() ? 64 : 1024)
|
||||
.maxCommitAttempts(5)
|
||||
.maxDeferredCommits((isQuickResponse() ? 64 : 1024) * 5);
|
||||
ReceiverOptions<Integer, K> options = receiverOptions
|
||||
.subscription(List.of("tdlib." + getChannelName()))
|
||||
.addAssignListener(partitions -> LOG.debug("onPartitionsAssigned {}", partitions))
|
||||
.addRevokeListener(partitions -> LOG.debug("onPartitionsRevoked {}", partitions));
|
||||
return KafkaReceiver.create(options);
|
||||
}
|
||||
|
||||
public boolean isQuickResponse() {
|
||||
return quickResponse;
|
||||
}
|
||||
|
||||
public ChannelCodec getChannelCodec() {
|
||||
return channelCodec;
|
||||
}
|
||||
|
||||
public String getChannelName() {
|
||||
return channelName;
|
||||
}
|
||||
|
||||
public Flux<Timestamped<K>> retryIfCleanup(Flux<Timestamped<K>> eventFlux) {
|
||||
return eventFlux.retryWhen(Retry
|
||||
.backoff(Long.MAX_VALUE, Duration.ofMillis(100))
|
||||
.maxBackoff(Duration.ofSeconds(5))
|
||||
.transientErrors(true)
|
||||
.filter(ex -> ex instanceof RebalanceInProgressException)
|
||||
.doBeforeRetry(s -> LOG.warn("Rebalancing in progress")));
|
||||
}
|
||||
|
||||
public Flux<Timestamped<K>> retryIfCommitFailed(Flux<Timestamped<K>> eventFlux) {
|
||||
return eventFlux.retryWhen(Retry
|
||||
.backoff(10, Duration.ofSeconds(1))
|
||||
.maxBackoff(Duration.ofSeconds(5))
|
||||
.transientErrors(true)
|
||||
.filter(ex -> ex instanceof CommitFailedException)
|
||||
.doBeforeRetry(s -> LOG.warn("Commit cannot be completed since the group has already rebalanced"
|
||||
+ " and assigned the partitions to another member. This means that the time between subsequent"
|
||||
+ " calls to poll() was longer than the configured max.poll.interval.ms, which typically implies"
|
||||
+ " that the poll loop is spending too much time message processing. You can address this either"
|
||||
+ " by increasing max.poll.interval.ms or by reducing the maximum size of batches returned in poll()"
|
||||
+ " with max.poll.records.")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Timestamped<K>> consumeMessages() {
|
||||
return consumeMessagesInternal();
|
||||
}
|
||||
|
||||
private Flux<Timestamped<K>> consumeMessagesInternal() {
|
||||
return createReceiver(kafkaParameters.groupId() + "-" + channelName)
|
||||
.receiveAutoAck(isQuickResponse() ? 1 : 4)
|
||||
.concatMap(Function.identity())
|
||||
.log("consume-messages",
|
||||
Level.FINEST,
|
||||
SignalType.REQUEST,
|
||||
SignalType.ON_NEXT,
|
||||
SignalType.ON_ERROR,
|
||||
SignalType.ON_COMPLETE
|
||||
)
|
||||
.map(record -> {
|
||||
if (record.timestampType() == TimestampType.CREATE_TIME) {
|
||||
return new Timestamped<>(record.timestamp(), record.value());
|
||||
} else {
|
||||
return new Timestamped<>(1, record.value());
|
||||
}
|
||||
})
|
||||
.transform(this::retryIfCleanup)
|
||||
.transform(this::retryIfCommitFailed)
|
||||
.transform(ReactorUtils::subscribeOnceUntilUnsubscribe);
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package it.tdlight.reactiveapi.kafka;
|
||||
|
||||
import it.tdlight.reactiveapi.Deserializer;
|
||||
import it.tdlight.reactiveapi.SerializationException;
|
||||
import it.tdlight.reactiveapi.Serializer;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Map;
|
||||
|
||||
public class KafkaDeserializer<T> implements Deserializer<T>, org.apache.kafka.common.serialization.Deserializer<T> {
|
||||
|
||||
private Deserializer<T> deserializer;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void configure(Map<String, ?> configs, boolean isKey) {
|
||||
var clazz = (Class<?>) configs.get("custom.deserializer.class");
|
||||
try {
|
||||
this.deserializer = (Deserializer<T>) clazz.getConstructor().newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(String topic, byte[] data) {
|
||||
try {
|
||||
return deserializer.deserialize(data);
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(byte[] data) throws IOException {
|
||||
return deserializer.deserialize(data);
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package it.tdlight.reactiveapi.kafka;
|
||||
|
||||
import it.tdlight.reactiveapi.ChannelCodec;
|
||||
import it.tdlight.reactiveapi.EventProducer;
|
||||
import it.tdlight.reactiveapi.KafkaParameters;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import org.apache.kafka.clients.producer.ProducerConfig;
|
||||
import org.apache.kafka.clients.producer.ProducerRecord;
|
||||
import org.apache.kafka.common.serialization.IntegerSerializer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.SignalType;
|
||||
import reactor.kafka.sender.KafkaSender;
|
||||
import reactor.kafka.sender.SenderOptions;
|
||||
import reactor.kafka.sender.SenderRecord;
|
||||
|
||||
public final class KafkaProducer<K> implements EventProducer<K> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(KafkaProducer.class);
|
||||
|
||||
private final KafkaSender<Integer, K> sender;
|
||||
private final ChannelCodec channelCodec;
|
||||
private final String channelName;
|
||||
|
||||
public KafkaProducer(KafkaParameters kafkaParameters, ChannelCodec channelCodec, String channelName) {
|
||||
this.channelCodec = channelCodec;
|
||||
this.channelName = channelName;
|
||||
Map<String, Object> props = new HashMap<>();
|
||||
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaParameters.getBootstrapServersString());
|
||||
props.put(ProducerConfig.CLIENT_ID_CONFIG, kafkaParameters.clientId());
|
||||
props.put(ProducerConfig.ACKS_CONFIG, "1");
|
||||
props.put(ProducerConfig.BATCH_SIZE_CONFIG, Integer.toString(32 * 1024));
|
||||
props.put(ProducerConfig.LINGER_MS_CONFIG, "20");
|
||||
props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "snappy");
|
||||
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
|
||||
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, KafkaSerializer.class);
|
||||
props.put("custom.serializer.class", getChannelCodec().getSerializerClass());
|
||||
SenderOptions<Integer, K> senderOptions = SenderOptions.create(props);
|
||||
|
||||
sender = KafkaSender.create(senderOptions.maxInFlight(1024));
|
||||
}
|
||||
|
||||
public ChannelCodec getChannelCodec() {
|
||||
return channelCodec;
|
||||
}
|
||||
|
||||
public String getChannelName() {
|
||||
return channelName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> sendMessages(Flux<K> eventsFlux) {
|
||||
var channelName = getChannelName();
|
||||
return eventsFlux
|
||||
.<SenderRecord<Integer, K, Integer>>map(event ->
|
||||
SenderRecord.create(new ProducerRecord<>("tdlib." + channelName, event), null))
|
||||
.log("produce-messages-" + channelName,
|
||||
Level.FINEST,
|
||||
SignalType.REQUEST,
|
||||
SignalType.ON_NEXT,
|
||||
SignalType.ON_ERROR,
|
||||
SignalType.ON_COMPLETE
|
||||
)
|
||||
.transform(sender::send)
|
||||
.doOnError(e -> LOG.error("Send failed", e))
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
sender.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package it.tdlight.reactiveapi.kafka;
|
||||
|
||||
import it.tdlight.reactiveapi.Serializer;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Map;
|
||||
import org.apache.kafka.common.errors.SerializationException;
|
||||
|
||||
public class KafkaSerializer<T> implements Serializer<T>, org.apache.kafka.common.serialization.Serializer<T> {
|
||||
|
||||
private Serializer<T> serializer;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void configure(Map<String, ?> configs, boolean isKey) {
|
||||
var clazz = (Class<?>) configs.get("custom.serializer.class");
|
||||
try {
|
||||
this.serializer = (Serializer<T>) clazz.getConstructor().newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(String topic, T data) {
|
||||
try {
|
||||
return serializer.serialize(data);
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(T data) throws IOException {
|
||||
return serializer.serialize(data);
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package it.tdlight.reactiveapi.rsocket;
|
||||
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
|
||||
public class CancelledChannelException extends java.io.IOException {
|
||||
|
||||
public CancelledChannelException() {
|
||||
}
|
||||
|
||||
public CancelledChannelException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CancelledChannelException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public CancelledChannelException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,208 @@
|
||||
package it.tdlight.reactiveapi.rsocket;
|
||||
|
||||
import io.rsocket.Payload;
|
||||
import it.tdlight.reactiveapi.Deserializer;
|
||||
import it.tdlight.reactiveapi.Timestamped;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.SignalType;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.EmitFailureHandler;
|
||||
import reactor.core.publisher.Sinks.Empty;
|
||||
import reactor.core.publisher.Sinks.Many;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class ConsumerConnection<T> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(ConsumerConnection.class);
|
||||
|
||||
private final String channel;
|
||||
private final int bufferSize;
|
||||
private Many<Flux<Payload>> remotes = Sinks.many().replay().all();
|
||||
private int remoteCount = 0;
|
||||
|
||||
private Deserializer<T> local;
|
||||
|
||||
private boolean connectedState = false;
|
||||
private Empty<Void> connectedSink = Sinks.empty();
|
||||
private Optional<Throwable> localTerminationState = null;
|
||||
private Empty<Void> localTerminationSink = Sinks.empty();
|
||||
|
||||
public ConsumerConnection(String channel, int bufferSize) {
|
||||
this.channel = channel;
|
||||
this.bufferSize = bufferSize;
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Create new blank connection", this.printStatus());
|
||||
}
|
||||
|
||||
private String printStatus() {
|
||||
return "[\"%s\" (%d)%s%s%s]".formatted(channel,
|
||||
System.identityHashCode(this),
|
||||
local != null ? ", local" : "",
|
||||
remoteCount > 0 ? (remoteCount > 1 ? ", " + remoteCount + " remotes" : ", 1 remote") : "",
|
||||
connectedState ? ((localTerminationState != null) ? (localTerminationState.isPresent() ? ", done with error" : ", done") : ", connected") : ", waiting"
|
||||
);
|
||||
}
|
||||
|
||||
public synchronized Flux<Timestamped<T>> connectLocal() {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Local is asking to connect", this.printStatus());
|
||||
return Mono.defer(() -> {
|
||||
synchronized (ConsumerConnection.this) {
|
||||
return connectedSink.asMono();
|
||||
}
|
||||
}).publishOn(Schedulers.parallel()).thenMany(Flux.defer(() -> {
|
||||
synchronized (ConsumerConnection.this) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Local is connected", this.printStatus());
|
||||
return Flux.merge(remotes.asFlux().map(remote -> {
|
||||
return remote.doOnError(ex -> {
|
||||
synchronized (ConsumerConnection.this) {
|
||||
if (remoteCount <= 1) {
|
||||
onRemoteLastError(ex);
|
||||
} else {
|
||||
remoteCount--;
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("%s Local connection ended with failure, but at least one remote is still online".formatted(
|
||||
this.printStatus()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}).doFinally(s -> {
|
||||
if (s != SignalType.ON_ERROR) {
|
||||
synchronized (ConsumerConnection.this) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ending with status {}", this.printStatus(), s);
|
||||
if (remoteCount <= 1) {
|
||||
onLastFinally(s);
|
||||
} else {
|
||||
remoteCount--;
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ended with status {}, but at least one remote is still online", this.printStatus(), s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).onErrorResume(ex -> {
|
||||
synchronized (ConsumerConnection.this) {
|
||||
if (remoteCount <= 1) {
|
||||
return Flux.error(ex);
|
||||
} else {
|
||||
return Flux.empty();
|
||||
}
|
||||
}
|
||||
});
|
||||
}), Integer.MAX_VALUE, bufferSize)
|
||||
.transform(remote -> {
|
||||
synchronized (ConsumerConnection.this) {
|
||||
return RSocketUtils.deserialize(remote, local);
|
||||
}
|
||||
})
|
||||
.map(element -> new Timestamped<>(System.currentTimeMillis(), element));
|
||||
}
|
||||
})).doOnError(this::onRemoteLastError).doFinally(this::onLastFinally);
|
||||
}
|
||||
|
||||
private synchronized void onLastFinally(SignalType s) {
|
||||
if (remoteCount > 0 && localTerminationState == null) {
|
||||
assert connectedState;
|
||||
var ex = new CancelledChannelException();
|
||||
localTerminationState = Optional.of(ex);
|
||||
if (s == SignalType.CANCEL) {
|
||||
localTerminationSink.emitError(ex, EmitFailureHandler.FAIL_FAST);
|
||||
} else {
|
||||
localTerminationSink.emitEmpty(EmitFailureHandler.FAIL_FAST);
|
||||
}
|
||||
}
|
||||
reset();
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ended with status {}, emitted termination complete", this.printStatus(), s);
|
||||
}
|
||||
|
||||
private synchronized void onRemoteLastError(Throwable ex) {
|
||||
if (remoteCount > 0 && localTerminationState == null) {
|
||||
localTerminationState = Optional.of(ex);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("%s Local connection ended with failure".formatted(this.printStatus()), ex);
|
||||
}
|
||||
if (remoteCount <= 1) {
|
||||
var sink = localTerminationSink;
|
||||
reset();
|
||||
sink.emitError(ex, EmitFailureHandler.FAIL_FAST);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("%s Local connection ended with failure, emitted termination failure".formatted(this.printStatus()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized Mono<Void> connectRemote() {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote is asking to connect", this.printStatus());
|
||||
return Mono.defer(() -> {
|
||||
synchronized (ConsumerConnection.this) {
|
||||
return connectedSink.asMono();
|
||||
}
|
||||
}).publishOn(Schedulers.parallel()).then(Mono.defer(() -> {
|
||||
synchronized (ConsumerConnection.this) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote is connected", this.printStatus());
|
||||
return localTerminationSink.asMono().publishOn(Schedulers.parallel());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public synchronized void reset() {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Reset started", this.printStatus());
|
||||
if (connectedState) {
|
||||
if (localTerminationState == null) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection is still marked as open but not terminated, interrupting it", this.printStatus());
|
||||
var ex = new InterruptedException("Interrupted this connection because a new one is being prepared");
|
||||
localTerminationState = Optional.of(ex);
|
||||
localTerminationSink.emitError(ex, EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection has been interrupted", this.printStatus());
|
||||
}
|
||||
} else {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection is still marked as waiting for a connection, interrupting it", this.printStatus());
|
||||
localTerminationState = Optional.empty();
|
||||
localTerminationSink.emitEmpty(EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection has been interrupted", this.printStatus());
|
||||
}
|
||||
local = null;
|
||||
remoteCount = 0;
|
||||
remotes.tryEmitComplete();
|
||||
remotes = Sinks.many().replay().all();
|
||||
connectedState = false;
|
||||
connectedSink = Sinks.empty();
|
||||
localTerminationState = null;
|
||||
localTerminationSink = Sinks.empty();
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Reset ended", this.printStatus());
|
||||
}
|
||||
|
||||
public synchronized void registerRemote(Flux<Payload> remote) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote is trying to register", this.printStatus());
|
||||
this.remoteCount++;
|
||||
this.remotes.tryEmitNext(remote);
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote registered", this.printStatus());
|
||||
onChanged();
|
||||
}
|
||||
|
||||
public synchronized Throwable registerLocal(Deserializer<T> local) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Local is trying to register", this.printStatus());
|
||||
if (this.local != null) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Local was already registered", this.printStatus());
|
||||
return new IllegalStateException("Local is already registered");
|
||||
}
|
||||
this.local = local;
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Local registered", this.printStatus());
|
||||
onChanged();
|
||||
return null;
|
||||
}
|
||||
|
||||
private synchronized void onChanged() {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Checking connection changes", this.printStatus());
|
||||
if (local != null && remoteCount > 0) {
|
||||
connectedState = true;
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Connected successfully! Emitting connected event", this.printStatus());
|
||||
connectedSink.emitEmpty(EmitFailureHandler.FAIL_FAST);
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Connected successfully! Emitted connected event", this.printStatus());
|
||||
} else {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Still not connected", this.printStatus());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package it.tdlight.reactiveapi.rsocket;
|
||||
|
||||
import it.cavallium.filequeue.Deserializer;
|
||||
import it.cavallium.filequeue.Serializer;
|
||||
import it.tdlight.reactiveapi.ClientBoundEventDeserializer;
|
||||
import java.io.DataInput;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
|
||||
public class FileQueueUtils {
|
||||
|
||||
public static <T> Serializer<T> convert(it.tdlight.reactiveapi.Serializer<T> serializer) {
|
||||
return new Serializer<T>() {
|
||||
@Override
|
||||
public byte[] serialize(T data) throws IOException {
|
||||
return serializer.serialize(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(T data, DataOutput output) throws IOException {
|
||||
serializer.serialize(data, output);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static <T> Deserializer<T> convert(it.tdlight.reactiveapi.Deserializer<T> deserializer) {
|
||||
return new Deserializer<T>() {
|
||||
@Override
|
||||
public T deserialize(byte[] data) throws IOException {
|
||||
return deserializer.deserialize(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(int length, DataInput dataInput) throws IOException {
|
||||
return deserializer.deserialize(length, dataInput);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package it.tdlight.reactiveapi.rsocket;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import io.rsocket.ConnectionSetupPayload;
|
||||
import io.rsocket.Payload;
|
||||
import io.rsocket.RSocket;
|
||||
import io.rsocket.SocketAcceptor;
|
||||
import io.rsocket.core.RSocketConnector;
|
||||
import io.rsocket.frame.decoder.PayloadDecoder;
|
||||
import io.rsocket.transport.ClientTransport;
|
||||
import io.rsocket.transport.netty.client.TcpClientTransport;
|
||||
import io.rsocket.util.DefaultPayload;
|
||||
import it.tdlight.reactiveapi.ChannelCodec;
|
||||
import it.tdlight.reactiveapi.Deserializer;
|
||||
import it.tdlight.reactiveapi.EventConsumer;
|
||||
import it.tdlight.reactiveapi.EventProducer;
|
||||
import it.tdlight.reactiveapi.Serializer;
|
||||
import it.tdlight.reactiveapi.SimpleEventProducer;
|
||||
import it.tdlight.reactiveapi.Timestamped;
|
||||
import it.tdlight.reactiveapi.TransportFactory;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Hooks;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.EmitFailureHandler;
|
||||
import reactor.core.publisher.Sinks.Empty;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
public class MyRSocketClient implements RSocketChannelManager {
|
||||
|
||||
private final Mono<RSocket> nextClient;
|
||||
private final AtomicReference<RSocket> lastClient = new AtomicReference<>();
|
||||
private final Empty<Void> disposeRequest = Sinks.empty();
|
||||
|
||||
public MyRSocketClient(HostAndPort baseHost) {
|
||||
this(TransportFactory.tcp(baseHost));
|
||||
}
|
||||
|
||||
public MyRSocketClient(TransportFactory transportFactory) {
|
||||
this.nextClient = RSocketConnector.create()
|
||||
.setupPayload(DefaultPayload.create("client", "setup-info"))
|
||||
.payloadDecoder(PayloadDecoder.ZERO_COPY)
|
||||
.connect(transportFactory.getClientTransport(0))
|
||||
.doOnNext(lastClient::set)
|
||||
.cacheInvalidateIf(RSocket::isDisposed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <K> EventConsumer<K> registerConsumer(ChannelCodec channelCodec, String channelName) {
|
||||
Deserializer<K> deserializer = channelCodec.getNewDeserializer();
|
||||
return new EventConsumer<K>() {
|
||||
@Override
|
||||
public Flux<Timestamped<K>> consumeMessages() {
|
||||
return nextClient.flatMapMany(client -> client
|
||||
.requestStream(DefaultPayload.create(channelName, "channel"))
|
||||
.transform(flux -> RSocketUtils.deserialize(flux, deserializer))
|
||||
.map(event -> new Timestamped<>(System.currentTimeMillis(), event)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public <K> EventProducer<K> registerProducer(ChannelCodec channelCodec, String channelName) {
|
||||
Serializer<K> serializer = channelCodec.getNewSerializer();
|
||||
return new SimpleEventProducer<K>() {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handleSendMessages(Flux<K> eventsFlux) {
|
||||
return Mono.defer(() -> {
|
||||
Flux<Payload> rawFlux = eventsFlux.transform(flux -> RSocketUtils.serialize(flux, serializer));
|
||||
Flux<Payload> combinedRawFlux = Flux.just(DefaultPayload.create(channelName, "channel")).concatWith(rawFlux);
|
||||
return nextClient.flatMapMany(client -> client.requestChannel(combinedRawFlux).take(1, true)).then();
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> onClose() {
|
||||
return disposeRequest.asMono();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
disposeRequest.emitEmpty(EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
|
||||
var c = lastClient.get();
|
||||
if (c != null) {
|
||||
c.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,193 @@
|
||||
package it.tdlight.reactiveapi.rsocket;
|
||||
|
||||
import static reactor.util.concurrent.Queues.XS_BUFFER_SIZE;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import io.rsocket.Closeable;
|
||||
import io.rsocket.ConnectionSetupPayload;
|
||||
import io.rsocket.Payload;
|
||||
import io.rsocket.RSocket;
|
||||
import io.rsocket.SocketAcceptor;
|
||||
import io.rsocket.core.RSocketServer;
|
||||
import io.rsocket.frame.decoder.PayloadDecoder;
|
||||
import io.rsocket.transport.ServerTransport;
|
||||
import io.rsocket.transport.netty.server.CloseableChannel;
|
||||
import io.rsocket.transport.netty.server.TcpServerTransport;
|
||||
import io.rsocket.util.DefaultPayload;
|
||||
import it.tdlight.reactiveapi.ChannelCodec;
|
||||
import it.tdlight.reactiveapi.Deserializer;
|
||||
import it.tdlight.reactiveapi.EventConsumer;
|
||||
import it.tdlight.reactiveapi.EventProducer;
|
||||
import it.tdlight.reactiveapi.Serializer;
|
||||
import it.tdlight.reactiveapi.Timestamped;
|
||||
import it.tdlight.reactiveapi.TransportFactory;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class MyRSocketServer implements RSocketChannelManager, RSocket {
|
||||
|
||||
private final Logger logger = LogManager.getLogger(this.getClass());
|
||||
|
||||
private final int bufferSize;
|
||||
private final Mono<Closeable> serverCloseable;
|
||||
|
||||
protected final Map<String, ConsumerConnection<?>> consumerRegistry = new ConcurrentHashMap<>();
|
||||
|
||||
protected final Map<String, ProducerConnection<?>> producerRegistry = new ConcurrentHashMap<>();
|
||||
|
||||
public MyRSocketServer(HostAndPort baseHost) {
|
||||
this(TransportFactory.tcp(baseHost));
|
||||
}
|
||||
|
||||
public MyRSocketServer(HostAndPort baseHost, int bufferSize) {
|
||||
this(TransportFactory.tcp(baseHost), bufferSize);
|
||||
}
|
||||
|
||||
public MyRSocketServer(TransportFactory transportFactory) {
|
||||
this(transportFactory, XS_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
public MyRSocketServer(TransportFactory transportFactory, int bufferSize) {
|
||||
this.bufferSize = bufferSize;
|
||||
Mono<Closeable> serverMono = RSocketServer
|
||||
.create(new SocketAcceptor() {
|
||||
@Override
|
||||
public @NotNull Mono<RSocket> accept(@NotNull ConnectionSetupPayload setup, @NotNull RSocket sendingSocket) {
|
||||
if (setup.getMetadataUtf8().equals("setup-info") && setup.getDataUtf8().equals("client")) {
|
||||
return Mono.just(MyRSocketServer.this);
|
||||
} else {
|
||||
return Mono.error(new IOException("Invalid credentials"));
|
||||
}
|
||||
}
|
||||
})
|
||||
.payloadDecoder(PayloadDecoder.ZERO_COPY)
|
||||
.bind(transportFactory.getServerTransport(0))
|
||||
.cast(Object.class)
|
||||
.doOnNext(d -> logger.debug("Server up"))
|
||||
.cacheInvalidateIf(t -> t instanceof Closeable closeableChannel && closeableChannel.isDisposed())
|
||||
.filter(t -> t instanceof Closeable)
|
||||
.cast(Closeable.class)
|
||||
.defaultIfEmpty(new Closeable() {
|
||||
@Override
|
||||
public void dispose() {}
|
||||
@Override
|
||||
public @NotNull Mono<Void> onClose() {
|
||||
return Mono.empty();
|
||||
}
|
||||
});
|
||||
|
||||
serverMono.subscribeOn(Schedulers.parallel()).subscribe(v -> {}, ex -> logger.warn("Failed to bind server"));
|
||||
|
||||
this.serverCloseable = serverMono;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Flux<Payload> requestChannel(@NotNull Publisher<Payload> payloads) {
|
||||
return Flux.from(payloads).switchOnFirst((first, flux) -> {
|
||||
if (first.isOnNext()) {
|
||||
var firstValue = first.get();
|
||||
assert firstValue != null;
|
||||
var meta = firstValue.getMetadataUtf8();
|
||||
if (!meta.equals("channel")) {
|
||||
return Mono.error(new CancelledChannelException("Metadata is wrong"));
|
||||
}
|
||||
var channel = firstValue.getDataUtf8();
|
||||
var conn = MyRSocketServer.this.consumerRegistry.computeIfAbsent(channel,
|
||||
ch -> new ConsumerConnection<>(ch, bufferSize));
|
||||
conn.registerRemote(flux.skip(1));
|
||||
return conn.connectRemote().then(Mono.fromSupplier(() -> DefaultPayload.create("ok", "result")));
|
||||
} else {
|
||||
return flux.take(1, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Flux<Payload> requestStream(@NotNull Payload payload) {
|
||||
var channel = payload.getDataUtf8();
|
||||
return Flux.defer(() -> {
|
||||
var conn = MyRSocketServer.this.producerRegistry.computeIfAbsent(channel,
|
||||
ch -> new ProducerConnection<>(ch, bufferSize));
|
||||
conn.registerRemote();
|
||||
return conn.connectRemote();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public final <K> EventConsumer<K> registerConsumer(ChannelCodec channelCodec, String channelName) {
|
||||
logger.debug("Registering consumer for channel \"{}\"", channelName);
|
||||
Deserializer<K> deserializer;
|
||||
try {
|
||||
deserializer = channelCodec.getNewDeserializer();
|
||||
} catch (Throwable ex) {
|
||||
logger.error("Failed to create codec for channel \"{}\"", channelName, ex);
|
||||
throw new IllegalStateException("Failed to create codec for channel " + channelName);
|
||||
}
|
||||
return new EventConsumer<K>() {
|
||||
@Override
|
||||
public Flux<Timestamped<K>> consumeMessages() {
|
||||
return serverCloseable.flatMapMany(x -> {
|
||||
//noinspection unchecked
|
||||
var conn = (ConsumerConnection<K>) consumerRegistry.computeIfAbsent(channelName,
|
||||
ch -> new ConsumerConnection<>(ch, bufferSize));
|
||||
Throwable ex = conn.registerLocal(deserializer);
|
||||
if (ex != null) {
|
||||
return Flux.error(ex);
|
||||
}
|
||||
return conn.connectLocal();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public <K> EventProducer<K> registerProducer(ChannelCodec channelCodec, String channelName) {
|
||||
logger.debug("Registering producer for channel \"{}\"", channelName);
|
||||
Serializer<K> serializer;
|
||||
try {
|
||||
serializer = channelCodec.getNewSerializer();
|
||||
} catch (Throwable ex) {
|
||||
logger.error("Failed to create codec for channel \"{}\"", channelName, ex);
|
||||
throw new IllegalStateException("Failed to create codec for channel " + channelName);
|
||||
}
|
||||
return new EventProducer<>() {
|
||||
@Override
|
||||
public Mono<Void> sendMessages(Flux<K> eventsFlux) {
|
||||
return serverCloseable.flatMap(x -> {
|
||||
//noinspection unchecked
|
||||
var conn = (ProducerConnection<K>) producerRegistry.computeIfAbsent(channelName,
|
||||
ch -> new ProducerConnection<>(ch, bufferSize));
|
||||
conn.registerLocal(eventsFlux.transform(flux -> RSocketUtils.serialize(flux, serializer)));
|
||||
return conn.connectLocal();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Mono<Void> onClose() {
|
||||
return Mono.when(serverCloseable.flatMap(Closeable::onClose));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
serverCloseable
|
||||
.doOnNext(Closeable::dispose)
|
||||
.subscribeOn(Schedulers.parallel())
|
||||
.subscribe(v -> {}, ex -> logger.error("Failed to dispose the server", ex));
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package it.tdlight.reactiveapi.rsocket;
|
||||
|
||||
import io.rsocket.Payload;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
sealed interface PendingEventsToProduce {
|
||||
|
||||
record ServerPendingEventsToProduce(Flux<Payload> events, CompletableFuture<Void> initCf,
|
||||
CompletableFuture<Void> doneCf) implements PendingEventsToProduce {}
|
||||
|
||||
record ClientPendingEventsToProduce(CompletableFuture<Void> doneCf,
|
||||
CompletableFuture<Flux<Payload>> fluxCf,
|
||||
CompletableFuture<Void> initCf) implements PendingEventsToProduce {}
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
package it.tdlight.reactiveapi.rsocket;
|
||||
|
||||
import io.rsocket.Payload;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.SignalType;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.EmitFailureHandler;
|
||||
import reactor.core.publisher.Sinks.Empty;
|
||||
import reactor.core.publisher.Sinks.Many;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class ProducerConnection<T> {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(ProducerConnection.class);
|
||||
|
||||
private final String channel;
|
||||
private final int bufferSize;
|
||||
private int remoteCount = 0;
|
||||
|
||||
private Flux<Payload> local;
|
||||
|
||||
private boolean connectedState = false;
|
||||
private Empty<Void> connectedSink = Sinks.empty();
|
||||
private Optional<Throwable> remoteTerminationState = null;
|
||||
private Empty<Void> remoteTerminationSink = Sinks.empty();
|
||||
|
||||
public ProducerConnection(String channel, int bufferSize) {
|
||||
this.channel = channel;
|
||||
this.bufferSize = bufferSize;
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Create new blank connection", this.printStatus());
|
||||
}
|
||||
|
||||
private synchronized String printStatus() {
|
||||
return "[\"%s\" (%d)%s%s%s]".formatted(channel,
|
||||
System.identityHashCode(this),
|
||||
local != null ? ", local" : "",
|
||||
remoteCount > 0 ? (remoteCount > 1 ? ", " + remoteCount + " remotes" : ", 1 remote") : "",
|
||||
connectedState ? ((remoteTerminationState != null) ? (remoteTerminationState.isPresent() ? ", done with error" : ", done") : ", connected") : ", waiting"
|
||||
);
|
||||
}
|
||||
|
||||
public synchronized Mono<Void> connectLocal() {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Local is asking to connect", this.printStatus());
|
||||
return Mono.defer(() -> {
|
||||
synchronized (ProducerConnection.this) {
|
||||
return connectedSink.asMono();
|
||||
}
|
||||
}).publishOn(Schedulers.parallel()).then(Mono.defer(() -> {
|
||||
synchronized (ProducerConnection.this) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Local is connected", this.printStatus());
|
||||
return remoteTerminationSink.asMono().publishOn(Schedulers.parallel());
|
||||
}
|
||||
})).doFinally(s -> {
|
||||
if (s == SignalType.ON_ERROR || s == SignalType.CANCEL) {
|
||||
synchronized (ProducerConnection.this) {
|
||||
if (connectedState) {
|
||||
connectedState = false;
|
||||
connectedSink = Sinks.empty();
|
||||
}
|
||||
local = null;
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Local is cancelled", this.printStatus());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public synchronized Flux<Payload> connectRemote() {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote is asking to connect", this.printStatus());
|
||||
return Mono.defer(() -> {
|
||||
synchronized (ProducerConnection.this) {
|
||||
return connectedSink.asMono();
|
||||
}
|
||||
}).publishOn(Schedulers.parallel()).thenMany(Flux.defer(() -> {
|
||||
synchronized (ProducerConnection.this) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote is connected", this.printStatus());
|
||||
return local;
|
||||
}
|
||||
})).doOnError(ex -> {
|
||||
synchronized (ProducerConnection.this) {
|
||||
if (remoteCount <= 1) {
|
||||
if (local != null && remoteTerminationState == null) {
|
||||
remoteTerminationState = Optional.of(ex);
|
||||
if (LOG.isDebugEnabled()) LOG.debug("%s Remote connection ended with failure, emitting termination failure".formatted(this.printStatus()), ex);
|
||||
var sink = remoteTerminationSink;
|
||||
reset();
|
||||
sink.emitError(ex, EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
|
||||
if (LOG.isDebugEnabled()) LOG.debug("%s Remote connection ended with failure, emitted termination failure".formatted(this.printStatus()));
|
||||
}
|
||||
} else {
|
||||
remoteCount--;
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("%s Remote connection ended with failure, but at least one remote is still online".formatted(
|
||||
this.printStatus()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}).doFinally(s -> {
|
||||
if (s != SignalType.ON_ERROR) {
|
||||
synchronized (ProducerConnection.this) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ending with status {}", this.printStatus(), s);
|
||||
if (remoteCount <= 1) {
|
||||
if (local != null && remoteTerminationState == null) {
|
||||
assert connectedState;
|
||||
remoteTerminationState = Optional.empty();
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ended with status {}, emitting termination complete", this.printStatus(), s);
|
||||
if (s == SignalType.CANCEL) {
|
||||
remoteTerminationSink.emitError(new CancelledChannelException(), EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
|
||||
} else {
|
||||
remoteTerminationSink.emitEmpty(EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
|
||||
}
|
||||
}
|
||||
reset();
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ended with status {}, emitted termination complete", this.printStatus(), s);
|
||||
} else {
|
||||
remoteCount--;
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote connection ended with status {}, but at least one remote is still online", this.printStatus(), s);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public synchronized void reset() {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Reset started", this.printStatus());
|
||||
if (connectedState) {
|
||||
if (remoteTerminationState == null) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection is still marked as open but not terminated, interrupting it", this.printStatus());
|
||||
var ex = new InterruptedException("Interrupted this connection because a new one is being prepared");
|
||||
remoteTerminationState = Optional.of(ex);
|
||||
remoteTerminationSink.emitError(ex, EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection has been interrupted", this.printStatus());
|
||||
}
|
||||
} else {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection is still marked as waiting for a connection, interrupting it", this.printStatus());
|
||||
remoteTerminationState = Optional.empty();
|
||||
remoteTerminationSink.emitEmpty(EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} The previous connection has been interrupted", this.printStatus());
|
||||
}
|
||||
local = null;
|
||||
remoteCount = 0;
|
||||
connectedState = false;
|
||||
connectedSink = Sinks.empty();
|
||||
remoteTerminationState = null;
|
||||
remoteTerminationSink = Sinks.empty();
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Reset ended", this.printStatus());
|
||||
}
|
||||
|
||||
public synchronized void registerRemote() {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote is trying to register", this.printStatus());
|
||||
this.remoteCount++;
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Remote registered", this.printStatus());
|
||||
onChanged();
|
||||
}
|
||||
|
||||
public synchronized void registerLocal(Flux<Payload> local) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Local is trying to register", this.printStatus());
|
||||
if (this.local != null) {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Local was already registered", this.printStatus());
|
||||
throw new IllegalStateException("Local is already registered");
|
||||
}
|
||||
this.local = local.publish(bufferSize).refCount(1);
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Local registered", this.printStatus());
|
||||
onChanged();
|
||||
}
|
||||
|
||||
private synchronized void onChanged() {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Checking connection changes", this.printStatus());
|
||||
if (local != null && remoteCount > 0) {
|
||||
connectedState = true;
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Connected successfully! Emitting connected event", this.printStatus());
|
||||
connectedSink.emitEmpty(EmitFailureHandler.busyLooping(Duration.ofMillis(100)));
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Connected successfully! Emitted connected event", this.printStatus());
|
||||
} else {
|
||||
if (LOG.isDebugEnabled()) LOG.debug("{} Still not connected", this.printStatus());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package it.tdlight.reactiveapi.rsocket;
|
||||
|
||||
import io.rsocket.Closeable;
|
||||
import it.tdlight.reactiveapi.ChannelCodec;
|
||||
import it.tdlight.reactiveapi.EventConsumer;
|
||||
import it.tdlight.reactiveapi.EventProducer;
|
||||
|
||||
public interface RSocketChannelManager extends Closeable {
|
||||
|
||||
<K> EventConsumer<K> registerConsumer(ChannelCodec channelCodec, String channelName);
|
||||
|
||||
<K> EventProducer<K> registerProducer(ChannelCodec channelCodec, String channelName);
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package it.tdlight.reactiveapi.rsocket;
|
||||
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.buffer.ByteBufInputStream;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.rsocket.Payload;
|
||||
import io.rsocket.util.DefaultPayload;
|
||||
import it.tdlight.reactiveapi.Deserializer;
|
||||
import it.tdlight.reactiveapi.Serializer;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public class RSocketUtils {
|
||||
|
||||
public static <T> Flux<T> deserialize(Flux<Payload> payloadFlux, Deserializer<T> deserializer) {
|
||||
return payloadFlux.map(payload -> {
|
||||
try {
|
||||
try (var bis = new ByteBufInputStream(payload.sliceData())) {
|
||||
return deserializer.deserialize(payload.data().readableBytes(), bis);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
} finally {
|
||||
payload.release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static <T> Flux<Payload> serialize(Flux<T> flux, Serializer<T> serializer) {
|
||||
return flux.map(element -> {
|
||||
var buf = ByteBufAllocator.DEFAULT.ioBuffer();
|
||||
try (var baos = new ByteBufOutputStream(buf)) {
|
||||
serializer.serialize(element, baos);
|
||||
return DefaultPayload.create(baos.buffer().retain());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
} finally {
|
||||
buf.release();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package it.tdlight.reactiveapi.transformer;
|
||||
|
||||
import it.tdlight.jni.TdApi.CheckAuthenticationCode;
|
||||
import it.tdlight.jni.TdApi.CheckAuthenticationPassword;
|
||||
import it.tdlight.reactiveapi.Cli;
|
||||
import it.tdlight.reactiveapi.Event.OnBotLoginCodeRequested;
|
||||
import it.tdlight.reactiveapi.Event.OnOtherDeviceLoginRequested;
|
||||
import it.tdlight.reactiveapi.Event.OnPasswordRequested;
|
||||
import it.tdlight.reactiveapi.Event.OnUserLoginCodeRequested;
|
||||
import it.tdlight.reactiveapi.ResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ClientBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEventTransformer;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class CommandLineAuth implements ResultingEventTransformer {
|
||||
|
||||
@Override
|
||||
public Flux<ResultingEvent> transform(boolean isBot, Flux<ResultingEvent> events) {
|
||||
return events.flatMapSequential(event -> {
|
||||
if (event instanceof ClientBoundResultingEvent clientBoundResultingEvent) {
|
||||
if (clientBoundResultingEvent.event() instanceof OnUserLoginCodeRequested requested) {
|
||||
return this
|
||||
.askParam("Please type the login code of " + requested.phoneNumber(), requested.userId())
|
||||
.<ResultingEvent>map(code -> new TDLibBoundResultingEvent<>(new CheckAuthenticationCode(code)));
|
||||
} else if (clientBoundResultingEvent.event() instanceof OnBotLoginCodeRequested requested) {
|
||||
return this
|
||||
.askParam("Please type the login code of " + requested.token(), requested.userId())
|
||||
.<ResultingEvent>map(code -> new TDLibBoundResultingEvent<>(new CheckAuthenticationCode(code)));
|
||||
} else if (clientBoundResultingEvent.event() instanceof OnPasswordRequested onPasswordRequested) {
|
||||
return this
|
||||
.askParam("Please type the password. Hint: " + onPasswordRequested.passwordHint() + (
|
||||
onPasswordRequested.hasRecoveryEmail() ? " Recovery e-mail: "
|
||||
+ onPasswordRequested.recoveryEmailPattern() : ""), onPasswordRequested.userId())
|
||||
.<ResultingEvent>map(password -> new TDLibBoundResultingEvent<>(new CheckAuthenticationPassword(password)));
|
||||
} else if (clientBoundResultingEvent.event() instanceof OnOtherDeviceLoginRequested onOtherDeviceLoginRequested) {
|
||||
return this
|
||||
.askParam("Please confirm the login on another other device, (after copying the link press enter): "
|
||||
+ onOtherDeviceLoginRequested.link(), onOtherDeviceLoginRequested.userId())
|
||||
.then(Mono.empty());
|
||||
}
|
||||
}
|
||||
return Mono.just(event);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<String> askParam(String text, long userId) {
|
||||
return Mono
|
||||
.fromCallable(() -> Cli.askParameter("[#IDU" + userId + "]" + text))
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package it.tdlight.reactiveapi.transformer;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.OptionValueBoolean;
|
||||
import it.tdlight.jni.TdApi.OptionValueInteger;
|
||||
import it.tdlight.reactiveapi.Event.OnUpdateData;
|
||||
import it.tdlight.reactiveapi.ResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ClientBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEventTransformer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public class DefaultOptions implements ResultingEventTransformer {
|
||||
|
||||
private static final Collection<ResultingEvent> DEFAULT_OPTIONS = List.of(
|
||||
setInt("message_unload_delay", 1800),
|
||||
setBoolean("disable_persistent_network_statistics", true),
|
||||
setBoolean("disable_time_adjustment_protection", true),
|
||||
setBoolean("ignore_inline_thumbnails", true),
|
||||
setBoolean("ignore_platform_restrictions", true),
|
||||
setBoolean("use_storage_optimizer", true)
|
||||
);
|
||||
|
||||
private static final Collection<ResultingEvent> DEFAULT_USER_OPTIONS = List.of(
|
||||
setBoolean("disable_animated_emoji", true),
|
||||
setBoolean("disable_contact_registered_notifications", true),
|
||||
setBoolean("disable_top_chats", true),
|
||||
setInt("notification_group_count_max", 0),
|
||||
setInt("notification_group_size_max", 1)
|
||||
);
|
||||
|
||||
@Override
|
||||
public Flux<ResultingEvent> transform(boolean isBot, Flux<ResultingEvent> events) {
|
||||
return events.concatMapIterable(event -> {
|
||||
|
||||
// Append the options if the initial auth state is intercepted
|
||||
if (event instanceof ClientBoundResultingEvent clientBoundResultingEvent
|
||||
&& clientBoundResultingEvent.event() instanceof OnUpdateData onUpdate
|
||||
&& onUpdate.update() instanceof TdApi.UpdateAuthorizationState authorizationState
|
||||
&& authorizationState.authorizationState instanceof TdApi.AuthorizationStateWaitTdlibParameters) {
|
||||
|
||||
var resultingEvent = new ArrayList<ResultingEvent>(1 + DEFAULT_OPTIONS.size() + DEFAULT_USER_OPTIONS.size());
|
||||
// Add the intercepted event
|
||||
resultingEvent.add(event);
|
||||
// Add the default options
|
||||
resultingEvent.addAll(DEFAULT_OPTIONS);
|
||||
// Add user-only default options
|
||||
if (!isBot) {
|
||||
resultingEvent.addAll(DEFAULT_USER_OPTIONS);
|
||||
}
|
||||
return resultingEvent;
|
||||
} else {
|
||||
// Return just the intercepted event as-is
|
||||
return List.of(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static ResultingEvent setBoolean(String optionName, boolean value) {
|
||||
return new TDLibBoundResultingEvent<>(new TdApi.SetOption(optionName, new OptionValueBoolean(value)));
|
||||
}
|
||||
|
||||
private static ResultingEvent setInt(String optionName, int value) {
|
||||
return new TDLibBoundResultingEvent<>(new TdApi.SetOption(optionName, new OptionValueInteger(value)));
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package it.tdlight.reactiveapi.transformer;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.reactiveapi.ResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEventTransformer;
|
||||
import java.util.List;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public class DisableChatDatabase implements ResultingEventTransformer {
|
||||
|
||||
@Override
|
||||
public Flux<ResultingEvent> transform(boolean isBot, Flux<ResultingEvent> events) {
|
||||
return events.concatMapIterable(event -> {
|
||||
|
||||
// Change option
|
||||
if (event instanceof TDLibBoundResultingEvent tdLibBoundResultingEvent
|
||||
&& tdLibBoundResultingEvent.action() instanceof TdApi.SetTdlibParameters setTdlibParameters) {
|
||||
setTdlibParameters.useChatInfoDatabase = false;
|
||||
}
|
||||
|
||||
return List.of(event);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package it.tdlight.reactiveapi.transformer;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.reactiveapi.ResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEventTransformer;
|
||||
import java.util.List;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public class DisableFileDatabase implements ResultingEventTransformer {
|
||||
|
||||
@Override
|
||||
public Flux<ResultingEvent> transform(boolean isBot, Flux<ResultingEvent> events) {
|
||||
return events.concatMapIterable(event -> {
|
||||
|
||||
// Change option
|
||||
if (event instanceof TDLibBoundResultingEvent tdLibBoundResultingEvent
|
||||
&& tdLibBoundResultingEvent.action() instanceof TdApi.SetTdlibParameters setTdlibParameters) {
|
||||
setTdlibParameters.useFileDatabase = false;
|
||||
}
|
||||
|
||||
return List.of(event);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package it.tdlight.reactiveapi.transformer;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.OptionValueBoolean;
|
||||
import it.tdlight.reactiveapi.ResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEventTransformer;
|
||||
import java.util.List;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public class DisableLogs implements ResultingEventTransformer {
|
||||
|
||||
@Override
|
||||
public Flux<ResultingEvent> transform(boolean isBot, Flux<ResultingEvent> events) {
|
||||
return events.concatMapIterable(event -> {
|
||||
|
||||
// Append SetVerbosityLevel after SetTDLibParameters
|
||||
if (event instanceof TDLibBoundResultingEvent tdLibBoundResultingEvent
|
||||
&& tdLibBoundResultingEvent.action() instanceof TdApi.SetTdlibParameters) {
|
||||
return List.of(event, new TDLibBoundResultingEvent<>(new TdApi.SetLogVerbosityLevel(0)));
|
||||
}
|
||||
|
||||
return List.of(event);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package it.tdlight.reactiveapi.transformer;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.reactiveapi.ResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEventTransformer;
|
||||
import java.util.List;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public class DisableMessageDatabase implements ResultingEventTransformer {
|
||||
|
||||
@Override
|
||||
public Flux<ResultingEvent> transform(boolean isBot, Flux<ResultingEvent> events) {
|
||||
return events.concatMapIterable(event -> {
|
||||
|
||||
// Change option
|
||||
if (event instanceof TDLibBoundResultingEvent tdLibBoundResultingEvent
|
||||
&& tdLibBoundResultingEvent.action() instanceof TdApi.SetTdlibParameters setTdlibParameters) {
|
||||
setTdlibParameters.useMessageDatabase = false;
|
||||
}
|
||||
|
||||
return List.of(event);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package it.tdlight.reactiveapi.transformer;
|
||||
|
||||
import it.tdlight.reactiveapi.ResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEventTransformer;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public class DummyResultingEventTransformer implements ResultingEventTransformer {
|
||||
|
||||
@Override
|
||||
public Flux<ResultingEvent> transform(boolean isBot, Flux<ResultingEvent> events) {
|
||||
return events;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package it.tdlight.reactiveapi.transformer;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Ok;
|
||||
import it.tdlight.jni.TdApi.OptionValueBoolean;
|
||||
import it.tdlight.reactiveapi.Event.OnUpdateData;
|
||||
import it.tdlight.reactiveapi.ResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ClientBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEventTransformer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public class EnableMinithumbnails implements ResultingEventTransformer {
|
||||
|
||||
@Override
|
||||
public Flux<ResultingEvent> transform(boolean isBot, Flux<ResultingEvent> events) {
|
||||
return events.concatMapIterable(event -> {
|
||||
|
||||
// Append the options if the initial auth state is intercepted
|
||||
if (event instanceof ClientBoundResultingEvent clientBoundResultingEvent
|
||||
&& clientBoundResultingEvent.event() instanceof OnUpdateData onUpdate
|
||||
&& onUpdate.update() instanceof TdApi.UpdateAuthorizationState authorizationState
|
||||
&& authorizationState.authorizationState instanceof TdApi.AuthorizationStateWaitTdlibParameters) {
|
||||
|
||||
var resultingEvent = new ArrayList<ResultingEvent>(2);
|
||||
// Add the intercepted event
|
||||
resultingEvent.add(event);
|
||||
// Enable minithumbnails
|
||||
resultingEvent.add(new TDLibBoundResultingEvent<>(new TdApi.SetOption("disable_minithumbnails",
|
||||
new OptionValueBoolean(false)), true));
|
||||
return resultingEvent;
|
||||
} else {
|
||||
// Return just the intercepted event as-is
|
||||
return List.of(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package it.tdlight.reactiveapi.transformer;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.ChatListArchive;
|
||||
import it.tdlight.jni.TdApi.ChatListMain;
|
||||
import it.tdlight.reactiveapi.Event.OnUpdateData;
|
||||
import it.tdlight.reactiveapi.ResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ClientBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEventTransformer;
|
||||
import java.util.List;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public class LoadChats implements ResultingEventTransformer {
|
||||
|
||||
@Override
|
||||
public Flux<ResultingEvent> transform(boolean isBot, Flux<ResultingEvent> events) {
|
||||
if (isBot) {
|
||||
return events;
|
||||
}
|
||||
return events.concatMapIterable(event -> {
|
||||
|
||||
// Append the LoadChat call if the ready auth state is intercepted
|
||||
if (event instanceof ClientBoundResultingEvent clientBoundResultingEvent
|
||||
&& clientBoundResultingEvent.event() instanceof OnUpdateData onUpdate
|
||||
&& onUpdate.update() instanceof TdApi.UpdateAuthorizationState authorizationState
|
||||
&& authorizationState.authorizationState instanceof TdApi.AuthorizationStateReady) {
|
||||
return List.of(event,
|
||||
new TDLibBoundResultingEvent<>(new TdApi.LoadChats(new ChatListMain(), 500), true),
|
||||
new TDLibBoundResultingEvent<>(new TdApi.LoadChats(new ChatListArchive(), 500), true)
|
||||
);
|
||||
}
|
||||
|
||||
return List.of(event);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package it.tdlight.reactiveapi.transformer;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.OptionValueBoolean;
|
||||
import it.tdlight.jni.TdApi.OptionValueInteger;
|
||||
import it.tdlight.reactiveapi.Event.OnUpdateData;
|
||||
import it.tdlight.reactiveapi.ResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.ClientBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEvent.TDLibBoundResultingEvent;
|
||||
import it.tdlight.reactiveapi.ResultingEventTransformer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
public class TdlightDefaultOptions implements ResultingEventTransformer {
|
||||
|
||||
private static final Collection<ResultingEvent> DEFAULT_OPTIONS = List.of(
|
||||
setBoolean("disable_document_filenames", true),
|
||||
setBoolean("disable_minithumbnails", true),
|
||||
setBoolean("disable_notifications", true),
|
||||
setBoolean("ignore_update_chat_last_message", true),
|
||||
setBoolean("ignore_update_chat_read_inbox", true),
|
||||
setBoolean("ignore_update_user_chat_action", true),
|
||||
setBoolean("ignore_server_deletes_and_reads", true)
|
||||
);
|
||||
|
||||
private static final Collection<ResultingEvent> DEFAULT_USER_OPTIONS = List.of();
|
||||
|
||||
@Override
|
||||
public Flux<ResultingEvent> transform(boolean isBot, Flux<ResultingEvent> events) {
|
||||
return events.concatMapIterable(event -> {
|
||||
|
||||
// Append the options if the initial auth state is intercepted
|
||||
if (event instanceof ClientBoundResultingEvent clientBoundResultingEvent
|
||||
&& clientBoundResultingEvent.event() instanceof OnUpdateData onUpdate
|
||||
&& onUpdate.update() instanceof TdApi.UpdateAuthorizationState authorizationState
|
||||
&& authorizationState.authorizationState instanceof TdApi.AuthorizationStateWaitTdlibParameters) {
|
||||
|
||||
var resultingEvent = new ArrayList<ResultingEvent>(1 + DEFAULT_OPTIONS.size() + DEFAULT_USER_OPTIONS.size());
|
||||
// Add the intercepted event
|
||||
resultingEvent.add(event);
|
||||
// Add the default options
|
||||
resultingEvent.addAll(DEFAULT_OPTIONS);
|
||||
// Add user-only default options
|
||||
if (!isBot) {
|
||||
resultingEvent.addAll(DEFAULT_USER_OPTIONS);
|
||||
}
|
||||
return resultingEvent;
|
||||
} else {
|
||||
// Return just the intercepted event as-is
|
||||
return List.of(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static ResultingEvent setBoolean(String optionName, boolean value) {
|
||||
return new TDLibBoundResultingEvent<>(new TdApi.SetOption(optionName, new OptionValueBoolean(value)));
|
||||
}
|
||||
|
||||
private static ResultingEvent setInt(String optionName, int value) {
|
||||
return new TDLibBoundResultingEvent<>(new TdApi.SetOption(optionName, new OptionValueInteger(value)));
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package it.tdlight.tdlibsession;
|
||||
|
||||
public enum FatalErrorType {
|
||||
ACCESS_TOKEN_INVALID, PHONE_NUMBER_INVALID, CONNECTION_KILLED, INVALID_UPDATE, PHONE_NUMBER_BANNED
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package it.tdlight.tdlibsession;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
class SignalMessage<T> {
|
||||
|
||||
private final SignalType signalType;
|
||||
private final T item;
|
||||
private final String errorMessage;
|
||||
|
||||
private SignalMessage(SignalType signalType, T item, String errorMessage) {
|
||||
this.signalType = signalType;
|
||||
this.item = item;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public static <T> SignalMessage<T> onNext(T item) {
|
||||
return new SignalMessage<>(SignalType.ITEM, Objects.requireNonNull(item), null);
|
||||
}
|
||||
|
||||
public static <T> SignalMessage<T> onError(Throwable throwable) {
|
||||
return new SignalMessage<T>(SignalType.ERROR, null, Objects.requireNonNull(throwable.getMessage()));
|
||||
}
|
||||
|
||||
static <T> SignalMessage<T> onDecodedError(String throwable) {
|
||||
return new SignalMessage<T>(SignalType.ERROR, null, Objects.requireNonNull(throwable));
|
||||
}
|
||||
|
||||
public static <T> SignalMessage<T> onComplete() {
|
||||
return new SignalMessage<T>(SignalType.COMPLETE, null, null);
|
||||
}
|
||||
|
||||
public SignalType getSignalType() {
|
||||
return signalType;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return Objects.requireNonNull(errorMessage);
|
||||
}
|
||||
|
||||
public T getItem() {
|
||||
return Objects.requireNonNull(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", SignalMessage.class.getSimpleName() + "[", "]")
|
||||
.add("signalType=" + signalType)
|
||||
.add("item=" + item)
|
||||
.add("errorMessage='" + errorMessage + "'")
|
||||
.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SignalMessage<?> that = (SignalMessage<?>) o;
|
||||
|
||||
if (signalType != that.signalType) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(item, that.item)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(errorMessage, that.errorMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = signalType != null ? signalType.hashCode() : 0;
|
||||
result = 31 * result + (item != null ? item.hashCode() : 0);
|
||||
result = 31 * result + (errorMessage != null ? errorMessage.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
package it.tdlight.tdlibsession;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
import it.tdlight.utils.VertxBufferInputStream;
|
||||
import it.tdlight.utils.VertxBufferOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.warp.commonutils.stream.SafeDataInputStream;
|
||||
import org.warp.commonutils.stream.SafeDataOutputStream;
|
||||
|
||||
public class SignalMessageCodec<T> implements MessageCodec<SignalMessage<T>, SignalMessage<T>> {
|
||||
|
||||
private final String codecName;
|
||||
private final MessageCodec<T, T> typeCodec;
|
||||
|
||||
public SignalMessageCodec(MessageCodec<T, T> typeCodec) {
|
||||
super();
|
||||
this.codecName = "SignalCodec-" + typeCodec.name();
|
||||
this.typeCodec = typeCodec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encodeToWire(Buffer buffer, SignalMessage<T> t) {
|
||||
try (var bos = new VertxBufferOutputStream(buffer)) {
|
||||
try (var dos = new SafeDataOutputStream(bos)) {
|
||||
switch (t.getSignalType()) {
|
||||
case ITEM:
|
||||
dos.writeByte(0x01);
|
||||
break;
|
||||
case ERROR:
|
||||
dos.writeByte(0x02);
|
||||
break;
|
||||
case COMPLETE:
|
||||
dos.writeByte(0x03);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected value: " + t.getSignalType());
|
||||
}
|
||||
}
|
||||
switch (t.getSignalType()) {
|
||||
case ITEM:
|
||||
typeCodec.encodeToWire(buffer, t.getItem());
|
||||
break;
|
||||
case ERROR:
|
||||
var stringBytes = t.getErrorMessage().getBytes(StandardCharsets.UTF_8);
|
||||
buffer.appendInt(stringBytes.length);
|
||||
buffer.appendBytes(stringBytes);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignalMessage<T> decodeFromWire(int pos, Buffer buffer) {
|
||||
try (var fis = new VertxBufferInputStream(buffer, pos)) {
|
||||
try (var dis = new SafeDataInputStream(fis)) {
|
||||
switch (dis.readByte()) {
|
||||
case 0x01:
|
||||
return SignalMessage.onNext(typeCodec.decodeFromWire(pos + 1, buffer));
|
||||
case 0x02:
|
||||
var size = dis.readInt();
|
||||
return SignalMessage.onDecodedError(new String(dis.readNBytes(size), StandardCharsets.UTF_8));
|
||||
case 0x03:
|
||||
return SignalMessage.onComplete();
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected value: " + dis.readByte());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignalMessage<T> transform(SignalMessage<T> t) {
|
||||
// If a message is sent *locally* across the event bus.
|
||||
// This sends message just as is
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return codecName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte systemCodecID() {
|
||||
// Always -1
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package it.tdlight.tdlibsession;
|
||||
|
||||
enum SignalType {
|
||||
COMPLETE, ERROR, ITEM
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user