Compare commits

...

153 Commits

Author SHA1 Message Date
Andrea Cavalli
cbb70a07b3 Add burst filter 2023-01-24 19:51:13 +01:00
Andrea Cavalli
78d98b5b41 Update tdlib 2023-01-17 16:27:01 +01:00
Andrea Cavalli
f667a99076 Update byte buddy 2023-01-05 12:48:39 +01:00
Andrea Cavalli
29fdd55405 Update tdlib 2023-01-03 22:34:47 +01:00
Andrea Cavalli
bf77691f86 Update tdlib 2023-01-02 17:18:57 +01:00
Andrea Cavalli
70fb94df97 Update tdlib 2023-01-02 17:14:14 +01:00
Andrea Cavalli
ffa33ac046 Update tdlight 2023-01-02 01:58:04 +01:00
Andrea Cavalli
75d4791f98 Update reactor 2022-12-21 01:05:49 +01:00
Andrea Cavalli
072be4c40f Update tdlib 2022-12-15 17:52:09 +01:00
Andrea Cavalli
99eaebc622 Update tdlib 2022-11-25 23:32:27 +01:00
Andrea Cavalli
84ad72f4cb Update tdlib 2022-11-16 01:53:33 +01:00
Andrea Cavalli
0b76d5ada4 Fix cluster manager 2022-11-10 13:15:50 +01:00
Andrea Cavalli
a14f8e1f0e Allow the use of local sockets 2022-11-09 18:56:28 +01:00
Andrea Cavalli
e5712361e0 fix 2022-10-31 14:06:25 +01:00
Andrea Cavalli
c5d3627970 Update dependencies 2022-10-24 01:17:58 +02:00
Andrea Cavalli
88f68f96f3 Update logging 2022-10-17 15:30:22 +02:00
Andrea Cavalli
e27ffc8d92 Remove encryption key phase 2022-10-17 13:26:53 +02:00
Andrea Cavalli
29cb9f6078 Update 2022-10-17 13:04:34 +02:00
Andrea Cavalli
dd70f2492c Fix missing serialized data 2022-10-13 12:40:30 +02:00
Andrea Cavalli
677ceb70a1 Fix some connection errors 2022-10-12 18:31:44 +02:00
Andrea Cavalli
58770ca649 Improve support for local cancellation 2022-10-12 14:31:41 +02:00
Andrea Cavalli
171f07ccec Code cleanup 2022-10-11 20:08:47 +02:00
Andrea Cavalli
24b4387b08 Accept multiple tdlib clients 2022-10-11 20:08:40 +02:00
Andrea Cavalli
a8510ed336 Use LMDB queues 2022-10-11 00:24:51 +02:00
Andrea Cavalli
abf9f28484 Improve queues 2022-10-10 20:30:32 +02:00
Andrea Cavalli
708fcbd1e4 Store resulting events and events into a disk queue 2022-10-10 01:05:53 +02:00
Andrea Cavalli
59027e8e62 Code cleanup 2022-10-08 13:44:32 +02:00
Andrea Cavalli
1eb1d7b95f Code cleanup 2022-10-08 03:12:50 +02:00
Andrea Cavalli
edb8cec873 Remove buffering 2022-10-07 17:50:34 +02:00
Andrea Cavalli
3bed3052d0 Remove required kafka dependency, zero-copy deserialization 2022-10-07 16:03:51 +02:00
Andrea Cavalli
705e5ca65e Sockets are working 2022-10-07 00:48:10 +02:00
Andrea Cavalli
0a74e1ab1a Fix sockets 2022-10-06 19:06:35 +02:00
Andrea Cavalli
5dc543f090 Improve tests 2022-10-06 00:36:00 +02:00
Andrea Cavalli
ba093e4c27 rsocket 2022-10-05 03:37:24 +02:00
Andrea Cavalli
03b8cfa579 Add RSocket 2022-10-05 02:26:30 +02:00
Andrea Cavalli
a93d6d4e24 Code cleanup 2022-10-04 12:43:24 +02:00
Andrea Cavalli
116e082d56 Fix resubscription failures 2022-09-30 15:25:06 +02:00
Andrea Cavalli
2b2e690da4 Code cleanup 2022-09-22 16:26:55 +02:00
Andrea Cavalli
b322400d87 Main lane 2022-09-22 16:05:56 +02:00
Andrea Cavalli
83c064220f Implement lanes 2022-09-22 15:46:31 +02:00
Andrea Cavalli
dfc393c953 Retries 2022-09-22 03:01:59 +02:00
Andrea Cavalli
41be43d711 Bugfixes 2022-09-22 02:26:22 +02:00
Andrea Cavalli
c36812e052 Update log4j2 2022-09-22 01:12:56 +02:00
Andrea Cavalli
e85a1de781 Update tdlib 2022-09-13 23:29:03 +02:00
Andrea Cavalli
8651ce3e97 Fix performance problems 2022-09-13 22:15:18 +02:00
Andrea Cavalli
5e40530a20 Bugfixes 2022-09-11 17:36:15 +02:00
Andrea Cavalli
83613b2d01 Quick/Slow response mode, no acks, filtering instead of grouping 2022-09-10 23:12:35 +02:00
Andrea Cavalli
bd463a74d2 Single consumers and producers 2022-09-10 20:25:54 +02:00
Andrea Cavalli
e9cbfaaa39 Fix pom 2022-09-10 20:25:35 +02:00
Andrea Cavalli
aa12013479 Optimize consumer 2022-09-10 13:36:38 +02:00
Andrea Cavalli
0055c54826 Update dependencies 2022-09-07 23:30:11 +02:00
Andrea Cavalli
202a90846b Update kafka producer 2022-09-07 15:29:46 +02:00
Andrea Cavalli
a77442bae5 More detailed logs 2022-09-05 23:31:39 +02:00
Andrea Cavalli
d2f74d7bbb Update commit batch size 2022-07-28 23:40:26 +02:00
Andrea Cavalli
fd658e5e6c Retry commit failures 2022-07-25 22:10:24 +02:00
Andrea Cavalli
3eea163a73 Update example 2022-07-23 15:26:49 +02:00
Andrea Cavalli
e41ee220cd Disable reactor agent by default 2022-07-05 00:47:13 +02:00
Andrea Cavalli
ab080b9838 Update tdlib 2022-07-04 20:03:33 +02:00
Andrea Cavalli
4d8e5fd3be Update bytebuddy 2022-06-30 15:07:14 +02:00
Andrea Cavalli
d931217e81 Implement timeouts properly 2022-06-28 00:11:34 +02:00
Andrea Cavalli
07c22d39f2 Avoid possible leaks 2022-06-27 00:32:02 +02:00
Andrea Cavalli
6339f78db8 Migrate from Atomix to Kafka 2022-06-27 00:06:53 +02:00
Andrea Cavalli
77593c2722 Update tdlight 2022-06-14 13:10:54 +02:00
Andrea Cavalli
cf724d4b51 Update tdlib 2022-06-08 01:38:40 +02:00
Andrea Cavalli
84bee0da20 Load chats on boot 2022-05-26 16:21:56 +02:00
Andrea Cavalli
79a0344921 Update reactor 2022-05-21 15:30:01 +02:00
Andrea Cavalli
cdf077fddd Bugfix 2022-05-11 20:22:25 +02:00
Andrea Cavalli
aa18865601 Update tdlight 2022-05-11 10:21:40 +02:00
Andrea Cavalli
ca536b788b Update tdlight 2022-05-11 10:02:51 +02:00
Andrea Cavalli
8ab21d8d7a Update tdlib 2022-05-11 09:49:57 +02:00
Andrea Cavalli
da1bab9adc Update dependencies 2022-05-10 00:34:09 +02:00
Andrea Cavalli
faeda1b693 Update log4j 2022-05-09 22:10:04 +02:00
Andrea Cavalli
5282a9d1cd Reformat code 2022-05-09 11:20:20 +02:00
Andrea Cavalli
cc3bdc76c5 Include version 2022-05-05 20:25:00 +02:00
Andrea Cavalli
272f7dfe75 Update tdlight 2022-05-05 01:05:56 +02:00
Andrea Cavalli
be144b9d1b Update pom.xml 2022-04-27 10:52:42 +02:00
Andrea Cavalli
b44fb2b20c Update record builder 2022-04-13 19:48:36 +02:00
Andrea Cavalli
2a390a60bb Update dependencies 2022-04-11 16:42:53 +02:00
Andrea Cavalli
afa70c75d9 Fix syntax 2022-04-09 23:51:56 +02:00
Andrea Cavalli
7b25092fa8 Java modules 2022-04-09 02:48:01 +02:00
Andrea Cavalli
8645231031 Improve kafka logging, update tdlib 2022-03-21 01:08:12 +01:00
Andrea Cavalli
5cf00542da Initialize client, print errors 2022-03-19 01:36:24 +01:00
Andrea Cavalli
c92f0aa589 Update dependencies 2022-03-19 00:06:30 +01:00
Andrea Cavalli
9a2015ef56 Update reactor 2022-03-18 16:21:00 +01:00
Andrea Cavalli
830cc51012 Update tdlib 2022-03-16 22:21:51 +01:00
Andrea Cavalli
d7ffd8bfa2 Use netty 5 alpha 2022-03-16 13:53:01 +01:00
Andrea Cavalli
cf6d9875d4 Update tdlib 2022-03-16 13:00:24 +01:00
Andrea Cavalli
a3c27a76aa Update record builder 2022-03-11 18:00:22 +01:00
Andrea Cavalli
e61f5d04b9 Update boringssl 2022-03-07 01:41:40 +01:00
Andrea Cavalli
5e2773a23e Merge branch 'old' 2022-03-06 17:13:15 +01:00
Andrea Cavalli
958d04519b Fix event transformers 2022-03-06 13:27:29 +01:00
Andrea Cavalli
0d5d4fb6a2 Merge branch 'old' 2022-03-06 13:21:20 +01:00
Andrea Cavalli
2527682d00 Add EnableMinithumbnails 2022-03-06 13:21:08 +01:00
Andrea Cavalli
9d92d7b015 Update reactor 2022-03-02 12:38:03 +01:00
Andrea Cavalli
eb2ccf6b04 Update utils 2022-02-21 01:00:49 +01:00
Andrea Cavalli
941ff5bc87 Update tdlight 2022-02-19 19:49:24 +01:00
Andrea Cavalli
84910b7488 Fastutil 2022-02-12 00:19:04 +01:00
Andrea Cavalli
aa887ba954 Highly optimize topic name 2022-01-23 21:57:43 +01:00
Andrea Cavalli
344be2b320 Bugfix 2022-01-23 14:52:08 +01:00
Andrea Cavalli
68e904681d Fix broken completable futures 2022-01-23 12:58:10 +01:00
Andrea Cavalli
ee6a0534a8 Add disableLog 2022-01-22 23:22:04 +01:00
Andrea Cavalli
5991b116f3 Log seen users 2022-01-22 20:24:35 +01:00
Andrea Cavalli
3a74997b49 Optimize dynamic live id resolution 2022-01-22 17:45:56 +01:00
Andrea Cavalli
76ba67b760 Fix NPE 2022-01-22 13:08:11 +01:00
Andrea Cavalli
797808114c Clean code 2022-01-21 22:25:47 +01:00
Andrea Cavalli
d4615b2cb4 Bugfixes 2022-01-21 19:54:53 +01:00
Andrea Cavalli
e1fee1f90d Fix requests handling 2022-01-21 19:45:46 +01:00
Andrea Cavalli
7e166b0920 Use custom database format and fix broken keys 2022-01-21 19:11:52 +01:00
Andrea Cavalli
f903035643 Restart jitter to avoid crashes 2022-01-20 19:57:43 +01:00
Andrea Cavalli
79bfd5d95c Don't print stacktrace if the bot is not found 2022-01-19 23:53:02 +01:00
Andrea Cavalli
101e9a814e Increase default timeout 2022-01-16 19:04:43 +01:00
Andrea Cavalli
788101aa0f Request timeouts 2022-01-16 15:55:28 +01:00
Andrea Cavalli
2e21f765ab Reduce duplicate code 2022-01-14 20:04:29 +01:00
Andrea Cavalli
2d0ab31fd0 Do not create infinite topics 2022-01-14 19:32:33 +01:00
Andrea Cavalli
48fbca5fad Bugfixes 2022-01-14 16:33:54 +01:00
Andrea Cavalli
4c4b7a3677 Bugfixes 2022-01-14 00:58:35 +01:00
Andrea Cavalli
3dd6241e2c Improve kafka grouping 2022-01-13 16:19:10 +01:00
Andrea Cavalli
a140e7a2b1 Configure kafka 2022-01-13 11:20:44 +01:00
Andrea Cavalli
006974ba23 Don't window records 2022-01-13 03:00:21 +01:00
Andrea Cavalli
f48a1d321b Implement reactor-kafka for updates 2022-01-13 01:59:26 +01:00
Andrea Cavalli
799fd4149c Add backpressure 2022-01-12 21:36:41 +01:00
Andrea Cavalli
be89c549ef Update reactor 2022-01-12 18:19:40 +01:00
Andrea Cavalli
3cd57bf61f Fix scheduling 2022-01-11 19:59:27 +01:00
Andrea Cavalli
37d3355ca4 Update pom 2022-01-11 16:52:21 +01:00
Andrea Cavalli
07c1e6c836 Bugfixes 2022-01-11 16:00:56 +01:00
Andrea Cavalli
735fccf043 Send multiple events together 2022-01-11 01:45:39 +01:00
Andrea Cavalli
e723cc6d98 Update host 2022-01-11 00:21:26 +01:00
Andrea Cavalli
602a63fad1 Update directory structure 2022-01-10 22:58:16 +01:00
Andrea Cavalli
2156ec9ed7 Check if the client is really closed 2022-01-09 20:50:58 +01:00
Andrea Cavalli
fd0bfda2eb Clean live sessions list 2022-01-09 20:32:27 +01:00
Andrea Cavalli
172c770524 Implement periodic restarter 2022-01-09 20:20:20 +01:00
Andrea Cavalli
5b9fec980e Implement event transformers 2022-01-09 18:27:14 +01:00
Andrea Cavalli
4bbb9cd762 Bugfixes 2022-01-08 18:13:40 +01:00
Andrea Cavalli
473783b501 Implement live and dynamic clients 2022-01-07 23:54:18 +01:00
Andrea Cavalli
af96cfb7dc Implement client 2022-01-07 12:21:41 +01:00
Andrea Cavalli
ede105a6ea Fix compilation issue 2022-01-07 11:18:21 +01:00
Andrea Cavalli
fd48777071 Update dependencies 2022-01-07 11:09:40 +01:00
Andrea Cavalli
e44df86246 Update tdlight 2021-12-11 17:19:06 +01:00
Andrea Cavalli
e76a596b85 Update log4j, tdlib, lucene 2021-12-11 13:21:09 +01:00
Andrea Cavalli
430dbeb261 Handle errors 2021-12-10 16:25:18 +01:00
Andrea Cavalli
ee19a97b00 Complete login phase 2021-12-10 02:23:19 +01:00
Andrea Cavalli
8b0220ccfc Update dependencies and move libraryversion 2021-12-10 02:23:01 +01:00
Andrea Cavalli
64cd9d4a9e Improve logging 2021-12-10 02:22:47 +01:00
Andrea Cavalli
a33a7f676a Create sessions using reactor core 2021-12-09 18:48:06 +01:00
Andrea Cavalli
da61270350 Rewrite requests management 2021-12-09 18:15:06 +01:00
Andrea Cavalli
f0d5706d77 Update TdApi 2021-12-08 11:53:39 +01:00
Andrea Cavalli
815876e7da Partially implement the api publisher 2021-12-07 02:25:01 +01:00
Andrea Cavalli
07c6bd1140 Rewrite local sessions management 2021-12-05 23:47:54 +01:00
Andrea Cavalli
2dc4a35d9f Rewrite using atomix 2021-12-05 15:15:28 +01:00
Andrea Cavalli
c3912c2edf Update 2021-11-30 09:29:04 +01:00
Andrea Cavalli
c7f696706b Update 2021-11-24 16:39:51 +01:00
Andrea Cavalli
251ee4951a Bugfixes 2021-11-09 15:54:28 +01:00
Andrea Cavalli
0bb4856c7e Bugfixes 2021-11-09 12:49:28 +01:00
173 changed files with 6748 additions and 6172 deletions

View File

@ -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
View File

@ -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>

View File

@ -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}";
}

View 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());
}
}

View 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
}

View File

@ -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();
});
}
}

View 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;
}
}

View 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);
}
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View 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();
}
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,8 @@
package it.tdlight.reactiveapi;
import java.util.Set;
public interface ChannelsParameters {
Set<String> getAllLanes();
}

View 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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
package it.tdlight.reactiveapi;
import it.tdlight.reactiveapi.ResultingEvent.ClientBoundResultingEvent;
public class ClientBoundResultingEventSerializer implements Serializer<ClientBoundResultingEvent> {
}

View File

@ -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);
};
}
}

View File

@ -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);
}
}
}

View 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));
}
}
}

View File

@ -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");
}
}
}
}

View File

@ -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) {
}

View 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);
}
}

View 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");
}
}
}

View 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;
}
}

View File

@ -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);
}
}

View 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();
}
}

View 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();
}
}

View 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();
}

View 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();
}

View 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;
}
}

View File

@ -0,0 +1,6 @@
package it.tdlight.reactiveapi;
public enum InstanceType {
UPDATES_CONSUMER,
TDLIB
}

View 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;
}
}

View File

@ -0,0 +1,5 @@
package it.tdlight.reactiveapi;
public class Lanes {
public static final String MAIN_LANE = "main";
}

View File

@ -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)));
}
}

View 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();
}
}

View 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();
}

View 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();
}

View File

@ -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;
}
};
}
}

View 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();
}
}
}

View 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 ReactiveApiThinClient {
<T extends TdApi.Object> Mono<T> request(TdApi.Function<T> request, Instant timeout);
long getUserId();
}

View 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();
}
}
}

View 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 {}
}

View File

@ -0,0 +1,9 @@
package it.tdlight.reactiveapi;
import reactor.core.publisher.Flux;
public interface ResultingEventTransformer {
Flux<ResultingEvent> transform(boolean isBot, Flux<ResultingEvent> events);
}

View 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);
}
}

View File

@ -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;
}
}

View 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));
}
}

View 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;
}
}

View File

@ -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();
}
}
}

View 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;
}
}

View 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) {
}
}
}

View File

@ -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;

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View 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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}
}

View 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);
}
}

View File

@ -0,0 +1,3 @@
package it.tdlight.reactiveapi;
public record Timestamped<T>(long timestamp, T data) {}

View 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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,3 @@
package it.tdlight.reactiveapi;
public record UserIdAndLiveId(long userId, long liveId) {}

View 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);
}
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
};
}
}

View File

@ -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();
}
}
}

View File

@ -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));
}
}

View File

@ -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 {}
}

View File

@ -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());
}
}
}

View File

@ -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);
}

View File

@ -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();
}
});
}
}

View File

@ -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());
}
}

View File

@ -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)));
}
}

View File

@ -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);
});
}
}

View File

@ -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);
});
}
}

View File

@ -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);
});
}
}

View File

@ -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);
});
}
}

View File

@ -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;
}
}

View File

@ -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);
}
});
}
}

View File

@ -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);
});
}
}

View File

@ -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)));
}
}

View File

@ -1,5 +0,0 @@
package it.tdlight.tdlibsession;
public enum FatalErrorType {
ACCESS_TOKEN_INVALID, PHONE_NUMBER_INVALID, CONNECTION_KILLED, INVALID_UPDATE, PHONE_NUMBER_BANNED
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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