Rewrite using atomix
This commit is contained in:
parent
c3912c2edf
commit
2dc4a35d9f
163
pom.xml
163
pom.xml
@ -3,23 +3,16 @@
|
||||
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>
|
||||
<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>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
|
||||
<vertx.version>4.2.0</vertx.version>
|
||||
<atomix.version>3.0.6</atomix.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>
|
||||
@ -51,51 +44,68 @@
|
||||
</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>io.atomix</groupId>
|
||||
<artifactId>atomix</artifactId>
|
||||
<version>${atomix.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.atomix</groupId>
|
||||
<artifactId>atomix-raft</artifactId>
|
||||
<version>${atomix.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.atomix</groupId>
|
||||
<artifactId>atomix-primary-backup</artifactId>
|
||||
<version>${atomix.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>it.tdlight</groupId>
|
||||
<artifactId>tdlight-java</artifactId>
|
||||
<version>2.7.9.4</version>
|
||||
<version>2.7.9.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
<version>3.4.12</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>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
<version>2.13.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.minecrell</groupId>
|
||||
<artifactId>terminalconsoleappender</artifactId>
|
||||
<version>1.3.0</version>
|
||||
</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>
|
||||
<version>5.8.2</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
@ -107,48 +117,24 @@
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.8.1</version>
|
||||
<version>5.8.2</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>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.akaita.java</groupId>
|
||||
<artifactId>rxjava2-debug</artifactId>
|
||||
<version>1.4.0</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>
|
||||
@ -158,38 +144,39 @@
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-tcnative-boringssl-static</artifactId>
|
||||
<version>2.0.45.Final</version>
|
||||
<version>2.0.46.Final</version>
|
||||
</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>io.atomix</groupId>
|
||||
<artifactId>atomix</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.atomix</groupId>
|
||||
<artifactId>atomix-raft</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.atomix</groupId>
|
||||
<artifactId>atomix-primary-backup</artifactId>
|
||||
</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>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.minecrell</groupId>
|
||||
<artifactId>terminalconsoleappender</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<resources>
|
||||
@ -224,7 +211,7 @@
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<release>11</release>
|
||||
<release>17</release>
|
||||
<useIncrementalCompilation>false</useIncrementalCompilation>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
104
src/main/java/it/tdlight/reactiveapi/Cli.java
Normal file
104
src/main/java/it/tdlight/reactiveapi/Cli.java
Normal file
@ -0,0 +1,104 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import io.atomix.core.Atomix;
|
||||
import io.atomix.core.AtomixBuilder;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.CreateBotSessionRequest;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.CreateUserSessionRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.minecrell.terminalconsole.SimpleTerminalConsole;
|
||||
import org.jline.reader.LineReader;
|
||||
import org.jline.reader.LineReaderBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Cli {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Cli.class);
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
var validArgs = Entrypoint.parseArguments(args);
|
||||
var atomixBuilder = Atomix.builder();
|
||||
var api = Entrypoint.start(validArgs, atomixBuilder);
|
||||
|
||||
AtomicBoolean alreadyShutDown = new AtomicBoolean(false);
|
||||
AtomicBoolean acceptInputs = new AtomicBoolean(true);
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
acceptInputs.set(false);
|
||||
if (alreadyShutDown.compareAndSet(false, true)) {
|
||||
api.getAtomix().stop().join();
|
||||
}
|
||||
}));
|
||||
|
||||
var console = new SimpleTerminalConsole() {
|
||||
|
||||
private static final Set<String> commands = Set.of("exit", "stop", "createsession", "help", "man", "?");
|
||||
|
||||
@Override
|
||||
protected LineReader buildReader(LineReaderBuilder builder) {
|
||||
var reader = super.buildReader(builder);
|
||||
reader.addCommandsInBuffer(commands);
|
||||
return reader;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isRunning() {
|
||||
return acceptInputs.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void runCommand(String command) {
|
||||
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" -> acceptInputs.set(false);
|
||||
case "createsession" -> createSession(api, commandArgs);
|
||||
case "help", "?", "man" -> LOG.info("Commands: {}", commands);
|
||||
default -> LOG.info("Unknown command \"{}\"", command);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void shutdown() {
|
||||
acceptInputs.set(false);
|
||||
if (alreadyShutDown.compareAndSet(false, true)) {
|
||||
api.getAtomix().stop().join();
|
||||
}
|
||||
}
|
||||
};
|
||||
console.start();
|
||||
}
|
||||
|
||||
private static void createSession(ReactiveApi api, String commandArgs) {
|
||||
var parts = commandArgs.split(" ");
|
||||
boolean invalid = false;
|
||||
if (parts.length == 3) {
|
||||
CreateSessionRequest request = switch (parts[0]) {
|
||||
case "bot" -> new CreateBotSessionRequest(Long.parseLong(parts[1]), parts[2]);
|
||||
case "user" -> new CreateUserSessionRequest(Long.parseLong(parts[1]),
|
||||
Long.parseLong(parts[2]));
|
||||
default -> {
|
||||
invalid = true;
|
||||
yield null;
|
||||
}
|
||||
};
|
||||
if (!invalid) {
|
||||
CreateSessionResponse response = api.createSession(request).join();
|
||||
LOG.info("Created a session with session id \"{}\"", response.sessionId());
|
||||
}
|
||||
} else {
|
||||
invalid = true;
|
||||
}
|
||||
if (invalid) {
|
||||
LOG.error("Syntax: CreateSession <\"bot\"|\"user\"> <userid> <token|phoneNumber>");
|
||||
}
|
||||
}
|
||||
}
|
21
src/main/java/it/tdlight/reactiveapi/ClusterSettings.java
Normal file
21
src/main/java/it/tdlight/reactiveapi/ClusterSettings.java
Normal file
@ -0,0 +1,21 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Define the cluster structure
|
||||
*/
|
||||
public class ClusterSettings {
|
||||
|
||||
public String id;
|
||||
public List<NodeSettings> nodes;
|
||||
|
||||
@JsonCreator
|
||||
public ClusterSettings(@JsonProperty(required = true, value = "id") String id,
|
||||
@JsonProperty(required = true, value = "nodes") List<NodeSettings> nodes) {
|
||||
this.id = id;
|
||||
this.nodes = nodes;
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.CreateBotSessionRequest;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.CreateUserSessionRequest;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.LoadSessionFromDiskRequest;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import org.apache.commons.lang3.SerializationException;
|
||||
|
||||
public sealed interface CreateSessionRequest permits CreateUserSessionRequest, CreateBotSessionRequest,
|
||||
LoadSessionFromDiskRequest {
|
||||
|
||||
long userId();
|
||||
|
||||
static CreateSessionRequest deserializeBytes(byte[] bytes) {
|
||||
byte type = bytes[0];
|
||||
long userId = Longs.fromBytes(bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8]);
|
||||
return switch (type) {
|
||||
case 0 -> new CreateUserSessionRequest(userId,
|
||||
Longs.fromBytes(bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16])
|
||||
);
|
||||
case 1 -> new CreateBotSessionRequest(userId,
|
||||
new String(bytes, 1 + Long.BYTES + Integer.BYTES, Ints.fromBytes(bytes[9], bytes[10], bytes[11], bytes[12]))
|
||||
);
|
||||
case 2 -> {
|
||||
var dis = new DataInputStream(new ByteArrayInputStream(bytes, 1 + Long.BYTES, bytes.length - (1 + Long.BYTES)));
|
||||
try {
|
||||
var pathName = dis.readUTF();
|
||||
var isBot = dis.readBoolean();
|
||||
String token;
|
||||
Long phoneNumber;
|
||||
if (isBot) {
|
||||
token = dis.readUTF();
|
||||
phoneNumber = null;
|
||||
} else {
|
||||
token = null;
|
||||
phoneNumber = dis.readLong();
|
||||
}
|
||||
yield new LoadSessionFromDiskRequest(userId, pathName, token, phoneNumber);
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException(e);
|
||||
}
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected value: " + type);
|
||||
};
|
||||
}
|
||||
|
||||
final record CreateUserSessionRequest(long userId, long phoneNumber) implements CreateSessionRequest {}
|
||||
|
||||
final record CreateBotSessionRequest(long userId, String token) implements CreateSessionRequest {}
|
||||
|
||||
final record LoadSessionFromDiskRequest(long userId, String pathName, String token, Long phoneNumber) implements
|
||||
CreateSessionRequest {
|
||||
|
||||
public LoadSessionFromDiskRequest {
|
||||
if ((token == null) == (phoneNumber == null)) {
|
||||
throw new IllegalArgumentException("This must be either a bot or an user");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
public record CreateSessionResponse(long sessionId) {
|
||||
|
||||
public static byte[] serializeBytes(CreateSessionResponse createSessionResponse) {
|
||||
return Longs.toByteArray(createSessionResponse.sessionId);
|
||||
}
|
||||
}
|
33
src/main/java/it/tdlight/reactiveapi/DiskSession.java
Normal file
33
src/main/java/it/tdlight/reactiveapi/DiskSession.java
Normal file
@ -0,0 +1,33 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
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 org.jetbrains.annotations.Nullable;
|
||||
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public class DiskSession {
|
||||
|
||||
public long userId;
|
||||
@Nullable
|
||||
public String token;
|
||||
@Nullable
|
||||
public Long phoneNumber;
|
||||
|
||||
@JsonCreator
|
||||
public DiskSession(@JsonProperty(required = true, value = "userId") long userId,
|
||||
@JsonProperty("token") @Nullable String token,
|
||||
@JsonProperty("phoneNumber") @Nullable Long phoneNumber) {
|
||||
this.userId = userId;
|
||||
this.token = token;
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.validate();
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
if ((token == null) == (phoneNumber == null)) {
|
||||
throw new UnsupportedOperationException("You must set either a token or a phone number");
|
||||
}
|
||||
}
|
||||
}
|
26
src/main/java/it/tdlight/reactiveapi/DiskSessions.java
Normal file
26
src/main/java/it/tdlight/reactiveapi/DiskSessions.java
Normal file
@ -0,0 +1,26 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class DiskSessions {
|
||||
|
||||
@NotNull
|
||||
public String path;
|
||||
|
||||
/**
|
||||
* key: session folder name
|
||||
*/
|
||||
@NotNull
|
||||
public Map<String, DiskSession> sessions;
|
||||
|
||||
@JsonCreator
|
||||
public DiskSessions(@JsonProperty(required = true, value = "path") @NotNull String path,
|
||||
@JsonProperty(required = true, value = "sessions") @NotNull Map<String, DiskSession> sessions) {
|
||||
this.path = path;
|
||||
this.sessions = sessions;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class DiskSessionsManager {
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
private final DiskSessions diskSessionsSettings;
|
||||
private final File file;
|
||||
|
||||
public DiskSessionsManager(ObjectMapper mapper, String diskSessionsConfigPath) throws IOException {
|
||||
this.mapper = mapper;
|
||||
this.file = Paths.get(diskSessionsConfigPath).toFile();
|
||||
diskSessionsSettings = mapper.readValue(file, DiskSessions.class);
|
||||
}
|
||||
|
||||
public DiskSessions getSettings() {
|
||||
return diskSessionsSettings;
|
||||
}
|
||||
|
||||
public synchronized void save() throws IOException {
|
||||
mapper.writeValue(file, diskSessionsSettings);
|
||||
}
|
||||
}
|
131
src/main/java/it/tdlight/reactiveapi/Entrypoint.java
Normal file
131
src/main/java/it/tdlight/reactiveapi/Entrypoint.java
Normal file
@ -0,0 +1,131 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import io.atomix.cluster.Node;
|
||||
import io.atomix.cluster.discovery.BootstrapDiscoveryProvider;
|
||||
import io.atomix.core.Atomix;
|
||||
import io.atomix.core.AtomixBuilder;
|
||||
import io.atomix.core.profile.Profile;
|
||||
import io.atomix.protocols.backup.partition.PrimaryBackupPartitionGroup;
|
||||
import io.atomix.protocols.raft.partition.RaftPartitionGroup;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Entrypoint {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Entrypoint.class);
|
||||
|
||||
public static 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, AtomixBuilder atomixBuilder) 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 = new DiskSessionsManager(mapper, diskSessionsConfigPath);
|
||||
}
|
||||
|
||||
atomixBuilder.withClusterId(clusterSettings.id);
|
||||
|
||||
if (instanceSettings.client) {
|
||||
atomixBuilder.withMemberId(instanceSettings.id).withAddress(instanceSettings.clientAddress);
|
||||
} else {
|
||||
// Find node settings
|
||||
var nodeSettingsOptional = clusterSettings.nodes
|
||||
.stream()
|
||||
.filter(node -> node.id.equals(instanceSettings.id))
|
||||
.findAny();
|
||||
|
||||
// Check node settings presence
|
||||
if (nodeSettingsOptional.isEmpty()) {
|
||||
System.err.printf("Node id \"%s\" has not been described in cluster.yaml nodes list%n", instanceSettings.id);
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
var nodeSettings = nodeSettingsOptional.get();
|
||||
|
||||
atomixBuilder.withMemberId(instanceSettings.id).withAddress(nodeSettings.address);
|
||||
}
|
||||
|
||||
var bootstrapDiscoveryProviderNodes = new ArrayList<Node>();
|
||||
List<String> systemPartitionGroupMembers = new ArrayList<>();
|
||||
for (NodeSettings node : clusterSettings.nodes) {
|
||||
bootstrapDiscoveryProviderNodes.add(Node.builder().withId(node.id).withAddress(node.address).build());
|
||||
systemPartitionGroupMembers.add(node.id);
|
||||
}
|
||||
|
||||
var bootstrapDiscoveryProviderBuilder = BootstrapDiscoveryProvider.builder();
|
||||
bootstrapDiscoveryProviderBuilder.withNodes(bootstrapDiscoveryProviderNodes).build();
|
||||
|
||||
atomixBuilder.withMembershipProvider(bootstrapDiscoveryProviderBuilder.build());
|
||||
|
||||
atomixBuilder.withManagementGroup(RaftPartitionGroup
|
||||
.builder("system")
|
||||
.withNumPartitions(1)
|
||||
.withMembers(systemPartitionGroupMembers)
|
||||
.build());
|
||||
|
||||
atomixBuilder.withPartitionGroups(PrimaryBackupPartitionGroup.builder("data").withNumPartitions(32).build());
|
||||
|
||||
atomixBuilder.withShutdownHook(false);
|
||||
atomixBuilder.withTypeRegistrationRequired();
|
||||
|
||||
if (instanceSettings.client) {
|
||||
atomixBuilder.addProfile(Profile.consensus(systemPartitionGroupMembers));
|
||||
atomixBuilder.addProfile(Profile.dataGrid(32));
|
||||
} else {
|
||||
atomixBuilder.addProfile(Profile.client());
|
||||
}
|
||||
|
||||
atomixBuilder.withCompatibleSerialization(false);
|
||||
|
||||
var atomix = atomixBuilder.build();
|
||||
|
||||
TdSerializer.register(atomix.getSerializationService());
|
||||
|
||||
atomix.start().join();
|
||||
|
||||
var api = new ReactiveApi(atomix, diskSessions);
|
||||
|
||||
LOG.info("Starting ReactiveApi...");
|
||||
|
||||
api.start();
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
var validArgs = parseArguments(args);
|
||||
var atomixBuilder = Atomix.builder().withShutdownHookEnabled();
|
||||
start(validArgs, atomixBuilder);
|
||||
}
|
||||
}
|
47
src/main/java/it/tdlight/reactiveapi/Event.java
Normal file
47
src/main/java/it/tdlight/reactiveapi/Event.java
Normal file
@ -0,0 +1,47 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.reactiveapi.Event.AuthenticatedEvent;
|
||||
|
||||
/**
|
||||
* Any event received from a session
|
||||
*/
|
||||
public sealed interface Event permits AuthenticatedEvent {
|
||||
|
||||
/**
|
||||
*
|
||||
* @return temporary unique identifier of the session
|
||||
*/
|
||||
long sessionId();
|
||||
|
||||
/**
|
||||
* Event received after choosing the user id of the session
|
||||
*/
|
||||
sealed interface AuthenticatedEvent extends Event permits OnLoginCodeRequested, OnUpdate {
|
||||
|
||||
/**
|
||||
*
|
||||
* @return telegram user id of the session
|
||||
*/
|
||||
long userId();
|
||||
}
|
||||
|
||||
/**
|
||||
* TDLib is asking for an authorization code
|
||||
*/
|
||||
sealed interface OnLoginCodeRequested extends AuthenticatedEvent
|
||||
permits OnBotLoginCodeRequested, OnUserLoginCodeRequested {}
|
||||
|
||||
final record OnUserLoginCodeRequested(long sessionId, long userId, long phoneNumber) implements OnLoginCodeRequested {}
|
||||
|
||||
final record OnBotLoginCodeRequested(long sessionId, long userId, String token) implements OnLoginCodeRequested {}
|
||||
|
||||
/**
|
||||
* Event received from TDLib
|
||||
*/
|
||||
sealed interface OnUpdate extends AuthenticatedEvent permits OnUpdateData, OnUpdateError {}
|
||||
|
||||
final record OnUpdateData(long sessionId, long userId, TdApi.Update update) implements OnUpdate {}
|
||||
|
||||
final record OnUpdateError(long sessionId, long userId, TdApi.Error error) implements OnUpdate {}
|
||||
}
|
33
src/main/java/it/tdlight/reactiveapi/InstanceSettings.java
Normal file
33
src/main/java/it/tdlight/reactiveapi/InstanceSettings.java
Normal file
@ -0,0 +1,33 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class InstanceSettings {
|
||||
|
||||
@NotNull
|
||||
public String id;
|
||||
|
||||
/**
|
||||
* True if this is just a client, false if this is a complete node
|
||||
* <p>
|
||||
* A client is a lightweight node
|
||||
*/
|
||||
public boolean client;
|
||||
|
||||
/**
|
||||
* If {@link #client} is true, this will be the address of this client
|
||||
*/
|
||||
public @Nullable String clientAddress;
|
||||
|
||||
@JsonCreator
|
||||
public InstanceSettings(@JsonProperty(required = true, value = "id") @NotNull String id,
|
||||
@JsonProperty(required = true, value = "client") boolean client,
|
||||
@JsonProperty("clientAddress") @Nullable String clientAddress) {
|
||||
this.id = id;
|
||||
this.client = client;
|
||||
this.clientAddress = clientAddress;
|
||||
}
|
||||
}
|
18
src/main/java/it/tdlight/reactiveapi/NodeSettings.java
Normal file
18
src/main/java/it/tdlight/reactiveapi/NodeSettings.java
Normal file
@ -0,0 +1,18 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty.Access;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class NodeSettings {
|
||||
|
||||
public @NotNull String id;
|
||||
public @NotNull String address;
|
||||
|
||||
public NodeSettings(@JsonProperty(required = true, value = "id") @NotNull String id,
|
||||
@JsonProperty(required = true, value = "address") @NotNull String address) {
|
||||
this.id = id;
|
||||
this.address = address;
|
||||
}
|
||||
}
|
199
src/main/java/it/tdlight/reactiveapi/ReactiveApi.java
Normal file
199
src/main/java/it/tdlight/reactiveapi/ReactiveApi.java
Normal file
@ -0,0 +1,199 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import static java.util.concurrent.CompletableFuture.completedFuture;
|
||||
import static java.util.concurrent.CompletableFuture.failedFuture;
|
||||
|
||||
import io.atomix.core.Atomix;
|
||||
import io.atomix.core.idgenerator.AsyncAtomicIdGenerator;
|
||||
import io.atomix.core.lock.AsyncAtomicLock;
|
||||
import io.atomix.core.map.AsyncAtomicMap;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.CreateBotSessionRequest;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.CreateUserSessionRequest;
|
||||
import it.tdlight.reactiveapi.CreateSessionRequest.LoadSessionFromDiskRequest;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class ReactiveApi {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ReactiveApi.class);
|
||||
|
||||
private final Atomix atomix;
|
||||
private static final SchedulerExecutor SCHEDULER_EXECUTOR = new SchedulerExecutor(Schedulers.parallel());
|
||||
private static final SchedulerExecutor BOUNDED_ELASTIC_EXECUTOR = new SchedulerExecutor(Schedulers.boundedElastic());
|
||||
private final AsyncAtomicIdGenerator nextSessionId;
|
||||
|
||||
private final AsyncAtomicLock sessionModificationLock;
|
||||
private final AsyncAtomicMap<Long, Long> sessionIdToUserId;
|
||||
private final ConcurrentMap<Long, ReactiveApiPublisher> localNodeSessions = new ConcurrentHashMap<>();
|
||||
private final DiskSessionsManager diskSessions;
|
||||
|
||||
public ReactiveApi(Atomix atomix, DiskSessionsManager diskSessions) {
|
||||
this.atomix = atomix;
|
||||
this.nextSessionId = atomix.getAtomicIdGenerator("session-id").async();
|
||||
this.sessionIdToUserId = atomix.<Long, Long>getAtomicMap("session-id-to-user-id").async();
|
||||
this.sessionModificationLock = atomix.getAtomicLock("session-modification").async();
|
||||
this.diskSessions = diskSessions;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
List<CompletableFuture<CreateSessionResponse>> requests = new ArrayList<>();
|
||||
synchronized (diskSessions) {
|
||||
for (Entry<String, DiskSession> entry : diskSessions.getSettings().sessions.entrySet()) {
|
||||
try {
|
||||
entry.getValue().validate();
|
||||
} catch (Throwable ex) {
|
||||
LOG.error("Failed to load disk session {}", entry.getKey(), ex);
|
||||
}
|
||||
var sessionFolderName = entry.getKey();
|
||||
var diskSession = entry.getValue();
|
||||
requests.add(createSession(new LoadSessionFromDiskRequest(diskSession.userId,
|
||||
sessionFolderName,
|
||||
diskSession.token,
|
||||
diskSession.phoneNumber
|
||||
)));
|
||||
}
|
||||
}
|
||||
CompletableFuture
|
||||
.allOf(requests.toArray(CompletableFuture<?>[]::new))
|
||||
.thenAccept(responses -> LOG.info("Loaded all saved sessions from disk"));
|
||||
}, BOUNDED_ELASTIC_EXECUTOR);
|
||||
|
||||
// Listen for create-session signals
|
||||
atomix.getEventService().subscribe("create-session", CreateSessionRequest::deserializeBytes, req -> {
|
||||
if (req instanceof LoadSessionFromDiskRequest) {
|
||||
return failedFuture(new IllegalArgumentException("Can't pass a local request through the cluster"));
|
||||
} else {
|
||||
return createSession(req);
|
||||
}
|
||||
}, CreateSessionResponse::serializeBytes);
|
||||
}
|
||||
|
||||
public CompletableFuture<CreateSessionResponse> createSession(CreateSessionRequest req) {
|
||||
// Lock sessions creation
|
||||
return sessionModificationLock.lock().thenCompose(lockVersion -> {
|
||||
// Generate session id
|
||||
return this.nextFreeId().thenCompose(sessionId -> {
|
||||
// Create the session instance
|
||||
ReactiveApiPublisher reactiveApiPublisher;
|
||||
boolean loadedFromDisk;
|
||||
long userId;
|
||||
String botToken;
|
||||
Long phoneNumber;
|
||||
if (req instanceof CreateBotSessionRequest createBotSessionRequest) {
|
||||
loadedFromDisk = false;
|
||||
userId = createBotSessionRequest.userId();
|
||||
botToken = createBotSessionRequest.token();
|
||||
phoneNumber = null;
|
||||
reactiveApiPublisher = ReactiveApiPublisher.fromToken(atomix, sessionId, userId, botToken);
|
||||
} else if (req instanceof CreateUserSessionRequest createUserSessionRequest) {
|
||||
loadedFromDisk = false;
|
||||
userId = createUserSessionRequest.userId();
|
||||
botToken = null;
|
||||
phoneNumber = createUserSessionRequest.phoneNumber();
|
||||
reactiveApiPublisher = ReactiveApiPublisher.fromPhoneNumber(atomix, sessionId, userId, phoneNumber);
|
||||
} else if (req instanceof LoadSessionFromDiskRequest loadSessionFromDiskRequest) {
|
||||
loadedFromDisk = true;
|
||||
userId = loadSessionFromDiskRequest.userId();
|
||||
botToken = loadSessionFromDiskRequest.token();
|
||||
phoneNumber = loadSessionFromDiskRequest.phoneNumber();
|
||||
if (loadSessionFromDiskRequest.phoneNumber() != null) {
|
||||
reactiveApiPublisher = ReactiveApiPublisher.fromPhoneNumber(atomix, sessionId, userId, phoneNumber);
|
||||
} else {
|
||||
reactiveApiPublisher = ReactiveApiPublisher.fromToken(atomix, sessionId, userId, botToken);
|
||||
}
|
||||
} else {
|
||||
return failedFuture(new UnsupportedOperationException("Unexpected value: " + req));
|
||||
}
|
||||
|
||||
// Register the session instance to the local nodes map
|
||||
var prev = localNodeSessions.put(sessionId, reactiveApiPublisher);
|
||||
if (prev != null) {
|
||||
LOG.error("Session id \"{}\" was already registered locally!", sessionId);
|
||||
}
|
||||
|
||||
// Register the session instance to the distributed nodes map
|
||||
return sessionIdToUserId.put(sessionId, req.userId()).thenComposeAsync(prevDistributed -> {
|
||||
if (prevDistributed != null) {
|
||||
LOG.error("Session id \"{}\" was already registered in the cluster!", sessionId);
|
||||
}
|
||||
|
||||
CompletableFuture<?> saveToDiskFuture;
|
||||
if (!loadedFromDisk) {
|
||||
// Load existing session paths
|
||||
HashSet<String> alreadyExistingPaths = new HashSet<>();
|
||||
synchronized (diskSessions) {
|
||||
for (var entry : diskSessions.getSettings().sessions.entrySet()) {
|
||||
var path = entry.getKey();
|
||||
var diskSessionSettings = entry.getValue();
|
||||
if (diskSessionSettings.userId == userId) {
|
||||
LOG.warn("User id \"{}\" session already exists in path: \"{}\"", userId, path);
|
||||
}
|
||||
alreadyExistingPaths.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
// Get a new disk session folder name
|
||||
String diskSessionFolderName;
|
||||
do {
|
||||
diskSessionFolderName = UUID.randomUUID().toString();
|
||||
} while (alreadyExistingPaths.contains(diskSessionFolderName));
|
||||
|
||||
// Create the disk session configuration
|
||||
var diskSession = new DiskSession(userId, botToken, phoneNumber);
|
||||
Path path;
|
||||
synchronized (diskSessions) {
|
||||
diskSessions.getSettings().sessions.put(diskSessionFolderName, diskSession);
|
||||
path = Paths.get(diskSessions.getSettings().path).resolve(diskSessionFolderName);
|
||||
}
|
||||
|
||||
// Start the session instance
|
||||
reactiveApiPublisher.start(path);
|
||||
|
||||
saveToDiskFuture = CompletableFuture.runAsync(() -> {
|
||||
// Save updated sessions configuration to disk
|
||||
try {
|
||||
synchronized (diskSessions) {
|
||||
diskSessions.save();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new CompletionException("Failed to save disk sessions configuration", e);
|
||||
}
|
||||
}, BOUNDED_ELASTIC_EXECUTOR);
|
||||
} else {
|
||||
saveToDiskFuture = completedFuture(null);
|
||||
}
|
||||
|
||||
return saveToDiskFuture.thenApply(ignored -> new CreateSessionResponse(sessionId));
|
||||
}, BOUNDED_ELASTIC_EXECUTOR);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<Long> nextFreeId() {
|
||||
return nextSessionId.nextId().thenCompose(id -> sessionIdToUserId.containsKey(id).thenCompose(exists -> {
|
||||
if (exists) {
|
||||
return nextFreeId();
|
||||
} else {
|
||||
return completedFuture(id);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public Atomix getAtomix() {
|
||||
return atomix;
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import io.atomix.core.Atomix;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.apache.commons.lang3.SerializationException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxSink.OverflowStrategy;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class ReactiveApiPublisher {
|
||||
|
||||
private static final SchedulerExecutor SCHEDULER_EXECUTOR = new SchedulerExecutor(Schedulers.boundedElastic());
|
||||
|
||||
private final Atomix atomix;
|
||||
private final long userId;
|
||||
private final long sessionId;
|
||||
private final String botToken;
|
||||
private final Long phoneNumber;
|
||||
|
||||
private ReactiveApiPublisher(Atomix atomix, long sessionId, long userId, String botToken, Long phoneNumber) {
|
||||
this.atomix = atomix;
|
||||
this.userId = userId;
|
||||
this.sessionId = sessionId;
|
||||
this.botToken = botToken;
|
||||
this.phoneNumber = phoneNumber;
|
||||
}
|
||||
|
||||
public static ReactiveApiPublisher fromToken(Atomix atomix, Long sessionId, long userId, String token) {
|
||||
return new ReactiveApiPublisher(atomix, sessionId, userId, token, null);
|
||||
}
|
||||
|
||||
public static ReactiveApiPublisher fromPhoneNumber(Atomix atomix, Long sessionId, long userId, long phoneNumber) {
|
||||
return new ReactiveApiPublisher(atomix, sessionId, userId, null, phoneNumber);
|
||||
}
|
||||
|
||||
public void start(Path path) {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
public class ReactiveApiSubscriber {
|
||||
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
|
||||
/**
|
||||
* {@link #sessionUuid} changes every time a session is restarted
|
||||
*/
|
||||
public record ReactiveApiUpdate(long sessionUuid, TdApi.Object update) {}
|
19
src/main/java/it/tdlight/reactiveapi/SchedulerExecutor.java
Normal file
19
src/main/java/it/tdlight/reactiveapi/SchedulerExecutor.java
Normal file
@ -0,0 +1,19 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
63
src/main/java/it/tdlight/reactiveapi/TdSerializer.java
Normal file
63
src/main/java/it/tdlight/reactiveapi/TdSerializer.java
Normal file
@ -0,0 +1,63 @@
|
||||
package it.tdlight.reactiveapi;
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo;
|
||||
import com.esotericsoftware.kryo.Serializer;
|
||||
import com.esotericsoftware.kryo.io.Input;
|
||||
import com.esotericsoftware.kryo.io.KryoDataInput;
|
||||
import com.esotericsoftware.kryo.io.KryoDataOutput;
|
||||
import com.esotericsoftware.kryo.io.Output;
|
||||
import io.atomix.primitive.serialization.SerializationService;
|
||||
import it.tdlight.common.ConstructorDetector;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.commons.lang3.SerializationException;
|
||||
|
||||
public class TdSerializer extends Serializer<TdApi.Object> {
|
||||
|
||||
private TdSerializer() {
|
||||
|
||||
}
|
||||
|
||||
public static void register(SerializationService serializationService) {
|
||||
var serializerBuilder = serializationService.newBuilder("TdApi");
|
||||
var tdApiClasses = TdApi.class.getDeclaredClasses();
|
||||
// Add types
|
||||
Class<?>[] classes = Stream
|
||||
.of(tdApiClasses)
|
||||
.filter(clazz -> clazz.isAssignableFrom(TdApi.Object.class))
|
||||
.toArray(Class<?>[]::new);
|
||||
|
||||
serializerBuilder.addSerializer(new TdSerializer(), classes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(Kryo kryo, Output output, TdApi.Object object) {
|
||||
try {
|
||||
object.serialize(new KryoDataOutput(output));
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TdApi.Object read(Kryo kryo, Input input, Class<TdApi.Object> type) {
|
||||
try {
|
||||
return TdApi.Deserializer.deserialize(new KryoDataInput(input));
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static TdApi.Object deserializeBytes(byte[] bytes) {
|
||||
var din = new DataInputStream(new ByteArrayInputStream(bytes));
|
||||
try {
|
||||
return TdApi.Deserializer.deserialize(din);
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package it.tdlight.tdlibsession;
|
||||
|
||||
public enum FatalErrorType {
|
||||
ACCESS_TOKEN_INVALID, PHONE_NUMBER_INVALID, CONNECTION_KILLED, INVALID_UPDATE, PHONE_NUMBER_BANNED
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package it.tdlight.tdlibsession;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
class SignalMessage<T> {
|
||||
|
||||
private final SignalType signalType;
|
||||
private final T item;
|
||||
private final String errorMessage;
|
||||
|
||||
private SignalMessage(SignalType signalType, T item, String errorMessage) {
|
||||
this.signalType = signalType;
|
||||
this.item = item;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public static <T> SignalMessage<T> onNext(T item) {
|
||||
return new SignalMessage<>(SignalType.ITEM, Objects.requireNonNull(item), null);
|
||||
}
|
||||
|
||||
public static <T> SignalMessage<T> onError(Throwable throwable) {
|
||||
return new SignalMessage<T>(SignalType.ERROR, null, Objects.requireNonNull(throwable.getMessage()));
|
||||
}
|
||||
|
||||
static <T> SignalMessage<T> onDecodedError(String throwable) {
|
||||
return new SignalMessage<T>(SignalType.ERROR, null, Objects.requireNonNull(throwable));
|
||||
}
|
||||
|
||||
public static <T> SignalMessage<T> onComplete() {
|
||||
return new SignalMessage<T>(SignalType.COMPLETE, null, null);
|
||||
}
|
||||
|
||||
public SignalType getSignalType() {
|
||||
return signalType;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return Objects.requireNonNull(errorMessage);
|
||||
}
|
||||
|
||||
public T getItem() {
|
||||
return Objects.requireNonNull(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", SignalMessage.class.getSimpleName() + "[", "]")
|
||||
.add("signalType=" + signalType)
|
||||
.add("item=" + item)
|
||||
.add("errorMessage='" + errorMessage + "'")
|
||||
.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SignalMessage<?> that = (SignalMessage<?>) o;
|
||||
|
||||
if (signalType != that.signalType) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(item, that.item)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(errorMessage, that.errorMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = signalType != null ? signalType.hashCode() : 0;
|
||||
result = 31 * result + (item != null ? item.hashCode() : 0);
|
||||
result = 31 * result + (errorMessage != null ? errorMessage.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
package it.tdlight.tdlibsession;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
import it.tdlight.utils.VertxBufferInputStream;
|
||||
import it.tdlight.utils.VertxBufferOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.warp.commonutils.stream.SafeDataInputStream;
|
||||
import org.warp.commonutils.stream.SafeDataOutputStream;
|
||||
|
||||
public class SignalMessageCodec<T> implements MessageCodec<SignalMessage<T>, SignalMessage<T>> {
|
||||
|
||||
private final String codecName;
|
||||
private final MessageCodec<T, T> typeCodec;
|
||||
|
||||
public SignalMessageCodec(MessageCodec<T, T> typeCodec) {
|
||||
super();
|
||||
this.codecName = "SignalCodec-" + typeCodec.name();
|
||||
this.typeCodec = typeCodec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encodeToWire(Buffer buffer, SignalMessage<T> t) {
|
||||
try (var bos = new VertxBufferOutputStream(buffer)) {
|
||||
try (var dos = new SafeDataOutputStream(bos)) {
|
||||
switch (t.getSignalType()) {
|
||||
case ITEM:
|
||||
dos.writeByte(0x01);
|
||||
break;
|
||||
case ERROR:
|
||||
dos.writeByte(0x02);
|
||||
break;
|
||||
case COMPLETE:
|
||||
dos.writeByte(0x03);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected value: " + t.getSignalType());
|
||||
}
|
||||
}
|
||||
switch (t.getSignalType()) {
|
||||
case ITEM:
|
||||
typeCodec.encodeToWire(buffer, t.getItem());
|
||||
break;
|
||||
case ERROR:
|
||||
var stringBytes = t.getErrorMessage().getBytes(StandardCharsets.UTF_8);
|
||||
buffer.appendInt(stringBytes.length);
|
||||
buffer.appendBytes(stringBytes);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignalMessage<T> decodeFromWire(int pos, Buffer buffer) {
|
||||
try (var fis = new VertxBufferInputStream(buffer, pos)) {
|
||||
try (var dis = new SafeDataInputStream(fis)) {
|
||||
switch (dis.readByte()) {
|
||||
case 0x01:
|
||||
return SignalMessage.onNext(typeCodec.decodeFromWire(pos + 1, buffer));
|
||||
case 0x02:
|
||||
var size = dis.readInt();
|
||||
return SignalMessage.onDecodedError(new String(dis.readNBytes(size), StandardCharsets.UTF_8));
|
||||
case 0x03:
|
||||
return SignalMessage.onComplete();
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected value: " + dis.readByte());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignalMessage<T> transform(SignalMessage<T> t) {
|
||||
// If a message is sent *locally* across the event bus.
|
||||
// This sends message just as is
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return codecName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte systemCodecID() {
|
||||
// Always -1
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package it.tdlight.tdlibsession;
|
||||
|
||||
enum SignalType {
|
||||
COMPLETE, ERROR, ITEM
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package it.tdlight.tdlibsession;
|
||||
|
||||
public class VariableWrapper<T> {
|
||||
|
||||
public volatile T var;
|
||||
|
||||
public VariableWrapper(T value) {
|
||||
this.var = value;
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
package it.tdlight.tdlibsession.remoteclient;
|
||||
|
||||
public class BinlogManager {}
|
@ -1,7 +0,0 @@
|
||||
package it.tdlight.tdlibsession.remoteclient;
|
||||
|
||||
public enum DeployClientResult {
|
||||
DEPLOYED,
|
||||
IGNORED,
|
||||
FAILED
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package it.tdlight.tdlibsession.remoteclient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class RemoteClientBotAddresses {
|
||||
|
||||
private final LinkedHashSet<String> addresses;
|
||||
private final LinkedHashSet<String> tempAddresses;
|
||||
private final Path addressesFilePath;
|
||||
|
||||
public RemoteClientBotAddresses(Path addressesFilePath) throws IOException {
|
||||
this.addressesFilePath = addressesFilePath;
|
||||
if (Files.notExists(addressesFilePath)) {
|
||||
Files.createFile(addressesFilePath);
|
||||
}
|
||||
tempAddresses = new LinkedHashSet<>();
|
||||
addresses = Files
|
||||
.readAllLines(addressesFilePath, StandardCharsets.UTF_8)
|
||||
.stream()
|
||||
.filter(address -> !address.isBlank())
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
public synchronized void putAddress(String address) throws IOException {
|
||||
tempAddresses.remove(address);
|
||||
addresses.add(address);
|
||||
Files.write(addressesFilePath, addresses, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.SYNC);
|
||||
}
|
||||
|
||||
public synchronized void putTempAddress(String address) {
|
||||
tempAddresses.add(address);
|
||||
}
|
||||
|
||||
public synchronized void removeAddress(String address) throws IOException {
|
||||
tempAddresses.remove(address);
|
||||
addresses.remove(address);
|
||||
Files.write(addressesFilePath, addresses, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.SYNC);
|
||||
}
|
||||
|
||||
public synchronized boolean has(String botAddress) {
|
||||
return addresses.contains(botAddress) || tempAddresses.contains(botAddress);
|
||||
}
|
||||
|
||||
public synchronized Set<String> values() {
|
||||
return new HashSet<>(addresses);
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
package it.tdlight.tdlibsession.remoteclient;
|
||||
|
||||
import io.vertx.core.file.FileSystemException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public class SecurityInfo {
|
||||
|
||||
private final Path keyStorePath;
|
||||
private final Path keyStorePasswordPath;
|
||||
private final Path trustStorePath;
|
||||
private final Path trustStorePasswordPath;
|
||||
|
||||
public SecurityInfo(Path keyStorePath, Path keyStorePasswordPath, Path trustStorePath, Path trustStorePasswordPath) {
|
||||
this.keyStorePath = keyStorePath;
|
||||
this.keyStorePasswordPath = keyStorePasswordPath;
|
||||
this.trustStorePath = trustStorePath;
|
||||
this.trustStorePasswordPath = trustStorePasswordPath;
|
||||
}
|
||||
|
||||
public Path getKeyStorePath() {
|
||||
return keyStorePath;
|
||||
}
|
||||
|
||||
public Path getKeyStorePasswordPath() {
|
||||
return keyStorePasswordPath;
|
||||
}
|
||||
|
||||
public String getKeyStorePassword(boolean required) {
|
||||
try {
|
||||
if (Files.isReadable(keyStorePasswordPath) && Files.size(keyStorePasswordPath) >= 6) {
|
||||
return Files.readString(keyStorePasswordPath, StandardCharsets.UTF_8).split("\n")[0];
|
||||
} else if (required) {
|
||||
throw new NoSuchElementException("No keystore password is set on '" + keyStorePasswordPath.toString() + "'");
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new FileSystemException(ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Path getTrustStorePath() {
|
||||
return trustStorePath;
|
||||
}
|
||||
|
||||
public Path getTrustStorePasswordPath() {
|
||||
return trustStorePasswordPath;
|
||||
}
|
||||
|
||||
public String getTrustStorePassword(boolean required) {
|
||||
try {
|
||||
if (Files.isReadable(trustStorePasswordPath) && Files.size(trustStorePasswordPath) >= 6) {
|
||||
return Files.readString(trustStorePasswordPath, StandardCharsets.UTF_8).split("\n")[0];
|
||||
} else if (required) {
|
||||
throw new NoSuchElementException("No truststore password is set on '" + trustStorePasswordPath.toString() + "'");
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new FileSystemException(ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", SecurityInfo.class.getSimpleName() + "[", "]")
|
||||
.add("keyStorePath=" + keyStorePath)
|
||||
.add("keyStorePasswordPath=" + keyStorePasswordPath)
|
||||
.add("trustStorePath=" + trustStorePath)
|
||||
.add("trustStorePasswordPath=" + trustStorePasswordPath)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
package it.tdlight.tdlibsession.remoteclient;
|
||||
|
||||
import com.akaita.java.rxjava2debug.RxJava2Debug;
|
||||
import io.vertx.core.DeploymentOptions;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.core.net.JksOptions;
|
||||
import io.vertx.reactivex.core.eventbus.Message;
|
||||
import io.vertx.reactivex.core.eventbus.MessageConsumer;
|
||||
import it.tdlight.common.Init;
|
||||
import it.tdlight.common.Log;
|
||||
import it.tdlight.common.utils.CantLoadLibrary;
|
||||
import it.tdlight.tdlibsession.td.middle.StartSessionMessage;
|
||||
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
|
||||
import it.tdlight.tdlibsession.td.middle.server.AsyncTdMiddleEventBusServer;
|
||||
import it.tdlight.tdnative.NativeLog;
|
||||
import it.tdlight.utils.BinlogUtils;
|
||||
import it.tdlight.utils.MonoUtils;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.warp.commonutils.log.Logger;
|
||||
import org.warp.commonutils.log.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.One;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.tools.agent.ReactorDebugAgent;
|
||||
|
||||
public class TDLibRemoteClient implements AutoCloseable {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TDLibRemoteClient.class);
|
||||
|
||||
@Nullable
|
||||
private final SecurityInfo securityInfo;
|
||||
private final String masterHostname;
|
||||
private final String netInterface;
|
||||
private final int port;
|
||||
private final Set<String> membersAddresses;
|
||||
private final AtomicReference<TdClusterManager> clusterManager = new AtomicReference<>();
|
||||
|
||||
public TDLibRemoteClient(@Nullable SecurityInfo securityInfo,
|
||||
String masterHostname,
|
||||
String netInterface,
|
||||
int port,
|
||||
Set<String> membersAddresses,
|
||||
boolean enableAsyncStacktraces,
|
||||
boolean enableFullAsyncStacktraces) {
|
||||
this.securityInfo = securityInfo;
|
||||
this.masterHostname = masterHostname;
|
||||
this.netInterface = netInterface;
|
||||
this.port = port;
|
||||
this.membersAddresses = membersAddresses;
|
||||
|
||||
if (enableAsyncStacktraces && enableFullAsyncStacktraces) {
|
||||
RxJava2Debug.enableRxJava2AssemblyTracking(new String[]{"it.tdlight.utils", "it.tdlight.tdlibsession"});
|
||||
}
|
||||
|
||||
try {
|
||||
Init.start();
|
||||
//noinspection deprecation
|
||||
Log.setVerbosityLevel(2);
|
||||
} catch (CantLoadLibrary ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws URISyntaxException {
|
||||
if (args.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
String masterHostname = args[0];
|
||||
|
||||
String[] interfaceAndPort = args[1].split(":", 2);
|
||||
|
||||
String netInterface = interfaceAndPort[0];
|
||||
|
||||
int port = Integer.parseInt(interfaceAndPort[1]);
|
||||
|
||||
Set<String> membersAddresses = Set.of(args[2].split(","));
|
||||
|
||||
Path keyStorePath = Paths.get(args[3]);
|
||||
Path keyStorePasswordPath = Paths.get(args[4]);
|
||||
Path trustStorePath = Paths.get(args[5]);
|
||||
Path trustStorePasswordPath = Paths.get(args[6]);
|
||||
boolean enableAsyncStacktraces = Boolean.parseBoolean(args[7]);
|
||||
boolean enableFullAsyncStacktraces = Boolean.parseBoolean(args[8]);
|
||||
|
||||
var loggerContext = (org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false);
|
||||
loggerContext.setConfigLocation(Objects
|
||||
.requireNonNull(TDLibRemoteClient.class.getResource("/tdlib-session-container-log4j2.xml"),
|
||||
"tdlib-session-container-log4j2.xml doesn't exist")
|
||||
.toURI());
|
||||
|
||||
var securityInfo = new SecurityInfo(keyStorePath, keyStorePasswordPath, trustStorePath, trustStorePasswordPath);
|
||||
|
||||
var client = new TDLibRemoteClient(securityInfo,
|
||||
masterHostname,
|
||||
netInterface,
|
||||
port,
|
||||
membersAddresses,
|
||||
enableAsyncStacktraces,
|
||||
enableFullAsyncStacktraces
|
||||
);
|
||||
|
||||
client
|
||||
.start()
|
||||
.block();
|
||||
|
||||
// Close vert.x on shutdown
|
||||
var vertxMono = Mono.fromCallable(() -> client.clusterManager.get().getVertx());
|
||||
Runtime
|
||||
.getRuntime()
|
||||
.addShutdownHook(new Thread(() -> vertxMono
|
||||
.flatMap(vertx -> MonoUtils.toMono(vertx.rxClose()))
|
||||
.blockOptional())
|
||||
);
|
||||
}
|
||||
|
||||
public Mono<Void> start() {
|
||||
var ksp = securityInfo == null ? null : securityInfo.getKeyStorePassword(false);
|
||||
var keyStoreOptions = securityInfo == null || ksp == null ? null : new JksOptions()
|
||||
.setPath(securityInfo.getKeyStorePath().toAbsolutePath().toString())
|
||||
.setPassword(ksp);
|
||||
|
||||
var tsp = securityInfo == null ? null : securityInfo.getTrustStorePassword(false);
|
||||
var trustStoreOptions = securityInfo == null || tsp == null ? null : new JksOptions()
|
||||
.setPath(securityInfo.getTrustStorePath().toAbsolutePath().toString())
|
||||
.setPassword(tsp);
|
||||
|
||||
return MonoUtils
|
||||
.fromBlockingEmpty(() -> {
|
||||
// Set verbosity level here, before creating the bots
|
||||
if (Files.notExists(Paths.get("logs"))) {
|
||||
try {
|
||||
Files.createDirectory(Paths.get("logs"));
|
||||
} catch (FileAlreadyExistsException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"TDLib remote client is being hosted on" + netInterface + ":" + port + ". Master: " + masterHostname);
|
||||
logger.info(
|
||||
"TDLib remote client SSL enabled: " + (keyStoreOptions != null && trustStoreOptions != null));
|
||||
})
|
||||
.then(TdClusterManager.ofNodes(keyStoreOptions,
|
||||
trustStoreOptions,
|
||||
false,
|
||||
masterHostname,
|
||||
netInterface,
|
||||
port,
|
||||
membersAddresses
|
||||
))
|
||||
.doOnSuccess(clusterManager::set)
|
||||
.single()
|
||||
.doOnError(ex -> logger.error("Failed to set cluster manager", ex))
|
||||
.flatMap(clusterManager -> {
|
||||
MessageConsumer<StartSessionMessage> startBotConsumer
|
||||
= clusterManager.getEventBus().consumer("bots.start-bot");
|
||||
|
||||
return this.listenForStartBotsCommand(
|
||||
clusterManager,
|
||||
MonoUtils.fromReplyableMessageConsumer(Mono.empty(), startBotConsumer)
|
||||
);
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
private Mono<Void> listenForStartBotsCommand(TdClusterManager clusterManager,
|
||||
Flux<Message<StartSessionMessage>> messages) {
|
||||
return MonoUtils
|
||||
.fromBlockingEmpty(() -> messages
|
||||
.flatMapSequential(msg -> {
|
||||
StartSessionMessage req = msg.body();
|
||||
DeploymentOptions deploymentOptions = clusterManager
|
||||
.newDeploymentOpts()
|
||||
.setConfig(new JsonObject()
|
||||
.put("botId", req.id())
|
||||
.put("botAlias", req.alias())
|
||||
.put("local", false)
|
||||
.put("implementationDetails", req.implementationDetails()));
|
||||
var verticle = new AsyncTdMiddleEventBusServer();
|
||||
|
||||
// Binlog path
|
||||
var sessPath = getSessionDirectory(req.id());
|
||||
var mediaPath = getMediaDirectory(req.id());
|
||||
var blPath = getSessionBinlogDirectory(req.id());
|
||||
|
||||
return BinlogUtils
|
||||
.chooseBinlog(clusterManager.getVertx().fileSystem(), blPath, req.binlog(), req.binlogDate())
|
||||
.then(BinlogUtils.cleanSessionPath(clusterManager.getVertx().fileSystem(), blPath, sessPath, mediaPath))
|
||||
.then(clusterManager.getVertx().rxDeployVerticle(verticle, deploymentOptions).as(MonoUtils::toMono))
|
||||
.then(MonoUtils.fromBlockingEmpty(() -> msg.reply(new byte[0])))
|
||||
.onErrorResume(ex -> {
|
||||
msg.fail(500, "Failed to deploy bot verticle: " + ex.getMessage());
|
||||
logger.error("Failed to deploy bot verticle", ex);
|
||||
return Mono.empty();
|
||||
});
|
||||
})
|
||||
.subscribeOn(Schedulers.parallel())
|
||||
.subscribe(
|
||||
v -> {},
|
||||
ex -> logger.error("Bots starter activity crashed. From now on, no new bots can be started anymore", ex)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static Path getSessionDirectory(long botId) {
|
||||
return Paths.get(".sessions-cache").resolve("id" + botId);
|
||||
}
|
||||
|
||||
public static Path getMediaDirectory(long botId) {
|
||||
return Paths.get(".cache").resolve("media").resolve("id" + botId);
|
||||
}
|
||||
|
||||
public static Path getSessionBinlogDirectory(long botId) {
|
||||
return getSessionDirectory(botId).resolve("td.binlog");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.clusterManager.get().getVertx().rxClose().blockingAwait();
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import java.time.Duration;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ReactorTelegramClient {
|
||||
|
||||
Mono<Void> initialize();
|
||||
|
||||
Flux<Object> receive();
|
||||
|
||||
<T extends TdApi.Object> Mono<TdApi.Object> send(TdApi.Function<T> query, Duration timeout);
|
||||
|
||||
<T extends TdApi.Object> TdApi.Object execute(TdApi.Function<T> query);
|
||||
}
|
@ -1,148 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class ResponseError extends IOException {
|
||||
|
||||
@NotNull
|
||||
private final String botName;
|
||||
@NotNull
|
||||
private final String tag;
|
||||
private final int code;
|
||||
@NotNull
|
||||
private final String message;
|
||||
|
||||
private ResponseError(@NotNull Function<?> function, @NotNull String botName, @NotNull TdApi.Error tdError, @Nullable Throwable cause) {
|
||||
super("Bot '" + botName + "' failed the request '" + functionToInlineString(function) + "': " + tdError.code + " " + tdError.message, cause);
|
||||
this.botName = botName;
|
||||
this.tag = functionToInlineString(function);
|
||||
this.code = tdError.code;
|
||||
this.message = tdError.message;
|
||||
}
|
||||
|
||||
private ResponseError(@NotNull String tag, @NotNull String botName, @NotNull TdApi.Error tdError, @Nullable Throwable cause) {
|
||||
super("Bot '" + botName + "' failed the request '" + tag + "': " + tdError.code + " " + tdError.message, cause);
|
||||
this.botName = botName;
|
||||
this.tag = tag;
|
||||
this.code = tdError.code;
|
||||
this.message = tdError.message;
|
||||
}
|
||||
|
||||
private ResponseError(@NotNull Function<?> function, @NotNull String botName, @Nullable Throwable cause) {
|
||||
super("Bot '" + botName + "' failed the request '" + functionToInlineString(function) + "': " + (cause == null ? null : cause.getMessage()), cause);
|
||||
this.botName = botName;
|
||||
this.tag = functionToInlineString(function);
|
||||
this.code = 500;
|
||||
this.message = (cause == null ? "" : (cause.getMessage() == null ? "" : cause.getMessage()));
|
||||
}
|
||||
|
||||
private ResponseError(@NotNull String tag, @NotNull String botName, @Nullable Throwable cause) {
|
||||
super("Bot '" + botName + "' failed the request '" + tag + "': " + (cause == null ? null : cause.getMessage()), cause);
|
||||
this.botName = botName;
|
||||
this.tag = tag;
|
||||
this.code = 500;
|
||||
this.message = (cause == null ? "" : (cause.getMessage() == null ? "" : cause.getMessage()));
|
||||
}
|
||||
|
||||
public static ResponseError newResponseError(@NotNull Function<?> function,
|
||||
@NotNull String botName,
|
||||
@NotNull TdApi.Error tdError,
|
||||
@Nullable Throwable cause) {
|
||||
return new ResponseError(function, botName, tdError, cause);
|
||||
}
|
||||
|
||||
public static ResponseError newResponseError(@NotNull String tag,
|
||||
@NotNull String botName,
|
||||
@NotNull TdApi.Error tdError,
|
||||
@Nullable Throwable cause) {
|
||||
return new ResponseError(tag, botName, tdError, cause);
|
||||
}
|
||||
|
||||
public static ResponseError newResponseError(@NotNull String tag,
|
||||
@NotNull String botName,
|
||||
@NotNull TdApi.Error tdError,
|
||||
@Nullable TdError cause) {
|
||||
return new ResponseError(tag, botName, tdError, cause);
|
||||
}
|
||||
|
||||
public static ResponseError newResponseError(@NotNull Function<?> function,
|
||||
@NotNull String botName,
|
||||
@Nullable Throwable cause) {
|
||||
return new ResponseError(function, botName, cause);
|
||||
}
|
||||
|
||||
public static ResponseError newResponseError(@NotNull String tag,
|
||||
@NotNull String botName,
|
||||
@Nullable Throwable cause) {
|
||||
return new ResponseError(tag, botName, cause);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static <T> T get(@NotNull Function<?> function, @NotNull String botName, CompletableFuture<T> action) throws ResponseError {
|
||||
try {
|
||||
return action.get();
|
||||
} catch (InterruptedException e) {
|
||||
throw ResponseError.newResponseError(function, botName, e);
|
||||
} catch (ExecutionException executionException) {
|
||||
if (executionException.getCause() instanceof ResponseError) {
|
||||
throw (ResponseError) executionException.getCause();
|
||||
} else {
|
||||
throw ResponseError.newResponseError(function, botName, executionException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static <T> T get(@NotNull String tag, @NotNull String botName, CompletableFuture<T> action) throws ResponseError {
|
||||
try {
|
||||
return action.get();
|
||||
} catch (InterruptedException e) {
|
||||
throw ResponseError.newResponseError(tag, botName, e);
|
||||
} catch (ExecutionException executionException) {
|
||||
if (executionException.getCause() instanceof ResponseError) {
|
||||
throw (ResponseError) executionException.getCause();
|
||||
} else {
|
||||
throw ResponseError.newResponseError(tag, botName, executionException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getBotName() {
|
||||
return botName;
|
||||
}
|
||||
|
||||
public int getErrorCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getTag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getErrorMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
private static String functionToInlineString(Function<?> function) {
|
||||
return function
|
||||
.toString()
|
||||
.replace("\n", " ")
|
||||
.replace("\t", "")
|
||||
.replace(" ", "")
|
||||
.replace(" = ", "=")
|
||||
.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getMessage();
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Error;
|
||||
|
||||
public class TdError extends RuntimeException {
|
||||
|
||||
private final int code;
|
||||
private final String message;
|
||||
|
||||
public TdError(int code, String message) {
|
||||
super(code + " " + message);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public TdError(int code, String message, Throwable cause) {
|
||||
super(code + " " + message, cause);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int getTdCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getTdMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public TdApi.Error getTdError() {
|
||||
return new Error(code, message);
|
||||
}
|
||||
}
|
@ -1,289 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Error;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.function.Function;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Encapsulates the result of an asynchronous operation.
|
||||
* <p>
|
||||
* Many operations in Vert.x APIs provide results back by passing an instance of this in a {@link io.vertx.core.Handler}.
|
||||
* <p>
|
||||
* The result can either have failed or succeeded.
|
||||
* <p>
|
||||
* If it failed then the cause of the failure is available with {@link #cause}.
|
||||
* <p>
|
||||
* If it succeeded then the actual result is available with {@link #result}
|
||||
*
|
||||
* @author <a href="http://tfox.org">Tim Fox</a>
|
||||
*/
|
||||
public interface TdResult<T extends TdApi.Object> {
|
||||
|
||||
/**
|
||||
* The result of the operation. This will be null if the operation failed.
|
||||
*
|
||||
* @return the result or null if the operation failed.
|
||||
*/
|
||||
T result();
|
||||
|
||||
/**
|
||||
* The result of the operation. This will throw CompletionException if the operation failed.
|
||||
*
|
||||
* @return the result.
|
||||
*/
|
||||
T orElseThrow() throws CompletionException;
|
||||
|
||||
/**
|
||||
* A TdApi.Error describing failure. This will be null if the operation succeeded.
|
||||
*
|
||||
* @return the cause or null if the operation succeeded.
|
||||
*/
|
||||
TdApi.Error cause();
|
||||
|
||||
/**
|
||||
* Did it succeed?
|
||||
*
|
||||
* @return true if it succeded or false otherwise
|
||||
*/
|
||||
boolean succeeded();
|
||||
|
||||
/**
|
||||
* Did it fail?
|
||||
*
|
||||
* @return true if it failed or false otherwise
|
||||
*/
|
||||
boolean failed();
|
||||
|
||||
/**
|
||||
* Apply a {@code mapper} function on this async result.<p>
|
||||
*
|
||||
* The {@code mapper} is called with the completed value and this mapper returns a value. This value will complete the result returned by this method call.<p>
|
||||
*
|
||||
* When this async result is failed, the failure will be propagated to the returned async result and the {@code mapper} will not be called.
|
||||
*
|
||||
* @param mapper the mapper function
|
||||
* @return the mapped async result
|
||||
*/
|
||||
default <U extends TdApi.Object> TdResult<U> map(Function<T, U> mapper) {
|
||||
if (mapper == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
return new TdResult<U>() {
|
||||
@Override
|
||||
public U result() {
|
||||
if (succeeded()) {
|
||||
return mapper.apply(TdResult.this.result());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public U orElseThrow() throws CompletionException {
|
||||
if (succeeded()) {
|
||||
return mapper.apply(TdResult.this.orElseThrow());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TdApi.Error cause() {
|
||||
return TdResult.this.cause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean succeeded() {
|
||||
return TdResult.this.succeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed() {
|
||||
return TdResult.this.failed();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the result of this async result to a specific {@code value}.<p>
|
||||
*
|
||||
* When this async result succeeds, this {@code value} will succeeed the async result returned by this method call.<p>
|
||||
*
|
||||
* When this async result fails, the failure will be propagated to the returned async result.
|
||||
*
|
||||
* @param value the value that eventually completes the mapped async result
|
||||
* @return the mapped async result
|
||||
*/
|
||||
default <V extends TdApi.Object> TdResult<V> map(V value) {
|
||||
return map((Function<T, V>) t -> value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the result of this async result to {@code null}.<p>
|
||||
*
|
||||
* This is a convenience for {@code TdResult.map((T) null)} or {@code TdResult.map((Void) null)}.<p>
|
||||
*
|
||||
* When this async result succeeds, {@code null} will succeeed the async result returned by this method call.<p>
|
||||
*
|
||||
* When this async result fails, the failure will be propagated to the returned async result.
|
||||
*
|
||||
* @return the mapped async result
|
||||
*/
|
||||
default <V extends TdApi.Object> TdResult<V> mapEmpty() {
|
||||
return map((V)null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a {@code mapper} function on this async result.<p>
|
||||
*
|
||||
* The {@code mapper} is called with the failure and this mapper returns a value. This value will complete the result returned by this method call.<p>
|
||||
*
|
||||
* When this async result is succeeded, the value will be propagated to the returned async result and the {@code mapper} will not be called.
|
||||
*
|
||||
* @param mapper the mapper function
|
||||
* @return the mapped async result
|
||||
*/
|
||||
default TdResult<T> otherwise(Function<TdApi.Error, T> mapper) {
|
||||
if (mapper == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
return new TdResult<T>() {
|
||||
@Override
|
||||
public T result() {
|
||||
if (TdResult.this.succeeded()) {
|
||||
return TdResult.this.result();
|
||||
} else if (TdResult.this.failed()) {
|
||||
return mapper.apply(TdResult.this.cause());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public T orElseThrow() {
|
||||
if (TdResult.this.succeeded()) {
|
||||
return TdResult.this.orElseThrow();
|
||||
} else if (TdResult.this.failed()) {
|
||||
return mapper.apply(TdResult.this.cause());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TdApi.Error cause() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean succeeded() {
|
||||
return TdResult.this.succeeded() || TdResult.this.failed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the failure of this async result to a specific {@code value}.<p>
|
||||
*
|
||||
* When this async result fails, this {@code value} will succeeed the async result returned by this method call.<p>
|
||||
*
|
||||
* When this async succeeds, the result will be propagated to the returned async result.
|
||||
*
|
||||
* @param value the value that eventually completes the mapped async result
|
||||
* @return the mapped async result
|
||||
*/
|
||||
default TdResult<T> otherwise(T value) {
|
||||
return otherwise(err -> value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the failure of this async result to {@code null}.<p>
|
||||
*
|
||||
* This is a convenience for {@code TdResult.otherwise((T) null)}.<p>
|
||||
*
|
||||
* When this async result fails, the {@code null} will succeeed the async result returned by this method call.<p>
|
||||
*
|
||||
* When this async succeeds, the result will be propagated to the returned async result.
|
||||
*
|
||||
* @return the mapped async result
|
||||
*/
|
||||
default TdResult<T> otherwiseEmpty() {
|
||||
return otherwise(err -> null);
|
||||
}
|
||||
|
||||
static <T extends TdApi.Object> TdResult<T> succeeded(@NotNull T value) {
|
||||
Objects.requireNonNull(value);
|
||||
return new TdResultImpl<T>(value, null);
|
||||
}
|
||||
|
||||
static <T extends TdApi.Object> TdResult<T> failed(@NotNull TdApi.Error error) {
|
||||
Objects.requireNonNull(error);
|
||||
return new TdResultImpl<T>(null, error);
|
||||
}
|
||||
|
||||
static <T extends TdApi.Object> TdResult<T> of(@NotNull TdApi.Object resultOrError) {
|
||||
if (resultOrError.getConstructor() == TdApi.Error.CONSTRUCTOR) {
|
||||
return failed((TdApi.Error) resultOrError);
|
||||
} else {
|
||||
//noinspection unchecked
|
||||
return succeeded((T) resultOrError);
|
||||
}
|
||||
}
|
||||
|
||||
class TdResultImpl<U extends TdApi.Object> implements TdResult<U> {
|
||||
|
||||
private final U value;
|
||||
private final Error error;
|
||||
|
||||
public TdResultImpl(U value, Error error) {
|
||||
this.value = value;
|
||||
this.error = error;
|
||||
|
||||
assert (value == null) != (error == null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public U result() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public U orElseThrow() {
|
||||
if (error != null) {
|
||||
throw new TdError(error.code, error.message);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Error cause() {
|
||||
return error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean succeeded() {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean failed() {
|
||||
return error != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", TdResultImpl.class.getSimpleName() + "[", "]")
|
||||
.add("value=" + value)
|
||||
.add("error=" + error)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td;
|
||||
|
||||
import it.tdlight.common.ReactiveTelegramClient;
|
||||
import it.tdlight.common.Signal;
|
||||
import it.tdlight.common.SignalListener;
|
||||
import it.tdlight.common.UpdatesHandler;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.utils.MonoUtils;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxSink.OverflowStrategy;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class WrappedReactorTelegramClient implements ReactorTelegramClient {
|
||||
|
||||
private final ReactiveTelegramClient reactiveTelegramClient;
|
||||
private final AtomicReference<Flux<Signal>> multicastSignals = new AtomicReference<>(null);
|
||||
|
||||
public WrappedReactorTelegramClient(ReactiveTelegramClient reactiveTelegramClient) {
|
||||
this.reactiveTelegramClient = reactiveTelegramClient;
|
||||
}
|
||||
|
||||
public Mono<Void> initialize() {
|
||||
return MonoUtils
|
||||
.fromBlockingEmpty(() -> {
|
||||
reactiveTelegramClient.createAndRegisterClient();
|
||||
Flux<Signal> signalsFlux = Flux
|
||||
.<Signal>create(sink -> {
|
||||
reactiveTelegramClient.setListener(sink::next);
|
||||
sink.onCancel(reactiveTelegramClient::cancel);
|
||||
sink.onDispose(reactiveTelegramClient::dispose);
|
||||
}, OverflowStrategy.BUFFER)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.takeWhile(Signal::isNotClosed);
|
||||
Flux<Signal> refCountedSharedSignalsFlux = signalsFlux.publish().refCount();
|
||||
multicastSignals.set(refCountedSharedSignalsFlux);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<TdApi.Object> receive() {
|
||||
return Flux
|
||||
.defer(() -> {
|
||||
Flux<Signal> flux = multicastSignals.get();
|
||||
if (flux == null) {
|
||||
return Flux.error(new IllegalStateException("TDLib session not started"));
|
||||
} else {
|
||||
return flux;
|
||||
}
|
||||
})
|
||||
.handle((item, sink) -> {
|
||||
if (item.isUpdate()) {
|
||||
sink.next(item.getUpdate());
|
||||
} else if (item.isException()) {
|
||||
sink.error(item.getException());
|
||||
} else {
|
||||
sink.error(new IllegalStateException("This shouldn't happen. Received unknown ReactiveItem type"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the TDLib.
|
||||
*
|
||||
* @param query Object representing a query to the TDLib.
|
||||
* @param timeout Response timeout.
|
||||
* @return a publisher that will emit exactly one item, or an error
|
||||
* @throws NullPointerException if query is null.
|
||||
*/
|
||||
@Override
|
||||
public <T extends TdApi.Object> Mono<TdApi.Object> send(TdApi.Function<T> query, Duration timeout) {
|
||||
return Mono.from(reactiveTelegramClient.send(query, timeout)).single();
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously executes a TDLib request. Only a few marked accordingly requests can be executed synchronously.
|
||||
*
|
||||
* @param query Object representing a query to the TDLib.
|
||||
* @return request result or {@link TdApi.Error}.
|
||||
* @throws NullPointerException if query is null.
|
||||
*/
|
||||
@Override
|
||||
public <T extends TdApi.Object> TdApi.Object execute(TdApi.Function<T> query) {
|
||||
return reactiveTelegramClient.execute(query);
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.direct;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.tdlibsession.td.TdResult;
|
||||
import java.time.Duration;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface AsyncTdDirect {
|
||||
|
||||
Mono<Void> initialize();
|
||||
|
||||
/**
|
||||
* Receives incoming updates and request responses from TDLib.
|
||||
* Can be called only once.
|
||||
*
|
||||
*/
|
||||
Flux<TdApi.Object> receive(AsyncTdDirectOptions options);
|
||||
|
||||
/**
|
||||
* Sends request to TDLib.
|
||||
* Should be called after receive.
|
||||
*
|
||||
* @param request Request to TDLib.
|
||||
* @param timeout Response timeout.
|
||||
* @param synchronous Execute synchronously.
|
||||
* @return The request response or {@link it.tdlight.jni.TdApi.Error}.
|
||||
*/
|
||||
<T extends TdApi.Object> Mono<TdResult<T>> execute(Function<T> request, Duration timeout, boolean synchronous);
|
||||
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.direct;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
|
||||
import it.tdlight.jni.TdApi.Close;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.jni.TdApi.Ok;
|
||||
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
|
||||
import it.tdlight.tdlibsession.td.ReactorTelegramClient;
|
||||
import it.tdlight.tdlibsession.td.TdError;
|
||||
import it.tdlight.tdlibsession.td.TdResult;
|
||||
import it.tdlight.utils.MonoUtils;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.warp.commonutils.log.Logger;
|
||||
import org.warp.commonutils.log.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.One;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class AsyncTdDirectImpl implements AsyncTdDirect {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AsyncTdDirect.class);
|
||||
|
||||
private final TelegramClientFactory telegramClientFactory;
|
||||
private final JsonObject implementationDetails;
|
||||
private final String botAlias;
|
||||
|
||||
private final AtomicReference<ReactorTelegramClient> td = new AtomicReference<>(null);
|
||||
|
||||
public AsyncTdDirectImpl(TelegramClientFactory telegramClientFactory,
|
||||
JsonObject implementationDetails,
|
||||
String botAlias) {
|
||||
this.telegramClientFactory = telegramClientFactory;
|
||||
this.implementationDetails = implementationDetails;
|
||||
this.botAlias = botAlias;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends TdApi.Object> Mono<TdResult<T>> execute(Function<T> request, Duration timeout, boolean synchronous) {
|
||||
if (synchronous) {
|
||||
return MonoUtils.fromBlockingSingle(() -> {
|
||||
var td = this.td.get();
|
||||
logger.trace("Sending execute to TDLib {}", request);
|
||||
Objects.requireNonNull(td, "td is null");
|
||||
TdResult<T> result = TdResult.of(td.execute(request));
|
||||
logger.trace("Received execute response from TDLib. Request was {}", request);
|
||||
return result;
|
||||
})
|
||||
.single();
|
||||
} else {
|
||||
return Mono.defer(() -> {
|
||||
var td = this.td.get();
|
||||
|
||||
if (td != null) {
|
||||
return Mono
|
||||
.fromRunnable(() -> logger.trace("Sending request to TDLib {}", request))
|
||||
.then(td.send(request, timeout))
|
||||
.single()
|
||||
.<TdResult<T>>map(TdResult::of)
|
||||
.doOnSuccess(s -> logger.trace("Sent request to TDLib {}", request));
|
||||
} else {
|
||||
return Mono.fromCallable(() -> {
|
||||
if (request.getConstructor() == Close.CONSTRUCTOR) {
|
||||
logger.trace("Sending close success to request {}", request);
|
||||
return TdResult.of(new Ok());
|
||||
} else {
|
||||
logger.trace("Sending close error to request {} ", request);
|
||||
throw new IllegalStateException("TDLib client is destroyed");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> initialize() {
|
||||
return Mono
|
||||
.fromRunnable(() -> logger.trace("Initializing"))
|
||||
.then(telegramClientFactory.create(implementationDetails))
|
||||
.flatMap(reactorTelegramClient -> reactorTelegramClient.initialize().thenReturn(reactorTelegramClient))
|
||||
.doOnNext(td::set)
|
||||
.doOnNext(client -> client.execute(new TdApi.SetLogVerbosityLevel(1)))
|
||||
.doOnSuccess(s -> logger.trace("Initialized"))
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<TdApi.Object> receive(AsyncTdDirectOptions options) {
|
||||
return Mono
|
||||
.fromCallable(td::get)
|
||||
.single()
|
||||
.flatMapMany(ReactorTelegramClient::receive)
|
||||
.doOnNext(update -> {
|
||||
// Close the emitter if receive closed state
|
||||
if (update.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR
|
||||
&& ((UpdateAuthorizationState) update).authorizationState.getConstructor()
|
||||
== AuthorizationStateClosed.CONSTRUCTOR) {
|
||||
logger.debug("Received closed status from tdlib");
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.direct;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public class AsyncTdDirectOptions {
|
||||
|
||||
private final Duration receiveDuration;
|
||||
private final int eventsSize;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param receiveDuration Maximum number of seconds allowed for this function to wait for new records. Default: 1 sec
|
||||
* @param eventsSize Maximum number of events allowed in list. Default: 350 events
|
||||
*/
|
||||
public AsyncTdDirectOptions(Duration receiveDuration, int eventsSize) {
|
||||
this.receiveDuration = receiveDuration;
|
||||
this.eventsSize = eventsSize;
|
||||
}
|
||||
|
||||
public Duration getReceiveDuration() {
|
||||
return receiveDuration;
|
||||
}
|
||||
|
||||
public int getEventsSize() {
|
||||
return eventsSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", AsyncTdDirectOptions.class.getSimpleName() + "[", "]")
|
||||
.add("receiveDuration=" + receiveDuration)
|
||||
.add("eventsSize=" + eventsSize)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.direct;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import it.tdlight.tdlibsession.td.ReactorTelegramClient;
|
||||
import it.tdlight.tdlibsession.td.WrappedReactorTelegramClient;
|
||||
import it.tdlight.tdlight.ClientManager;
|
||||
import it.tdlight.utils.MonoUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class TelegramClientFactory {
|
||||
|
||||
public TelegramClientFactory() {
|
||||
|
||||
}
|
||||
|
||||
public Mono<ReactorTelegramClient> create(JsonObject implementationDetails) {
|
||||
return MonoUtils.fromBlockingSingle(() -> {
|
||||
var implementationName = implementationDetails.getString("name", "native-client");
|
||||
switch (implementationName) {
|
||||
case "native-client":
|
||||
return new WrappedReactorTelegramClient(ClientManager.createReactive());
|
||||
case "test-client":
|
||||
return new TestClient(implementationDetails.getJsonObject("test-client-settings"));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.direct;
|
||||
|
||||
import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateClosing;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateReady;
|
||||
import it.tdlight.jni.TdApi.Close;
|
||||
import it.tdlight.jni.TdApi.ConnectionStateReady;
|
||||
import it.tdlight.jni.TdApi.FormattedText;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.jni.TdApi.GetMe;
|
||||
import it.tdlight.jni.TdApi.Message;
|
||||
import it.tdlight.jni.TdApi.MessageSenderUser;
|
||||
import it.tdlight.jni.TdApi.MessageText;
|
||||
import it.tdlight.jni.TdApi.Ok;
|
||||
import it.tdlight.jni.TdApi.SetLogTagVerbosityLevel;
|
||||
import it.tdlight.jni.TdApi.SetLogVerbosityLevel;
|
||||
import it.tdlight.jni.TdApi.SetOption;
|
||||
import it.tdlight.jni.TdApi.SetTdlibParameters;
|
||||
import it.tdlight.jni.TdApi.TextEntity;
|
||||
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
|
||||
import it.tdlight.jni.TdApi.UpdateConnectionState;
|
||||
import it.tdlight.jni.TdApi.UpdateNewMessage;
|
||||
import it.tdlight.jni.TdApi.User;
|
||||
import it.tdlight.tdlibsession.td.ReactorTelegramClient;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.warp.commonutils.log.Logger;
|
||||
import org.warp.commonutils.log.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.Empty;
|
||||
|
||||
public class TestClient implements ReactorTelegramClient {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(TestClient.class);
|
||||
|
||||
private static final AtomicLong incrementalMessageId = new AtomicLong(1);
|
||||
private final List<String> features;
|
||||
private final Empty<java.lang.Object> closedSink = Sinks.empty();
|
||||
|
||||
public TestClient(JsonObject testClientSettings) {
|
||||
JsonArray features = testClientSettings.getJsonArray("features", new JsonArray());
|
||||
this.features = new ArrayList<>();
|
||||
for (java.lang.Object feature : features) {
|
||||
var featureName = (String) feature;
|
||||
this.features.add(featureName);
|
||||
}
|
||||
}
|
||||
|
||||
private static Message generateRandomMessage(boolean randomSender, boolean randomChat, boolean randomText) {
|
||||
var msg = new Message();
|
||||
msg.sender = new MessageSenderUser(312042);
|
||||
msg.chatId = 240213;
|
||||
msg.id = incrementalMessageId.getAndIncrement();
|
||||
var content = new MessageText();
|
||||
content.text = new FormattedText("Text", new TextEntity[0]);
|
||||
msg.content = content;
|
||||
msg.date = (int) System.currentTimeMillis() / 1000;
|
||||
return msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> initialize() {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<TdApi.Object> receive() {
|
||||
return Flux.fromIterable(features).flatMap(featureName -> {
|
||||
switch (featureName) {
|
||||
case "status-update":
|
||||
return Flux
|
||||
.<TdApi.Object>just(
|
||||
new UpdateAuthorizationState(new AuthorizationStateReady()),
|
||||
new UpdateConnectionState(new ConnectionStateReady())
|
||||
)
|
||||
.mergeWith(closedSink
|
||||
.asMono()
|
||||
.thenMany(Flux.just(new UpdateAuthorizationState(new AuthorizationStateClosing()),
|
||||
new UpdateAuthorizationState(new AuthorizationStateClosed())
|
||||
))
|
||||
);
|
||||
case "infinite-messages":
|
||||
var randomSenders = features.contains("random-senders");
|
||||
var randomChats = features.contains("random-chats");
|
||||
var randomTexts = features.contains("random-text");
|
||||
return Flux
|
||||
.<TdApi.Object>fromIterable(() -> new Iterator<>() {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TdApi.Object next() {
|
||||
return new UpdateNewMessage(generateRandomMessage(randomSenders, randomChats, randomTexts));
|
||||
}
|
||||
}).takeUntilOther(this.closedSink.asMono());
|
||||
default:
|
||||
return Mono.fromCallable(() -> {
|
||||
throw new IllegalArgumentException("Unknown feature name: " + featureName);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends TdApi.Object> Mono<TdApi.Object> send(Function<T> query, Duration timeout) {
|
||||
return Mono.fromCallable(() -> {
|
||||
TdApi.Object result = executeCommon(query);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
return new TdApi.Error(500, "Unsupported");
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends TdApi.Object> TdApi.Object execute(Function<T> query) {
|
||||
TdApi.Object result = executeCommon(query);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
return new TdApi.Error(500, "Unsupported");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public <T extends TdApi.Object> TdApi.Object executeCommon(Function<T> query) {
|
||||
switch (query.getConstructor()) {
|
||||
case SetLogVerbosityLevel.CONSTRUCTOR:
|
||||
case SetLogTagVerbosityLevel.CONSTRUCTOR:
|
||||
case SetTdlibParameters.CONSTRUCTOR:
|
||||
case SetOption.CONSTRUCTOR:
|
||||
return new Ok();
|
||||
case GetMe.CONSTRUCTOR:
|
||||
var user = new User();
|
||||
user.id = 420;
|
||||
user.firstName = "Test";
|
||||
user.lastName = "Test";
|
||||
user.phoneNumber = "+77";
|
||||
return user;
|
||||
case Close.CONSTRUCTOR:
|
||||
closedSink.tryEmitEmpty();
|
||||
return new Ok();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,657 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.easy;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.AuthorizationState;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateClosing;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateReady;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateWaitCode;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateWaitEncryptionKey;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateWaitOtherDeviceConfirmation;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateWaitPassword;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateWaitPhoneNumber;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateWaitRegistration;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateWaitTdlibParameters;
|
||||
import it.tdlight.jni.TdApi.CheckAuthenticationBotToken;
|
||||
import it.tdlight.jni.TdApi.CheckAuthenticationCode;
|
||||
import it.tdlight.jni.TdApi.CheckAuthenticationPassword;
|
||||
import it.tdlight.jni.TdApi.CheckDatabaseEncryptionKey;
|
||||
import it.tdlight.jni.TdApi.Error;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.jni.TdApi.OptionValueBoolean;
|
||||
import it.tdlight.jni.TdApi.OptionValueEmpty;
|
||||
import it.tdlight.jni.TdApi.OptionValueInteger;
|
||||
import it.tdlight.jni.TdApi.OptionValueString;
|
||||
import it.tdlight.jni.TdApi.PhoneNumberAuthenticationSettings;
|
||||
import it.tdlight.jni.TdApi.RegisterUser;
|
||||
import it.tdlight.jni.TdApi.SetAuthenticationPhoneNumber;
|
||||
import it.tdlight.jni.TdApi.SetTdlibParameters;
|
||||
import it.tdlight.jni.TdApi.TdlibParameters;
|
||||
import it.tdlight.jni.TdApi.Update;
|
||||
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
|
||||
import it.tdlight.tdlibsession.FatalErrorType;
|
||||
import it.tdlight.tdlibsession.td.TdError;
|
||||
import it.tdlight.tdlibsession.td.TdResult;
|
||||
import it.tdlight.tdlibsession.td.middle.AsyncTdMiddle;
|
||||
import it.tdlight.utils.MonoUtils;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.warp.commonutils.log.Logger;
|
||||
import org.warp.commonutils.log.LoggerFactory;
|
||||
import org.warp.commonutils.error.InitializationException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.EmitResult;
|
||||
import reactor.core.publisher.Sinks.Empty;
|
||||
import reactor.core.publisher.Sinks.Many;
|
||||
import reactor.core.publisher.Sinks.One;
|
||||
import reactor.core.publisher.SynchronousSink;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AsyncTdEasy {
|
||||
|
||||
private final Logger logger;
|
||||
private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(1);
|
||||
|
||||
private final Empty<Void> closed = Sinks.empty();
|
||||
private final Many<AuthorizationState> authStateSink = Sinks.many().replay().latest();
|
||||
private final AtomicReference<AuthorizationState> authState = new AtomicReference<>(new AuthorizationStateClosed());
|
||||
private final AtomicBoolean requestedDefinitiveExit = new AtomicBoolean();
|
||||
private final AtomicBoolean canSendCloseRequest = new AtomicBoolean();
|
||||
private final AtomicReference<TdEasySettings> settings = new AtomicReference<>(null);
|
||||
private final Many<Error> globalErrors = Sinks.many().multicast().onBackpressureBuffer();
|
||||
private final One<FatalErrorType> fatalError = Sinks.one();
|
||||
private final AsyncTdMiddle td;
|
||||
private final String logName;
|
||||
private final Flux<Update> incomingUpdates;
|
||||
private final Scheduler scheduler = Schedulers.parallel();
|
||||
|
||||
public AsyncTdEasy(AsyncTdMiddle td, String logName) {
|
||||
this.td = td;
|
||||
this.logName = logName;
|
||||
this.logger = LoggerFactory.getLogger("AsyncTdEasy " + logName);
|
||||
|
||||
this.incomingUpdates = td.receive()
|
||||
.doFirst(() -> {
|
||||
canSendCloseRequest.set(true);
|
||||
logger.debug("From now onwards TdApi.Close cannot be called");
|
||||
})
|
||||
.doOnTerminate(() -> {
|
||||
canSendCloseRequest.set(false);
|
||||
logger.debug("From now onwards TdApi.Close can be called");
|
||||
})
|
||||
.flatMapSequential(this::preprocessUpdates)
|
||||
.map(update -> {
|
||||
var state = authState.get();
|
||||
Objects.requireNonNull(state, "State is not set");
|
||||
return new AsyncTdUpdateObj(state, update);
|
||||
})
|
||||
.map(upd -> (TdApi.Update) upd.getUpdate())
|
||||
.takeUntil(update -> {
|
||||
if (update.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
|
||||
var state = ((UpdateAuthorizationState) update).authorizationState;
|
||||
return state.getConstructor() == AuthorizationStateClosed.CONSTRUCTOR;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.doOnError(ex -> {
|
||||
if (ex instanceof TdError) {
|
||||
var tdEx = (TdError) ex;
|
||||
logger.error("Received an error update from telegram: " + tdEx.getTdCode() + " " + tdEx.getTdMessage());
|
||||
FatalErrorType fatalErrorType;
|
||||
try {
|
||||
fatalErrorType = FatalErrorType.valueOf(tdEx.getTdMessage());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
fatalErrorType = FatalErrorType.INVALID_UPDATE;
|
||||
}
|
||||
this.fatalError.tryEmitValue(fatalErrorType);
|
||||
} else {
|
||||
logger.error(ex.getLocalizedMessage(), ex);
|
||||
}
|
||||
})
|
||||
.doFinally(s -> {
|
||||
var state = authState.get();
|
||||
onUpdatesTerminated();
|
||||
if (state.getConstructor() != AuthorizationStateClosed.CONSTRUCTOR) {
|
||||
logger.warn("Updates stream has closed while"
|
||||
+ " the current authorization state is"
|
||||
+ " still {}. Setting authorization state as closed!", state.getClass().getSimpleName());
|
||||
this.fatalError.tryEmitValue(FatalErrorType.CONNECTION_KILLED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onUpdatesTerminated() {
|
||||
logger.debug("Incoming updates flux terminated. Setting requestedDefinitiveExit: true");
|
||||
requestedDefinitiveExit.set(true);
|
||||
|
||||
var newState = new AuthorizationStateClosed();
|
||||
emitState(newState);
|
||||
}
|
||||
|
||||
public Mono<Void> create(TdEasySettings settings) {
|
||||
return Mono
|
||||
.fromCallable(() -> {
|
||||
// Create session directories
|
||||
if (Files.notExists(Path.of(settings.databaseDirectory))) {
|
||||
try {
|
||||
Files.createDirectories(Path.of(settings.databaseDirectory));
|
||||
} catch (IOException ex) {
|
||||
throw new InitializationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Register fatal error handler
|
||||
fatalError.asMono().flatMap(settings.getFatalErrorHandler()::onFatalError).subscribeOn(scheduler).subscribe();
|
||||
|
||||
return true;
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.flatMap(_v -> {
|
||||
this.settings.set(settings);
|
||||
return Mono.empty();
|
||||
})
|
||||
.then(td.initialize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TDLib state
|
||||
*/
|
||||
public Flux<AuthorizationState> state() {
|
||||
return authStateSink.asFlux().distinct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get incoming updates from TDLib.
|
||||
*/
|
||||
public Flux<TdApi.Update> getIncomingUpdates() {
|
||||
return incomingUpdates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get generic error updates from TDLib (When they are not linked to a precise request).
|
||||
*/
|
||||
public Flux<TdApi.Error> getIncomingErrors() {
|
||||
return Flux.from(globalErrors.asFlux());
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives fatal errors from TDLib.
|
||||
*/
|
||||
public Mono<FatalErrorType> getFatalErrors() {
|
||||
return Mono.from(fatalError.asMono());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends request to TDLib.
|
||||
* @param timeout Timeout duration.
|
||||
* @return The response or {@link TdApi.Error}.
|
||||
*/
|
||||
public <T extends Object> Mono<TdResult<T>> send(TdApi.Function<T> request, Duration timeout) {
|
||||
return td.execute(request, timeout, false);
|
||||
}
|
||||
|
||||
private <T extends TdApi.Object> Mono<TdResult<T>> sendDirectly(Function<T> obj, boolean synchronous) {
|
||||
return td.execute(obj, AsyncTdEasy.DEFAULT_TIMEOUT, synchronous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set verbosity level
|
||||
* @param i level
|
||||
*/
|
||||
public Mono<Void> setVerbosityLevel(int i) {
|
||||
return sendDirectly(new TdApi.SetLogVerbosityLevel(i), true).transform(this::thenOrFatalError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear option on TDLib
|
||||
* @param name option name
|
||||
*/
|
||||
public Mono<Void> clearOption(String name) {
|
||||
return sendDirectly(new TdApi.SetOption(name, new TdApi.OptionValueEmpty()), false)
|
||||
.transform(this::thenOrFatalError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set option on TDLib
|
||||
* @param name option name
|
||||
* @param value option value
|
||||
*/
|
||||
public Mono<Void> setOptionString(String name, String value) {
|
||||
return sendDirectly(new TdApi.SetOption(name, new TdApi.OptionValueString(value)), false)
|
||||
.transform(this::thenOrFatalError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set option on TDLib
|
||||
* @param name option name
|
||||
* @param value option value
|
||||
*/
|
||||
public Mono<Void> setOptionInteger(String name, long value) {
|
||||
return sendDirectly(new TdApi.SetOption(name, new TdApi.OptionValueInteger(value)), false)
|
||||
.transform(this::thenOrFatalError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set option on TDLib
|
||||
* @param name option name
|
||||
* @param value option value
|
||||
*/
|
||||
public Mono<Void> setOptionBoolean(String name, boolean value) {
|
||||
return sendDirectly(new TdApi.SetOption(name, new TdApi.OptionValueBoolean(value)), false)
|
||||
.transform(this::thenOrFatalError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get option from TDLib
|
||||
* @param name option name
|
||||
* @return The value or nothing
|
||||
*/
|
||||
public Mono<String> getOptionString(String name) {
|
||||
return this
|
||||
.<TdApi.OptionValue>sendDirectly(new TdApi.GetOption(name), false)
|
||||
.<TdApi.OptionValue>handle(MonoUtils::orElseThrow)
|
||||
.flatMap(value -> {
|
||||
switch (value.getConstructor()) {
|
||||
case OptionValueString.CONSTRUCTOR:
|
||||
return Mono.just(((OptionValueString) value).value);
|
||||
case OptionValueEmpty.CONSTRUCTOR:
|
||||
return Mono.empty();
|
||||
default:
|
||||
return Mono.error(new UnsupportedOperationException("The option " + name + " is of type "
|
||||
+ value.getClass().getSimpleName()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get option from TDLib
|
||||
* @param name option name
|
||||
* @return The value or nothing
|
||||
*/
|
||||
public Mono<Long> getOptionInteger(String name) {
|
||||
return this
|
||||
.<TdApi.OptionValue>sendDirectly(new TdApi.GetOption(name), false)
|
||||
.<TdApi.OptionValue>handle(MonoUtils::orElseThrow)
|
||||
.flatMap(value -> {
|
||||
switch (value.getConstructor()) {
|
||||
case OptionValueInteger.CONSTRUCTOR:
|
||||
return Mono.just(((OptionValueInteger) value).value);
|
||||
case OptionValueEmpty.CONSTRUCTOR:
|
||||
return Mono.empty();
|
||||
default:
|
||||
return Mono.error(new UnsupportedOperationException(
|
||||
"The option " + name + " is of type " + value.getClass().getSimpleName()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get option from TDLib
|
||||
* @param name option name
|
||||
* @return The value or nothing
|
||||
*/
|
||||
public Mono<Boolean> getOptionBoolean(String name) {
|
||||
return this
|
||||
.sendDirectly(new TdApi.GetOption(name), false)
|
||||
.<TdApi.OptionValue>handle(MonoUtils::orElseThrow)
|
||||
.flatMap(value -> {
|
||||
switch (value.getConstructor()) {
|
||||
case OptionValueBoolean.CONSTRUCTOR:
|
||||
return Mono.just(((OptionValueBoolean) value).value);
|
||||
case OptionValueEmpty.CONSTRUCTOR:
|
||||
return Mono.empty();
|
||||
default:
|
||||
return Mono.error(new UnsupportedOperationException(
|
||||
"The option " + name + " is of type " + value.getClass().getSimpleName()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously executes TDLib requests. Only a few requests can be executed synchronously. May
|
||||
* be called from any thread.
|
||||
*
|
||||
* @param request Request to the TDLib.
|
||||
* @param timeout Timeout.
|
||||
* @return The request response.
|
||||
*/
|
||||
public <T extends Object> Mono<TdResult<T>> execute(TdApi.Function<T> request, Duration timeout) {
|
||||
return td.execute(request, timeout, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the client gracefully by sending {@link TdApi.Close}.
|
||||
*/
|
||||
public Mono<Void> close() {
|
||||
var waitClosed = closed.asMono()
|
||||
.doFirst(() -> logger.debug("Waiting for AuthorizationStateClosed..."))
|
||||
.doOnSuccess(s -> logger.debug("Received AuthorizationStateClosed after TdApi.Close"))
|
||||
.transformDeferred(mono -> {
|
||||
if (canSendCloseRequest.get()) {
|
||||
return mono;
|
||||
} else {
|
||||
return Mono.fromRunnable(() -> emitState(new AuthorizationStateClosed()));
|
||||
}
|
||||
});
|
||||
|
||||
return Mono
|
||||
.fromSupplier(authState::get)
|
||||
.filter(state -> {
|
||||
switch (state.getConstructor()) {
|
||||
case AuthorizationStateClosing.CONSTRUCTOR:
|
||||
case AuthorizationStateClosed.CONSTRUCTOR:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.then(Mono.fromCallable(requestedDefinitiveExit::get).single())
|
||||
.filter(closeRequested -> !closeRequested)
|
||||
.doOnSuccess(s -> {
|
||||
logger.debug("Setting requestedDefinitiveExit: true");
|
||||
requestedDefinitiveExit.set(true);
|
||||
})
|
||||
.then(td
|
||||
.execute(new TdApi.Close(), Duration.ofSeconds(5), false)
|
||||
.doFirst(() -> logger.debug("Sending TdApi.Close"))
|
||||
.doOnNext(closeResponse -> logger.debug("TdApi.Close response is: \"{}\"",
|
||||
closeResponse.toString().replace('\n', ' ')
|
||||
))
|
||||
.doOnSuccess(s -> logger.debug("Sent TdApi.Close"))
|
||||
.transformDeferred(closeMono -> {
|
||||
if (canSendCloseRequest.get()) {
|
||||
return closeMono;
|
||||
} else {
|
||||
return Mono.empty();
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
.then(waitClosed)
|
||||
.doOnSuccess(s -> logger.info("AsyncTdEasy closed successfully"))
|
||||
.then();
|
||||
}
|
||||
|
||||
private Mono<Update> catchErrors(Object obj) {
|
||||
return Mono.fromCallable(() -> {
|
||||
if (obj.getConstructor() == Error.CONSTRUCTOR) {
|
||||
var error = (Error) obj;
|
||||
|
||||
switch (error.message) {
|
||||
case "PHONE_CODE_INVALID":
|
||||
globalErrors.tryEmitNext(error);
|
||||
return new UpdateAuthorizationState(new AuthorizationStateWaitCode());
|
||||
case "PASSWORD_HASH_INVALID":
|
||||
globalErrors.tryEmitNext(error);
|
||||
return new UpdateAuthorizationState(new AuthorizationStateWaitPassword());
|
||||
default:
|
||||
globalErrors.tryEmitNext(error);
|
||||
break;
|
||||
}
|
||||
analyzeFatalErrors(error);
|
||||
return null;
|
||||
} else {
|
||||
return (Update) obj;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void analyzeFatalErrors(Object obj) {
|
||||
if (obj != null && obj.getConstructor() == Error.CONSTRUCTOR) {
|
||||
var error = (Error) obj;
|
||||
switch (error.message) {
|
||||
case "PHONE_NUMBER_INVALID":
|
||||
fatalError.tryEmitValue(FatalErrorType.PHONE_NUMBER_INVALID);
|
||||
break;
|
||||
case "ACCESS_TOKEN_INVALID":
|
||||
fatalError.tryEmitValue(FatalErrorType.ACCESS_TOKEN_INVALID);
|
||||
break;
|
||||
case "CONNECTION_KILLED":
|
||||
fatalError.tryEmitValue(FatalErrorType.CONNECTION_KILLED);
|
||||
break;
|
||||
case "INVALID_UPDATE":
|
||||
fatalError.tryEmitValue(FatalErrorType.INVALID_UPDATE);
|
||||
break;
|
||||
case "PHONE_NUMBER_BANNED":
|
||||
fatalError.tryEmitValue(FatalErrorType.PHONE_NUMBER_BANNED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Publisher<TdApi.Update> preprocessUpdates(TdApi.Object updateObj) {
|
||||
return Mono
|
||||
.just(updateObj)
|
||||
.flatMap(this::catchErrors)
|
||||
.filter(obj -> obj.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR)
|
||||
.map(obj -> ((UpdateAuthorizationState) obj).authorizationState)
|
||||
.flatMap(obj -> {
|
||||
switch (obj.getConstructor()) {
|
||||
case AuthorizationStateWaitTdlibParameters.CONSTRUCTOR:
|
||||
return Mono
|
||||
.fromCallable(this.settings::get)
|
||||
.single()
|
||||
.map(settings -> {
|
||||
var parameters = new TdlibParameters();
|
||||
parameters.useTestDc = settings.useTestDc;
|
||||
parameters.databaseDirectory = settings.databaseDirectory;
|
||||
parameters.filesDirectory = settings.filesDirectory;
|
||||
parameters.useFileDatabase = settings.useFileDatabase;
|
||||
parameters.useChatInfoDatabase = settings.useChatInfoDatabase;
|
||||
parameters.useMessageDatabase = settings.useMessageDatabase;
|
||||
parameters.useSecretChats = false;
|
||||
parameters.apiId = settings.apiId;
|
||||
parameters.apiHash = settings.apiHash;
|
||||
parameters.systemLanguageCode = settings.systemLanguageCode;
|
||||
parameters.deviceModel = settings.deviceModel;
|
||||
parameters.systemVersion = settings.systemVersion;
|
||||
parameters.applicationVersion = settings.applicationVersion;
|
||||
parameters.enableStorageOptimizer = settings.enableStorageOptimizer;
|
||||
parameters.ignoreFileNames = settings.ignoreFileNames;
|
||||
return new SetTdlibParameters(parameters);
|
||||
})
|
||||
.flatMap((SetTdlibParameters obj1) -> sendDirectly(obj1, false))
|
||||
.transform(this::thenOrFatalError);
|
||||
case AuthorizationStateWaitEncryptionKey.CONSTRUCTOR:
|
||||
return sendDirectly(new CheckDatabaseEncryptionKey(), false)
|
||||
.transform(this::thenOrFatalError)
|
||||
.onErrorResume((error) -> {
|
||||
logger.error("Error while checking TDLib encryption key", error);
|
||||
return sendDirectly(new TdApi.Close(), false).then();
|
||||
});
|
||||
case AuthorizationStateWaitPhoneNumber.CONSTRUCTOR:
|
||||
return Mono
|
||||
.fromCallable(this.settings::get).single().flatMap(settings -> {
|
||||
if (settings.isPhoneNumberSet()) {
|
||||
return sendDirectly(new SetAuthenticationPhoneNumber(String.valueOf(settings.getPhoneNumber()),
|
||||
new PhoneNumberAuthenticationSettings(false, false, false)
|
||||
), false);
|
||||
} else if (settings.isBotTokenSet()) {
|
||||
return sendDirectly(new CheckAuthenticationBotToken(settings.getBotToken()), false);
|
||||
} else {
|
||||
return Mono.error(new IllegalArgumentException("A bot is neither an user or a bot"));
|
||||
}
|
||||
})
|
||||
.transform(this::thenOrFatalError)
|
||||
.onErrorResume((error) -> {
|
||||
logger.error("Error while waiting for phone number", error);
|
||||
return sendDirectly(new TdApi.Close(), false).then();
|
||||
});
|
||||
case AuthorizationStateWaitRegistration.CONSTRUCTOR:
|
||||
var authorizationStateWaitRegistration = (AuthorizationStateWaitRegistration) obj;
|
||||
RegisterUser registerUser = new RegisterUser();
|
||||
if (authorizationStateWaitRegistration.termsOfService != null
|
||||
&& authorizationStateWaitRegistration.termsOfService.text != null
|
||||
&& !authorizationStateWaitRegistration.termsOfService.text.text.isBlank()) {
|
||||
logger.info("Telegram Terms of Service:\n" + authorizationStateWaitRegistration.termsOfService.text.text);
|
||||
}
|
||||
|
||||
return Mono
|
||||
.fromCallable(this.settings::get)
|
||||
.single()
|
||||
.map(TdEasySettings::getParameterRequestHandler)
|
||||
.flatMap(handler -> handler
|
||||
.onParameterRequest(Parameter.ASK_FIRST_NAME, new ParameterInfoEmpty())
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(firstName -> !firstName.isBlank() && firstName.length() <= 64 && firstName.length() > 0)
|
||||
.repeatWhen(s -> s.takeWhile(n -> n == 0))
|
||||
.last()
|
||||
.doOnNext(firstName -> registerUser.firstName = firstName)
|
||||
.then(handler
|
||||
.onParameterRequest(Parameter.ASK_LAST_NAME, new ParameterInfoEmpty())
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(lastName -> lastName.length() <= 64)
|
||||
.repeatWhen(s -> s.takeWhile(n -> n == 0))
|
||||
.last()
|
||||
.defaultIfEmpty("")
|
||||
.doOnNext(lastName -> registerUser.lastName = lastName)
|
||||
)
|
||||
.then(sendDirectly(registerUser, false))
|
||||
.transform(this::thenOrLogRepeatError)
|
||||
);
|
||||
case TdApi.AuthorizationStateWaitOtherDeviceConfirmation.CONSTRUCTOR:
|
||||
var authorizationStateWaitOtherDeviceConfirmation = (AuthorizationStateWaitOtherDeviceConfirmation) obj;
|
||||
return Mono
|
||||
.fromCallable(this.settings::get)
|
||||
.single()
|
||||
.map(TdEasySettings::getParameterRequestHandler)
|
||||
.flatMap(handler -> handler.onParameterRequest(Parameter.NOTIFY_LINK,
|
||||
new ParameterInfoNotifyLink(authorizationStateWaitOtherDeviceConfirmation.link)
|
||||
));
|
||||
case TdApi.AuthorizationStateWaitCode.CONSTRUCTOR:
|
||||
var authorizationStateWaitCode = (AuthorizationStateWaitCode) obj;
|
||||
return Mono
|
||||
.fromCallable(this.settings::get)
|
||||
.single()
|
||||
.map(TdEasySettings::getParameterRequestHandler)
|
||||
.flatMap(handler -> handler
|
||||
.onParameterRequest(Parameter.ASK_CODE, new ParameterInfoCode(authorizationStateWaitCode.codeInfo.phoneNumber,
|
||||
authorizationStateWaitCode.codeInfo.nextType,
|
||||
authorizationStateWaitCode.codeInfo.timeout,
|
||||
authorizationStateWaitCode.codeInfo.type))
|
||||
.flatMap(code -> sendDirectly(new CheckAuthenticationCode(code), false))
|
||||
.transform(this::thenOrLogRepeatError)
|
||||
);
|
||||
case AuthorizationStateWaitPassword.CONSTRUCTOR:
|
||||
var authorizationStateWaitPassword = (AuthorizationStateWaitPassword) obj;
|
||||
return Mono
|
||||
.fromCallable(this.settings::get)
|
||||
.single()
|
||||
.map(TdEasySettings::getParameterRequestHandler)
|
||||
.flatMap(handler -> handler
|
||||
.onParameterRequest(Parameter.ASK_PASSWORD, new ParameterInfoPasswordHint(
|
||||
authorizationStateWaitPassword.passwordHint))
|
||||
.flatMap(password -> sendDirectly(new CheckAuthenticationPassword(password), false))
|
||||
)
|
||||
.transform(this::thenOrLogRepeatError);
|
||||
case AuthorizationStateReady.CONSTRUCTOR: {
|
||||
var state = new AuthorizationStateReady();
|
||||
emitState(state);
|
||||
return Mono.empty();
|
||||
}
|
||||
case AuthorizationStateClosing.CONSTRUCTOR:
|
||||
logger.debug("Received AuthorizationStateClosing from td");
|
||||
return Mono.empty();
|
||||
case AuthorizationStateClosed.CONSTRUCTOR:
|
||||
logger.debug("Received AuthorizationStateClosed from td");
|
||||
return Mono.fromCallable(() -> {
|
||||
var closeRequested = this.requestedDefinitiveExit.get();
|
||||
if (closeRequested) {
|
||||
logger.debug("td closed successfully");
|
||||
} else {
|
||||
logger.warn("td closed unexpectedly: {}", logName);
|
||||
}
|
||||
emitState(obj);
|
||||
return closeRequested;
|
||||
}).flatMap(closeRequested -> {
|
||||
if (closeRequested) {
|
||||
return Mono
|
||||
.fromCallable(settings::get)
|
||||
.single()
|
||||
.map(settings -> settings.databaseDirectory)
|
||||
.map(Path::of)
|
||||
.flatMapIterable(sessionPath -> Set.of(sessionPath.resolve("media"),
|
||||
sessionPath.resolve("passport"),
|
||||
sessionPath.resolve("profile_photos"),
|
||||
sessionPath.resolve("stickers"),
|
||||
sessionPath.resolve("temp"),
|
||||
sessionPath.resolve("thumbnails"),
|
||||
sessionPath.resolve("wallpapers")
|
||||
))
|
||||
.filterWhen(file -> Mono
|
||||
.fromCallable(() -> Files.exists(file))
|
||||
.subscribeOn(Schedulers.boundedElastic()))
|
||||
.doOnNext(directory -> {
|
||||
try {
|
||||
if (!Files.walk(directory)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.allMatch(File::delete)) {
|
||||
throw new IOException("Can't delete a file!");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Can't delete temporary session subdirectory", e);
|
||||
}
|
||||
})
|
||||
.then(Mono.just(true));
|
||||
} else {
|
||||
return Mono.just(false);
|
||||
}
|
||||
}).then();
|
||||
default:
|
||||
return Mono.empty();
|
||||
}
|
||||
})
|
||||
.then(Mono.justOrEmpty(updateObj.getConstructor() == Error.CONSTRUCTOR ? null : (Update) updateObj));
|
||||
}
|
||||
|
||||
private void emitState(AuthorizationState state) {
|
||||
if (state.getConstructor() == AuthorizationStateClosed.CONSTRUCTOR) {
|
||||
this.closed.tryEmitEmpty();
|
||||
}
|
||||
this.authState.set(state);
|
||||
EmitResult emitResult;
|
||||
while ((emitResult = this.authStateSink.tryEmitNext(state)) == EmitResult.FAIL_NON_SERIALIZED) {
|
||||
// Wait 10ms
|
||||
LockSupport.parkNanos(10L * 1000000L);
|
||||
}
|
||||
emitResult.orThrow();
|
||||
}
|
||||
|
||||
private <T extends TdApi.Object> Mono<Void> thenOrFatalError(Mono<TdResult<T>> mono) {
|
||||
return mono.doOnNext(result -> {
|
||||
if (result.failed()) {
|
||||
analyzeFatalErrors(result.cause());
|
||||
}
|
||||
}).transform(MonoUtils::thenOrError);
|
||||
}
|
||||
|
||||
|
||||
private <T extends TdApi.Object> Mono<Void> thenOrLogRepeatError(Mono<TdResult<T>> mono) {
|
||||
return mono.handle((TdResult<T> optional, SynchronousSink<Void> sink) -> {
|
||||
if (optional.succeeded()) {
|
||||
sink.complete();
|
||||
} else {
|
||||
logger.error("Received TDLib error: {}", optional.cause());
|
||||
sink.error(new TdError(optional.cause().code, optional.cause().message));
|
||||
}
|
||||
}).retry();
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.easy;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.AuthorizationState;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public class AsyncTdUpdateObj {
|
||||
private final AuthorizationState state;
|
||||
private final TdApi.Object update;
|
||||
|
||||
public AsyncTdUpdateObj(AuthorizationState state, TdApi.Object update) {
|
||||
this.state = state;
|
||||
this.update = update;
|
||||
}
|
||||
|
||||
public AuthorizationState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public TdApi.Object getUpdate() {
|
||||
return update;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
AsyncTdUpdateObj that = (AsyncTdUpdateObj) o;
|
||||
return Objects.equals(state, that.state) && Objects.equals(update, that.update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(state, update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", AsyncTdUpdateObj.class.getSimpleName() + "[", "]")
|
||||
.add("state=" + state)
|
||||
.add("update=" + update)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.easy;
|
||||
|
||||
import it.tdlight.tdlibsession.FatalErrorType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface FatalErrorHandler {
|
||||
@NotNull Mono<Void> onFatalError(FatalErrorType error);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.easy;
|
||||
|
||||
public enum Parameter {
|
||||
ASK_FIRST_NAME,
|
||||
ASK_LAST_NAME,
|
||||
ASK_CODE,
|
||||
ASK_PASSWORD,
|
||||
NOTIFY_LINK
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.easy;
|
||||
|
||||
public interface ParameterInfo {
|
||||
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.easy;
|
||||
|
||||
import it.tdlight.jni.TdApi.AuthenticationCodeType;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public class ParameterInfoCode implements ParameterInfo {
|
||||
private final String phoneNumber;
|
||||
private final AuthenticationCodeType nextType;
|
||||
private final int timeout;
|
||||
private final AuthenticationCodeType type;
|
||||
|
||||
public ParameterInfoCode(String phoneNumber,
|
||||
AuthenticationCodeType nextType,
|
||||
int timeout,
|
||||
AuthenticationCodeType type) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.nextType = nextType;
|
||||
this.timeout = timeout;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public AuthenticationCodeType getNextType() {
|
||||
return nextType;
|
||||
}
|
||||
|
||||
public int getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
public AuthenticationCodeType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", ParameterInfoCode.class.getSimpleName() + "[", "]")
|
||||
.add("phoneNumber='" + phoneNumber + "'")
|
||||
.add("nextType=" + nextType)
|
||||
.add("timeout=" + timeout)
|
||||
.add("type=" + type)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.easy;
|
||||
|
||||
public class ParameterInfoEmpty implements ParameterInfo {
|
||||
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.easy;
|
||||
|
||||
public class ParameterInfoNotifyLink implements ParameterInfo {
|
||||
private final String link;
|
||||
|
||||
public ParameterInfoNotifyLink(String link) {
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
public String getLink() {
|
||||
return link;
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.easy;
|
||||
|
||||
public class ParameterInfoPasswordHint implements ParameterInfo {
|
||||
private final String hint;
|
||||
|
||||
public ParameterInfoPasswordHint(String hint) {
|
||||
this.hint = hint;
|
||||
}
|
||||
|
||||
public String getHint() {
|
||||
return hint;
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.easy;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface ParameterRequestHandler {
|
||||
Mono<String> onParameterRequest(Parameter parameter, ParameterInfo parameterInfo);
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.easy;
|
||||
|
||||
import it.tdlight.common.utils.ScannerUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class ScannerParameterRequestHandler implements ParameterRequestHandler {
|
||||
|
||||
private final String botName;
|
||||
|
||||
public ScannerParameterRequestHandler(String botName) {
|
||||
this.botName = botName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> onParameterRequest(Parameter parameter, ParameterInfo parameterInfo) {
|
||||
return Mono.fromCallable(() -> {
|
||||
String question;
|
||||
boolean trim = false;
|
||||
switch (parameter) {
|
||||
case ASK_FIRST_NAME: question = "Enter first name"; trim = true; break;
|
||||
case ASK_LAST_NAME: question = "Enter last name"; trim = true; break;
|
||||
case ASK_CODE: question = "Enter authentication code"; trim = true; break;
|
||||
case ASK_PASSWORD:
|
||||
question = "Enter your password";
|
||||
String passwordMessage = "Password authorization of '" + this.botName + "':";
|
||||
String hint = ((ParameterInfoPasswordHint) parameterInfo).getHint();
|
||||
if (hint != null && !hint.isBlank()) {
|
||||
passwordMessage += "\n\tHint: " + hint;
|
||||
}
|
||||
System.out.println(passwordMessage);
|
||||
break;
|
||||
case NOTIFY_LINK:
|
||||
System.out.println("Please confirm this login link on another device: "
|
||||
+ ((ParameterInfoNotifyLink) parameterInfo).getLink());
|
||||
return "";
|
||||
default: question = parameter.toString(); break;
|
||||
}
|
||||
var result = ScannerUtils.askParameter(this.botName, question);
|
||||
if (trim) {
|
||||
return result.trim();
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
}
|
@ -1,310 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.easy;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class TdEasySettings {
|
||||
public final boolean useTestDc;
|
||||
public final String databaseDirectory;
|
||||
public final String filesDirectory;
|
||||
public final boolean useFileDatabase;
|
||||
public final boolean useChatInfoDatabase;
|
||||
public final boolean useMessageDatabase;
|
||||
public final int apiId;
|
||||
public final String apiHash;
|
||||
public final String systemLanguageCode;
|
||||
public final String deviceModel;
|
||||
public final String systemVersion;
|
||||
public final String applicationVersion;
|
||||
public final boolean enableStorageOptimizer;
|
||||
public final boolean ignoreFileNames;
|
||||
private final Long phoneNumber;
|
||||
private final String botToken;
|
||||
private final ParameterRequestHandler parameterRequestHandler;
|
||||
private final FatalErrorHandler fatalErrorHandler;
|
||||
|
||||
public TdEasySettings(boolean useTestDc,
|
||||
String databaseDirectory,
|
||||
String filesDirectory,
|
||||
boolean useFileDatabase,
|
||||
boolean useChatInfoDatabase,
|
||||
boolean useMessageDatabase,
|
||||
int apiId,
|
||||
String apiHash,
|
||||
String systemLanguageCode,
|
||||
String deviceModel,
|
||||
String systemVersion,
|
||||
String applicationVersion,
|
||||
boolean enableStorageOptimizer,
|
||||
boolean ignoreFileNames,
|
||||
@Nullable Long phoneNumber,
|
||||
@Nullable String botToken,
|
||||
@Nullable ParameterRequestHandler parameterRequestHandler,
|
||||
@Nullable FatalErrorHandler fatalErrorHandler) {
|
||||
this.useTestDc = useTestDc;
|
||||
this.databaseDirectory = databaseDirectory;
|
||||
this.filesDirectory = filesDirectory;
|
||||
this.useFileDatabase = useFileDatabase;
|
||||
this.useChatInfoDatabase = useChatInfoDatabase;
|
||||
this.useMessageDatabase = useMessageDatabase;
|
||||
this.apiId = apiId;
|
||||
this.apiHash = apiHash;
|
||||
this.systemLanguageCode = systemLanguageCode;
|
||||
this.deviceModel = deviceModel;
|
||||
this.systemVersion = systemVersion;
|
||||
this.applicationVersion = applicationVersion;
|
||||
this.enableStorageOptimizer = enableStorageOptimizer;
|
||||
this.ignoreFileNames = ignoreFileNames;
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.botToken = botToken;
|
||||
if ((phoneNumber == null) == (botToken == null)) {
|
||||
throw new IllegalArgumentException("You must set a phone number or a bot token");
|
||||
}
|
||||
if (parameterRequestHandler == null) {
|
||||
if (botToken != null) {
|
||||
parameterRequestHandler = new ScannerParameterRequestHandler("bot_" + botToken.split(":")[0]);
|
||||
} else {
|
||||
parameterRequestHandler = new ScannerParameterRequestHandler("+" + phoneNumber);
|
||||
}
|
||||
}
|
||||
this.parameterRequestHandler = parameterRequestHandler;
|
||||
if (fatalErrorHandler == null) {
|
||||
fatalErrorHandler = error -> Mono.empty();
|
||||
}
|
||||
this.fatalErrorHandler = fatalErrorHandler;
|
||||
}
|
||||
|
||||
public boolean isPhoneNumberSet() {
|
||||
return phoneNumber != null;
|
||||
}
|
||||
|
||||
public long getPhoneNumber() {
|
||||
return Objects.requireNonNull(phoneNumber, "You must set a phone number");
|
||||
}
|
||||
|
||||
public boolean isBotTokenSet() {
|
||||
return botToken != null;
|
||||
}
|
||||
|
||||
public String getBotToken() {
|
||||
return Objects.requireNonNull(botToken, "You must set a bot token");
|
||||
}
|
||||
|
||||
public ParameterRequestHandler getParameterRequestHandler() {
|
||||
return Objects.requireNonNull(parameterRequestHandler, "You must set a parameter request handler");
|
||||
}
|
||||
|
||||
public FatalErrorHandler getFatalErrorHandler() {
|
||||
return Objects.requireNonNull(fatalErrorHandler, "You must set a fatal error handler");
|
||||
}
|
||||
|
||||
public static Builder newBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
|
||||
public static class Builder {
|
||||
private boolean useTestDc = false;
|
||||
private String databaseDirectory = "jtdlib-database";
|
||||
private String filesDirectory = "jtdlib-files";
|
||||
private boolean useFileDatabase = true;
|
||||
private boolean useChatInfoDatabase = true;
|
||||
private boolean useMessageDatabase = true;
|
||||
private int apiId = 376588;
|
||||
private String apiHash = "2143fdfc2bbba3ec723228d2f81336c9";
|
||||
private String systemLanguageCode = "en";
|
||||
private String deviceModel = "JTDLib";
|
||||
private String systemVersion = "JTDLib";
|
||||
private String applicationVersion = "1.0";
|
||||
private boolean enableStorageOptimizer = false;
|
||||
private boolean ignoreFileNames = false;
|
||||
@Nullable
|
||||
private Long phoneNumber = null;
|
||||
@Nullable
|
||||
private String botToken = null;
|
||||
private ParameterRequestHandler parameterRequestHandler;
|
||||
@Nullable
|
||||
private FatalErrorHandler fatalErrorHandler;
|
||||
|
||||
private Builder() {
|
||||
|
||||
}
|
||||
|
||||
public boolean isUseTestDc() {
|
||||
return useTestDc;
|
||||
}
|
||||
|
||||
public Builder setUseTestDc(boolean useTestDc) {
|
||||
this.useTestDc = useTestDc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDatabaseDirectory() {
|
||||
return databaseDirectory;
|
||||
}
|
||||
|
||||
public Builder setDatabaseDirectory(String databaseDirectory) {
|
||||
this.databaseDirectory = databaseDirectory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getFilesDirectory() {
|
||||
return filesDirectory;
|
||||
}
|
||||
|
||||
public Builder setFilesDirectory(String filesDirectory) {
|
||||
this.filesDirectory = filesDirectory;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isUseFileDatabase() {
|
||||
return useFileDatabase;
|
||||
}
|
||||
|
||||
public Builder setUseFileDatabase(boolean useFileDatabase) {
|
||||
this.useFileDatabase = useFileDatabase;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isUseChatInfoDatabase() {
|
||||
return useChatInfoDatabase;
|
||||
}
|
||||
|
||||
public Builder setUseChatInfoDatabase(boolean useChatInfoDatabase) {
|
||||
this.useChatInfoDatabase = useChatInfoDatabase;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isUseMessageDatabase() {
|
||||
return useMessageDatabase;
|
||||
}
|
||||
|
||||
public Builder setUseMessageDatabase(boolean useMessageDatabase) {
|
||||
this.useMessageDatabase = useMessageDatabase;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getApiId() {
|
||||
return apiId;
|
||||
}
|
||||
|
||||
public Builder setApiId(int apiId) {
|
||||
this.apiId = apiId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getApiHash() {
|
||||
return apiHash;
|
||||
}
|
||||
|
||||
public Builder setApiHash(String apiHash) {
|
||||
this.apiHash = apiHash;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getSystemLanguageCode() {
|
||||
return systemLanguageCode;
|
||||
}
|
||||
|
||||
public Builder setSystemLanguageCode(String systemLanguageCode) {
|
||||
this.systemLanguageCode = systemLanguageCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDeviceModel() {
|
||||
return deviceModel;
|
||||
}
|
||||
|
||||
public Builder setDeviceModel(String deviceModel) {
|
||||
this.deviceModel = deviceModel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getSystemVersion() {
|
||||
return systemVersion;
|
||||
}
|
||||
|
||||
public Builder setSystemVersion(String systemVersion) {
|
||||
this.systemVersion = systemVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getApplicationVersion() {
|
||||
return applicationVersion;
|
||||
}
|
||||
|
||||
public Builder setApplicationVersion(String applicationVersion) {
|
||||
this.applicationVersion = applicationVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isEnableStorageOptimizer() {
|
||||
return enableStorageOptimizer;
|
||||
}
|
||||
|
||||
public Builder setEnableStorageOptimizer(boolean enableStorageOptimizer) {
|
||||
this.enableStorageOptimizer = enableStorageOptimizer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isIgnoreFileNames() {
|
||||
return ignoreFileNames;
|
||||
}
|
||||
|
||||
public Builder setIgnoreFileNames(boolean ignoreFileNames) {
|
||||
this.ignoreFileNames = ignoreFileNames;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPhoneNumber(long phoneNumber) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBotToken(String botToken) {
|
||||
this.botToken = botToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setParameterRequestHandler(ParameterRequestHandler parameterRequestHandler) {
|
||||
this.parameterRequestHandler = parameterRequestHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ParameterRequestHandler getParameterRequestHandler() {
|
||||
return parameterRequestHandler;
|
||||
}
|
||||
|
||||
public Builder setFatalErrorHandler(FatalErrorHandler fatalErrorHandler) {
|
||||
this.fatalErrorHandler = fatalErrorHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @Nullable FatalErrorHandler getFatalErrorHandler() {
|
||||
return fatalErrorHandler;
|
||||
}
|
||||
|
||||
public TdEasySettings build() {
|
||||
return new TdEasySettings(useTestDc,
|
||||
databaseDirectory,
|
||||
filesDirectory,
|
||||
useFileDatabase,
|
||||
useChatInfoDatabase,
|
||||
useMessageDatabase,
|
||||
apiId,
|
||||
apiHash,
|
||||
systemLanguageCode,
|
||||
deviceModel,
|
||||
systemVersion,
|
||||
applicationVersion,
|
||||
enableStorageOptimizer,
|
||||
ignoreFileNames,
|
||||
phoneNumber,
|
||||
botToken,
|
||||
parameterRequestHandler,
|
||||
fatalErrorHandler
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.tdlibsession.td.TdResult;
|
||||
import java.time.Duration;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface AsyncTdMiddle {
|
||||
|
||||
Mono<Void> initialize();
|
||||
|
||||
/**
|
||||
* Receives incoming updates from TDLib.
|
||||
*
|
||||
* @return Updates (or Error if received a fatal error. A fatal error means that the client is no longer working)
|
||||
*/
|
||||
Flux<TdApi.Object> receive();
|
||||
|
||||
/**
|
||||
* Sends request to TDLib. May be called from any thread.
|
||||
*
|
||||
* @param request Request to TDLib.
|
||||
* @param timeout Timeout.
|
||||
* @param executeSync Execute the function synchronously.
|
||||
*/
|
||||
<T extends TdApi.Object> Mono<TdResult<T>> execute(TdApi.Function<T> request, Duration timeout, boolean executeSync);
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.reactivex.core.buffer.Buffer;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public final class EndSessionMessage {
|
||||
|
||||
private final int id;
|
||||
private final Buffer binlog;
|
||||
|
||||
public EndSessionMessage(int id, Buffer binlog) {
|
||||
this.id = id;
|
||||
this.binlog = binlog;
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Buffer binlog() {
|
||||
return binlog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
EndSessionMessage that = (EndSessionMessage) o;
|
||||
return id == that.id && Objects.equals(binlog, that.binlog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, binlog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", EndSessionMessage.class.getSimpleName() + "[", "]")
|
||||
.add("id=" + id)
|
||||
.add("binlog=" + binlog)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
import it.tdlight.utils.BufferUtils;
|
||||
|
||||
public class EndSessionMessageCodec implements MessageCodec<EndSessionMessage, EndSessionMessage> {
|
||||
|
||||
private final String codecName;
|
||||
|
||||
public EndSessionMessageCodec() {
|
||||
super();
|
||||
this.codecName = "EndSessionMessageCodec";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encodeToWire(Buffer buffer, EndSessionMessage t) {
|
||||
BufferUtils.encode(buffer, os -> {
|
||||
os.writeInt(t.id());
|
||||
BufferUtils.writeBuf(os, t.binlog());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndSessionMessage decodeFromWire(int pos, Buffer buffer) {
|
||||
return BufferUtils.decode(pos, buffer, is -> new EndSessionMessage(is.readInt(), BufferUtils.rxReadBuf(is)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public EndSessionMessage transform(EndSessionMessage t) {
|
||||
// If a message is sent *locally* across the event bus.
|
||||
// This sends message just as is
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return codecName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte systemCodecID() {
|
||||
// Always -1
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public class ExecuteObject<T extends TdApi.Object> {
|
||||
|
||||
private static final TdExecuteObjectMessageCodec<?> realCodec = new TdExecuteObjectMessageCodec<>();
|
||||
|
||||
private boolean executeDirectly;
|
||||
private TdApi.Function<T> request;
|
||||
private Duration timeout;
|
||||
private int pos;
|
||||
private Buffer buffer;
|
||||
|
||||
public ExecuteObject(boolean executeDirectly, Function<T> request, Duration timeout) {
|
||||
if (request == null) throw new NullPointerException();
|
||||
|
||||
this.executeDirectly = executeDirectly;
|
||||
this.request = request;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public ExecuteObject(int pos, Buffer buffer) {
|
||||
this.pos = pos;
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
private void tryDecode() {
|
||||
if (request == null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
ExecuteObject<T> data = (ExecuteObject<T>) realCodec.decodeFromWire(pos, buffer);
|
||||
this.executeDirectly = data.executeDirectly;
|
||||
this.request = data.request;
|
||||
this.buffer = null;
|
||||
this.timeout = data.timeout;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isExecuteDirectly() {
|
||||
tryDecode();
|
||||
return executeDirectly;
|
||||
}
|
||||
|
||||
public TdApi.Function<T> getRequest() {
|
||||
tryDecode();
|
||||
return request;
|
||||
}
|
||||
|
||||
public Duration getTimeout() {
|
||||
tryDecode();
|
||||
return timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
tryDecode();
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ExecuteObject<?> that = (ExecuteObject<?>) o;
|
||||
|
||||
if (executeDirectly != that.executeDirectly) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(request, that.request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
tryDecode();
|
||||
int result = (executeDirectly ? 1 : 0);
|
||||
result = 31 * result + (request != null ? request.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", ExecuteObject.class.getSimpleName() + "[", "]")
|
||||
.add("executeDirectly=" + executeDirectly)
|
||||
.add("request=" + request)
|
||||
.add("timeout=" + timeout)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
import it.tdlight.jni.TdApi;
|
||||
|
||||
public class LazyTdExecuteObjectMessageCodec<T extends TdApi.Object>
|
||||
implements MessageCodec<ExecuteObject<T>, ExecuteObject<T>> {
|
||||
|
||||
private static final TdExecuteObjectMessageCodec<?> realCodec = new TdExecuteObjectMessageCodec<>();
|
||||
|
||||
public LazyTdExecuteObjectMessageCodec() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encodeToWire(Buffer buffer, ExecuteObject t) {
|
||||
realCodec.encodeToWire(buffer, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecuteObject<T> decodeFromWire(int pos, Buffer buffer) {
|
||||
return new ExecuteObject<>(pos, buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecuteObject<T> transform(ExecuteObject t) {
|
||||
// If a message is sent *locally* across the event bus.
|
||||
// This sends message just as is
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "ExecuteObjectCodec";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte systemCodecID() {
|
||||
// Always -1
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class LazyTdResultListMessageCodec implements MessageCodec<TdResultList, TdResultList> {
|
||||
|
||||
private final String codecName;
|
||||
private static final TdResultListMessageCodec realCodec = new TdResultListMessageCodec();
|
||||
|
||||
public LazyTdResultListMessageCodec() {
|
||||
super();
|
||||
this.codecName = "TdOptListCodec";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encodeToWire(Buffer buffer, TdResultList t) {
|
||||
realCodec.encodeToWire(buffer, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TdResultList decodeFromWire(int pos, Buffer buffer) {
|
||||
return new TdResultList(pos, buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TdResultList transform(TdResultList t) {
|
||||
// If a message is sent *locally* across the event bus.
|
||||
// This sends message just as is
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return codecName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte systemCodecID() {
|
||||
// Always -1
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class LazyTdResultMessageCodec implements MessageCodec<TdResultMessage, TdResultMessage> {
|
||||
|
||||
private final String codecName;
|
||||
private static final TdResultMessageCodec realCodec = new TdResultMessageCodec();
|
||||
|
||||
public LazyTdResultMessageCodec() {
|
||||
super();
|
||||
this.codecName = "TdResultCodec";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encodeToWire(Buffer buffer, TdResultMessage t) {
|
||||
realCodec.encodeToWire(buffer, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TdResultMessage decodeFromWire(int pos, Buffer buffer) {
|
||||
return new TdResultMessage(pos, buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TdResultMessage transform(TdResultMessage t) {
|
||||
// If a message is sent *locally* across the event bus.
|
||||
// This sends message just as is
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return codecName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte systemCodecID() {
|
||||
// Always -1
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.reactivex.core.buffer.Buffer;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public final class StartSessionMessage {
|
||||
|
||||
private final long id;
|
||||
private final String alias;
|
||||
private final Buffer binlog;
|
||||
private final long binlogDate;
|
||||
private final JsonObject implementationDetails;
|
||||
|
||||
public StartSessionMessage(long id, String alias, Buffer binlog, long binlogDate, JsonObject implementationDetails) {
|
||||
this.id = id;
|
||||
this.alias = alias;
|
||||
this.binlog = binlog;
|
||||
this.binlogDate = binlogDate;
|
||||
this.implementationDetails = implementationDetails;
|
||||
}
|
||||
|
||||
public long id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String alias() {
|
||||
return alias;
|
||||
}
|
||||
|
||||
public Buffer binlog() {
|
||||
return binlog;
|
||||
}
|
||||
|
||||
public long binlogDate() {
|
||||
return binlogDate;
|
||||
}
|
||||
|
||||
public JsonObject implementationDetails() {
|
||||
return implementationDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
StartSessionMessage that = (StartSessionMessage) o;
|
||||
return id == that.id && binlogDate == that.binlogDate && Objects.equals(alias, that.alias) && Objects.equals(binlog,
|
||||
that.binlog
|
||||
) && Objects.equals(implementationDetails, that.implementationDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, alias, binlog, binlogDate, implementationDetails);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", StartSessionMessage.class.getSimpleName() + "[", "]")
|
||||
.add("id=" + id)
|
||||
.add("alias='" + alias + "'")
|
||||
.add("binlog=" + binlog)
|
||||
.add("binlogDate=" + binlogDate)
|
||||
.add("implementationDetails=" + implementationDetails)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import it.tdlight.utils.BufferUtils;
|
||||
import org.warp.commonutils.serialization.UTFUtils;
|
||||
|
||||
public class StartSessionMessageCodec implements MessageCodec<StartSessionMessage, StartSessionMessage> {
|
||||
|
||||
private final String codecName;
|
||||
|
||||
public StartSessionMessageCodec() {
|
||||
super();
|
||||
this.codecName = "StartSessionMessageCodec";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encodeToWire(Buffer buffer, StartSessionMessage t) {
|
||||
BufferUtils.encode(buffer, os -> {
|
||||
os.writeLong(t.id());
|
||||
UTFUtils.writeUTF(os, t.alias());
|
||||
BufferUtils.writeBuf(os, t.binlog());
|
||||
os.writeLong(t.binlogDate());
|
||||
UTFUtils.writeUTF(os, t.implementationDetails().toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public StartSessionMessage decodeFromWire(int pos, Buffer buffer) {
|
||||
return BufferUtils.decode(pos, buffer, is -> new StartSessionMessage(is.readLong(),
|
||||
UTFUtils.readUTF(is),
|
||||
BufferUtils.rxReadBuf(is),
|
||||
is.readLong(),
|
||||
new JsonObject(UTFUtils.readUTF(is))
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public StartSessionMessage transform(StartSessionMessage t) {
|
||||
// If a message is sent *locally* across the event bus.
|
||||
// This sends message just as is
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return codecName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte systemCodecID() {
|
||||
// Always -1
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -1,303 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import com.hazelcast.config.Config;
|
||||
import com.hazelcast.config.MapConfig;
|
||||
import com.hazelcast.config.MultiMapConfig;
|
||||
import com.hazelcast.config.cp.SemaphoreConfig;
|
||||
import io.vertx.core.DeploymentOptions;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.VertxOptions;
|
||||
import io.vertx.core.eventbus.DeliveryOptions;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
import io.vertx.core.http.ClientAuth;
|
||||
import io.vertx.core.metrics.MetricsOptions;
|
||||
import io.vertx.core.net.JksOptions;
|
||||
import io.vertx.core.spi.cluster.ClusterManager;
|
||||
import io.vertx.reactivex.core.Vertx;
|
||||
import io.vertx.reactivex.core.eventbus.EventBus;
|
||||
import io.vertx.reactivex.core.eventbus.Message;
|
||||
import io.vertx.reactivex.core.eventbus.MessageConsumer;
|
||||
import io.vertx.reactivex.core.shareddata.SharedData;
|
||||
import io.vertx.spi.cluster.hazelcast.HazelcastClusterManager;
|
||||
import it.tdlight.common.ConstructorDetector;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.utils.MonoUtils;
|
||||
import java.nio.channels.AlreadyBoundException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class TdClusterManager {
|
||||
|
||||
private static final AtomicBoolean definedMasterCluster = new AtomicBoolean(false);
|
||||
private static final AtomicBoolean definedNodesCluster = new AtomicBoolean(false);
|
||||
private final ClusterManager mgr;
|
||||
private final VertxOptions vertxOptions;
|
||||
private final Vertx vertx;
|
||||
private final boolean isMaster;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public TdClusterManager(ClusterManager mgr, VertxOptions vertxOptions, Vertx vertx, boolean isMaster) {
|
||||
this.isMaster = isMaster;
|
||||
this.mgr = mgr;
|
||||
this.vertxOptions = vertxOptions;
|
||||
this.vertx = vertx;
|
||||
|
||||
if (vertx != null && vertx.eventBus() != null) {
|
||||
vertx
|
||||
.eventBus()
|
||||
.getDelegate()
|
||||
.registerDefaultCodec(TdResultList.class, new TdResultListMessageCodec())
|
||||
.registerDefaultCodec(ExecuteObject.class, new TdExecuteObjectMessageCodec())
|
||||
.registerDefaultCodec(TdResultMessage.class, new TdResultMessageCodec())
|
||||
.registerDefaultCodec(StartSessionMessage.class, new StartSessionMessageCodec())
|
||||
.registerDefaultCodec(EndSessionMessage.class, new EndSessionMessageCodec());
|
||||
for (Class<?> declaredClass : TdApi.class.getDeclaredClasses()) {
|
||||
if (declaredClass.isAssignableFrom(declaredClass)) {
|
||||
vertx.eventBus().getDelegate().registerDefaultCodec(declaredClass, new TdMessageCodec(declaredClass));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Mono<TdClusterManager> ofMaster(@Nullable JksOptions keyStoreOptions,
|
||||
@Nullable JksOptions trustStoreOptions,
|
||||
boolean onlyLocal,
|
||||
String masterHostname,
|
||||
String netInterface,
|
||||
int port,
|
||||
Set<String> nodesAddresses) {
|
||||
if (definedMasterCluster.compareAndSet(false, true)) {
|
||||
var vertxOptions = new VertxOptions();
|
||||
netInterface = onlyLocal ? "127.0.0.1" : netInterface;
|
||||
Config cfg;
|
||||
if (!onlyLocal) {
|
||||
cfg = new Config();
|
||||
cfg.setInstanceName("Master");
|
||||
} else {
|
||||
cfg = null;
|
||||
}
|
||||
return of(cfg,
|
||||
vertxOptions,
|
||||
keyStoreOptions, trustStoreOptions, masterHostname, netInterface, port, nodesAddresses, true);
|
||||
} else {
|
||||
return Mono.error(new AlreadyBoundException());
|
||||
}
|
||||
}
|
||||
|
||||
public static Mono<TdClusterManager> ofNodes(@Nullable JksOptions keyStoreOptions,
|
||||
@Nullable JksOptions trustStoreOptions,
|
||||
boolean onlyLocal,
|
||||
String masterHostname,
|
||||
String netInterface,
|
||||
int port,
|
||||
Set<String> nodesAddresses) {
|
||||
return Mono.defer(() -> {
|
||||
if (definedNodesCluster.compareAndSet(false, true)) {
|
||||
var vertxOptions = new VertxOptions();
|
||||
var netInterfaceF = onlyLocal ? "127.0.0.1" : netInterface;
|
||||
Config cfg;
|
||||
if (!onlyLocal) {
|
||||
cfg = new Config();
|
||||
cfg.setInstanceName("Node-" + new Random().nextLong());
|
||||
} else {
|
||||
cfg = null;
|
||||
}
|
||||
return of(cfg, vertxOptions, keyStoreOptions, trustStoreOptions, masterHostname, netInterfaceF, port,
|
||||
nodesAddresses, false);
|
||||
} else {
|
||||
return Mono.error(new AlreadyBoundException());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static Mono<TdClusterManager> of(@Nullable Config cfg,
|
||||
VertxOptions vertxOptions,
|
||||
@Nullable JksOptions keyStoreOptions,
|
||||
@Nullable JksOptions trustStoreOptions,
|
||||
String masterHostname,
|
||||
String netInterface,
|
||||
int port,
|
||||
Set<String> nodesAddresses,
|
||||
boolean isMaster) {
|
||||
ClusterManager mgr;
|
||||
if (cfg != null) {
|
||||
cfg.getNetworkConfig().setPortCount(1);
|
||||
cfg.getNetworkConfig().setPort(port);
|
||||
cfg.getNetworkConfig().setPortAutoIncrement(false);
|
||||
cfg.getPartitionGroupConfig().setEnabled(false);
|
||||
cfg.addMapConfig(new MapConfig().setName("__vertx.haInfo").setBackupCount(1));
|
||||
cfg.addMapConfig(new MapConfig().setName("__vertx.nodeInfo").setBackupCount(1));
|
||||
cfg
|
||||
.getCPSubsystemConfig()
|
||||
.setCPMemberCount(0)
|
||||
.setSemaphoreConfigs(Map.of("__vertx.*", new SemaphoreConfig().setInitialPermits(1).setJDKCompatible(false)));
|
||||
cfg.addMultiMapConfig(new MultiMapConfig().setName("__vertx.subs").setBackupCount(1).setValueCollectionType("SET"));
|
||||
cfg.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false);
|
||||
cfg.getNetworkConfig().getJoin().getAwsConfig().setEnabled(false);
|
||||
cfg.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true);
|
||||
var addresses = new ArrayList<>(nodesAddresses);
|
||||
cfg.getNetworkConfig().getJoin().getTcpIpConfig().setMembers(addresses);
|
||||
cfg.getNetworkConfig().getInterfaces().clear();
|
||||
cfg.getNetworkConfig().getInterfaces().setInterfaces(Collections.singleton(netInterface)).setEnabled(true);
|
||||
cfg.getNetworkConfig().setOutboundPorts(Collections.singleton(0));
|
||||
|
||||
cfg.setProperty("hazelcast.logging.type", "slf4j");
|
||||
cfg.setProperty("hazelcast.wait.seconds.before.join", "0");
|
||||
cfg.setProperty("hazelcast.tcp.join.port.try.count", "5");
|
||||
cfg.setProperty("hazelcast.socket.bind.any", "false");
|
||||
cfg.setProperty("hazelcast.health.monitoring.level", "OFF");
|
||||
cfg.setClusterName("tdlib-session-container");
|
||||
mgr = new HazelcastClusterManager(cfg);
|
||||
vertxOptions.setClusterManager(mgr);
|
||||
vertxOptions.getEventBusOptions().setConnectTimeout(120000);
|
||||
//vertxOptions.getEventBusOptions().setIdleTimeout(60);
|
||||
//vertxOptions.getEventBusOptions().setSsl(false);
|
||||
|
||||
vertxOptions.getEventBusOptions().setSslHandshakeTimeout(120000).setSslHandshakeTimeoutUnit(TimeUnit.MILLISECONDS);
|
||||
if (keyStoreOptions != null && trustStoreOptions != null) {
|
||||
vertxOptions.getEventBusOptions().setKeyStoreOptions(keyStoreOptions);
|
||||
vertxOptions.getEventBusOptions().setTrustStoreOptions(trustStoreOptions);
|
||||
vertxOptions
|
||||
.getEventBusOptions()
|
||||
.setUseAlpn(true)
|
||||
.setSsl(true)
|
||||
.setEnabledSecureTransportProtocols(Set.of("TLSv1.3", "TLSv1.2"));
|
||||
} else {
|
||||
vertxOptions
|
||||
.getEventBusOptions()
|
||||
.setSsl(false);
|
||||
}
|
||||
vertxOptions.getEventBusOptions().setHost(masterHostname);
|
||||
vertxOptions.getEventBusOptions().setPort(port + 1);
|
||||
vertxOptions.getEventBusOptions().setClientAuth(ClientAuth.REQUIRED);
|
||||
} else {
|
||||
mgr = null;
|
||||
vertxOptions.setClusterManager(null);
|
||||
}
|
||||
|
||||
vertxOptions.setPreferNativeTransport(true);
|
||||
vertxOptions.setMetricsOptions(new MetricsOptions().setEnabled(false));
|
||||
// check for blocked threads every 5s
|
||||
vertxOptions.setBlockedThreadCheckInterval(5);
|
||||
vertxOptions.setBlockedThreadCheckIntervalUnit(TimeUnit.SECONDS);
|
||||
// warn if an event loop thread handler took more than 10s to execute
|
||||
vertxOptions.setMaxEventLoopExecuteTime(10);
|
||||
vertxOptions.setMaxEventLoopExecuteTimeUnit(TimeUnit.SECONDS);
|
||||
// warn if an worker thread handler took more than 10s to execute
|
||||
vertxOptions.setMaxWorkerExecuteTime(10);
|
||||
vertxOptions.setMaxWorkerExecuteTimeUnit(TimeUnit.SECONDS);
|
||||
// log the stack trace if an event loop or worker handler took more than 20s to execute
|
||||
vertxOptions.setWarningExceptionTime(100);
|
||||
vertxOptions.setWarningExceptionTimeUnit(TimeUnit.MILLISECONDS);
|
||||
|
||||
return Mono
|
||||
.defer(() -> {
|
||||
if (mgr != null) {
|
||||
return Vertx.rxClusteredVertx(vertxOptions).as(MonoUtils::toMono).subscribeOn(Schedulers.boundedElastic());
|
||||
} else {
|
||||
return Mono.just(Vertx.vertx(vertxOptions));
|
||||
}
|
||||
})
|
||||
.flatMap(vertx -> Mono
|
||||
.fromCallable(() -> new TdClusterManager(mgr, vertxOptions, vertx, isMaster))
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
);
|
||||
}
|
||||
|
||||
public Vertx getVertx() {
|
||||
return vertx;
|
||||
}
|
||||
|
||||
public EventBus getEventBus() {
|
||||
return vertx.eventBus();
|
||||
}
|
||||
|
||||
public VertxOptions getVertxOptions() {
|
||||
return vertxOptions;
|
||||
}
|
||||
|
||||
public DeliveryOptions newDeliveryOpts() {
|
||||
return new DeliveryOptions().setSendTimeout(120000);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param messageCodec
|
||||
* @param <T>
|
||||
* @return true if registered, false if already registered
|
||||
*/
|
||||
public <T> boolean registerCodec(MessageCodec<T, T> messageCodec) {
|
||||
try {
|
||||
vertx.eventBus().registerCodec(messageCodec);
|
||||
return true;
|
||||
} catch (IllegalStateException ex) {
|
||||
if (ex.getMessage().startsWith("Already a default codec registered for class")) {
|
||||
return false;
|
||||
}
|
||||
if (ex.getMessage().startsWith("Already a codec registered with name")) {
|
||||
return false;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message consumer against the specified address.
|
||||
* <p>
|
||||
* The returned consumer is not yet registered
|
||||
* at the address, registration will be effective when {@link MessageConsumer#handler(io.vertx.core.Handler)}
|
||||
* is called.
|
||||
*
|
||||
* @param address the address that it will register it at
|
||||
* @param localOnly if you want to receive only local messages
|
||||
* @return the event bus message consumer
|
||||
*/
|
||||
public <T> MessageConsumer<T> consumer(String address, boolean localOnly) {
|
||||
if (localOnly) {
|
||||
return vertx.eventBus().localConsumer(address);
|
||||
} else {
|
||||
return vertx.eventBus().consumer(address);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a consumer and register it against the specified address.
|
||||
*
|
||||
* @param address the address that will register it at
|
||||
* @param localOnly if you want to receive only local messages
|
||||
* @param handler the handler that will process the received messages
|
||||
*
|
||||
* @return the event bus message consumer
|
||||
*/
|
||||
public <T> MessageConsumer<T> consumer(String address, boolean localOnly, Handler<Message<T>> handler) {
|
||||
if (localOnly) {
|
||||
return vertx.eventBus().localConsumer(address, handler);
|
||||
} else {
|
||||
return vertx.eventBus().consumer(address, handler);
|
||||
}
|
||||
}
|
||||
|
||||
public DeploymentOptions newDeploymentOpts() {
|
||||
return new DeploymentOptions().setWorkerPoolName("td-main-pool");
|
||||
}
|
||||
|
||||
public SharedData getSharedData() {
|
||||
return vertx.sharedData();
|
||||
}
|
||||
|
||||
public Mono<Void> close() {
|
||||
return Mono.from(vertx.rxClose().toFlowable()).then(Mono.fromRunnable(() -> {
|
||||
if (isMaster) {
|
||||
definedMasterCluster.set(false);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.utils.BufferUtils;
|
||||
import java.time.Duration;
|
||||
|
||||
public class TdExecuteObjectMessageCodec<T extends TdApi.Object>
|
||||
implements MessageCodec<ExecuteObject<T>, ExecuteObject<T>> {
|
||||
|
||||
public TdExecuteObjectMessageCodec() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encodeToWire(Buffer buffer, ExecuteObject t) {
|
||||
BufferUtils.encode(buffer, os -> {
|
||||
os.writeBoolean(t.isExecuteDirectly());
|
||||
t.getRequest().serialize(os);
|
||||
os.writeLong(t.getTimeout().toMillis());
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public ExecuteObject<T> decodeFromWire(int pos, Buffer buffer) {
|
||||
return BufferUtils.decode(pos, buffer, is -> new ExecuteObject<T>(
|
||||
is.readBoolean(),
|
||||
(Function<T>) TdApi.Deserializer.deserialize(is),
|
||||
Duration.ofMillis(is.readLong())
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecuteObject<T> transform(ExecuteObject<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 "ExecuteObjectCodec";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte systemCodecID() {
|
||||
// Always -1
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.netty.buffer.ByteBufInputStream;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import java.io.IOException;
|
||||
|
||||
public class TdMessageCodec<T extends TdApi.Object> implements MessageCodec<T, T> {
|
||||
|
||||
private final Class<T> clazz;
|
||||
private final String codecName;
|
||||
|
||||
public TdMessageCodec(Class<T> clazz) {
|
||||
super();
|
||||
this.clazz = clazz;
|
||||
this.codecName = clazz.getSimpleName() + "TdCodec";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encodeToWire(Buffer buffer, T t) {
|
||||
try (var os = new ByteBufOutputStream(buffer.getByteBuf())) {
|
||||
t.serialize(os);
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T decodeFromWire(int pos, Buffer buffer) {
|
||||
try (var is = new ByteBufInputStream(buffer.getByteBuf(), pos)) {
|
||||
//noinspection unchecked
|
||||
return (T) TdApi.Deserializer.deserialize(is);
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T transform(T t) {
|
||||
// If a message is sent *locally* across the event bus.
|
||||
// This sends message just as is
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return codecName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte systemCodecID() {
|
||||
// Always -1
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Error;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public class TdResultList {
|
||||
|
||||
private static final TdResultListMessageCodec realCodec = new TdResultListMessageCodec();
|
||||
|
||||
private List<TdApi.Object> values;
|
||||
private Error error;
|
||||
private int pos;
|
||||
private Buffer buffer;
|
||||
|
||||
public TdResultList(List<TdApi.Object> values) {
|
||||
this.values = values;
|
||||
this.error = null;
|
||||
if (values == null) throw new NullPointerException("Null message");
|
||||
}
|
||||
|
||||
public TdResultList(TdApi.Error error) {
|
||||
this.values = null;
|
||||
this.error = error;
|
||||
if (error == null) throw new NullPointerException("Null message");
|
||||
}
|
||||
|
||||
public TdResultList(int pos, Buffer buffer) {
|
||||
this.pos = pos;
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
private void tryDecode() {
|
||||
if (error == null && values == null) {
|
||||
var value = realCodec.decodeFromWire(pos, buffer);
|
||||
this.values = value.values;
|
||||
this.error = value.error;
|
||||
this.buffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<TdApi.Object> value() {
|
||||
tryDecode();
|
||||
return values;
|
||||
}
|
||||
|
||||
public TdApi.Error error() {
|
||||
tryDecode();
|
||||
return error;
|
||||
}
|
||||
|
||||
public boolean succeeded() {
|
||||
tryDecode();
|
||||
return error == null && values != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
tryDecode();
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TdResultList that = (TdResultList) o;
|
||||
|
||||
if (!Objects.equals(values, that.values)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(error, that.error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
tryDecode();
|
||||
int result = values != null ? values.hashCode() : 0;
|
||||
result = 31 * result + (error != null ? error.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", TdResultList.class.getSimpleName() + "[", "]")
|
||||
.add("values=" + values)
|
||||
.add("error=" + error)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Error;
|
||||
import it.tdlight.utils.BufferUtils;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class TdResultListMessageCodec implements MessageCodec<TdResultList, TdResultList> {
|
||||
|
||||
public TdResultListMessageCodec() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encodeToWire(Buffer buffer, TdResultList ts) {
|
||||
BufferUtils.encode(buffer, os -> {
|
||||
if (ts.succeeded()) {
|
||||
os.writeBoolean(true);
|
||||
var t = ts.value();
|
||||
os.writeInt(t.size());
|
||||
for (TdApi.Object t1 : t) {
|
||||
t1.serialize(os);
|
||||
}
|
||||
} else {
|
||||
os.writeBoolean(false);
|
||||
ts.error().serialize(os);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public TdResultList decodeFromWire(int pos, Buffer buffer) {
|
||||
return BufferUtils.decode(pos, buffer, is -> {
|
||||
if (is.readBoolean()) {
|
||||
var size = is.readInt();
|
||||
ArrayList<TdApi.Object> list = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
list.add((TdApi.Object) TdApi.Deserializer.deserialize(is));
|
||||
}
|
||||
return new TdResultList(list);
|
||||
} else {
|
||||
return new TdResultList((Error) TdApi.Deserializer.deserialize(is));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public TdResultList transform(TdResultList ts) {
|
||||
return ts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "TdOptListCodec";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte systemCodecID() {
|
||||
// Always -1
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Error;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.tdlibsession.td.TdResult;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public class TdResultMessage {
|
||||
|
||||
private static final TdResultMessageCodec realCodec = new TdResultMessageCodec();
|
||||
|
||||
public TdApi.Object value;
|
||||
public TdApi.Error cause;
|
||||
private int pos;
|
||||
private Buffer buffer;
|
||||
|
||||
public TdResultMessage(Object value, Error cause) {
|
||||
this.value = value;
|
||||
this.cause = cause;
|
||||
if (value == null && cause == null) throw new NullPointerException("Null message");
|
||||
}
|
||||
|
||||
public TdResultMessage(int pos, Buffer buffer) {
|
||||
this.pos = pos;
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
public <T extends Object> TdResult<T> toTdResult() {
|
||||
if (value != null) {
|
||||
//noinspection unchecked
|
||||
return TdResult.succeeded((T) value);
|
||||
} else if (cause != null) {
|
||||
return TdResult.failed(cause);
|
||||
} else {
|
||||
var data = realCodec.decodeFromWire(pos, buffer);
|
||||
this.value = data.value;
|
||||
this.cause = data.cause;
|
||||
this.buffer = null;
|
||||
if (value != null) {
|
||||
//noinspection unchecked
|
||||
return TdResult.succeeded((T) value);
|
||||
} else {
|
||||
return TdResult.failed(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringJoiner(", ", TdResultMessage.class.getSimpleName() + "[", "]")
|
||||
.add("value=" + value)
|
||||
.add("cause=" + cause)
|
||||
.toString();
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.eventbus.MessageCodec;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.utils.BufferUtils;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class TdResultMessageCodec implements MessageCodec<TdResultMessage, TdResultMessage> {
|
||||
|
||||
private final String codecName;
|
||||
|
||||
public TdResultMessageCodec() {
|
||||
super();
|
||||
this.codecName = "TdResultCodec";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encodeToWire(Buffer buffer, TdResultMessage t) {
|
||||
BufferUtils.encode(buffer, os -> {
|
||||
if (t.value != null) {
|
||||
os.writeBoolean(true);
|
||||
t.value.serialize(os);
|
||||
} else {
|
||||
os.writeBoolean(false);
|
||||
t.cause.serialize(os);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public TdResultMessage decodeFromWire(int pos, Buffer buffer) {
|
||||
return BufferUtils.decode(pos, buffer, is -> {
|
||||
if (is.readBoolean()) {
|
||||
return new TdResultMessage(TdApi.Deserializer.deserialize(is), null);
|
||||
} else {
|
||||
return new TdResultMessage(null, (TdApi.Error) TdApi.Deserializer.deserialize(is));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public TdResultMessage transform(TdResultMessage t) {
|
||||
// If a message is sent *locally* across the event bus.
|
||||
// This sends message just as is
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return codecName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte systemCodecID() {
|
||||
// Always -1
|
||||
return -1;
|
||||
}
|
||||
}
|
@ -1,366 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle.client;
|
||||
|
||||
import io.vertx.core.eventbus.DeliveryOptions;
|
||||
import io.vertx.core.eventbus.ReplyException;
|
||||
import io.vertx.core.eventbus.ReplyFailure;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.reactivex.core.Vertx;
|
||||
import io.vertx.reactivex.core.buffer.Buffer;
|
||||
import io.vertx.reactivex.core.eventbus.Message;
|
||||
import io.vertx.reactivex.core.eventbus.MessageConsumer;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
|
||||
import it.tdlight.tdlibsession.td.ResponseError;
|
||||
import it.tdlight.tdlibsession.td.TdError;
|
||||
import it.tdlight.tdlibsession.td.TdResult;
|
||||
import it.tdlight.tdlibsession.td.middle.TdResultMessage;
|
||||
import it.tdlight.tdlibsession.td.middle.AsyncTdMiddle;
|
||||
import it.tdlight.tdlibsession.td.middle.EndSessionMessage;
|
||||
import it.tdlight.tdlibsession.td.middle.ExecuteObject;
|
||||
import it.tdlight.tdlibsession.td.middle.StartSessionMessage;
|
||||
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
|
||||
import it.tdlight.tdlibsession.td.middle.TdResultList;
|
||||
import it.tdlight.utils.BinlogAsyncFile;
|
||||
import it.tdlight.utils.BinlogUtils;
|
||||
import it.tdlight.utils.MonoUtils;
|
||||
import java.net.ConnectException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import org.warp.commonutils.locks.LockUtils;
|
||||
import org.warp.commonutils.log.Logger;
|
||||
import org.warp.commonutils.log.LoggerFactory;
|
||||
import reactor.adapter.rxjava.RxJava2Adapter;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.EmitResult;
|
||||
import reactor.core.publisher.Sinks.Empty;
|
||||
import reactor.core.publisher.Sinks.One;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class AsyncTdMiddleEventBusClient implements AsyncTdMiddle {
|
||||
|
||||
private Logger logger;
|
||||
|
||||
public static final byte[] EMPTY = new byte[0];
|
||||
|
||||
private final TdClusterManager cluster;
|
||||
private final DeliveryOptions deliveryOptions;
|
||||
private final DeliveryOptions deliveryOptionsWithTimeout;
|
||||
private final DeliveryOptions pingDeliveryOptions;
|
||||
|
||||
private final AtomicReference<BinlogAsyncFile> binlog = new AtomicReference<>();
|
||||
|
||||
private final AtomicReference<Disposable> pinger = new AtomicReference<>();
|
||||
|
||||
private final AtomicReference<MessageConsumer<TdResultList>> updates = new AtomicReference<>();
|
||||
// This will only result in a successful completion, never completes in other ways
|
||||
private final Empty<Void> updatesStreamEnd = Sinks.empty();
|
||||
// This will only result in a crash, never completes in other ways
|
||||
private final AtomicReference<Throwable> crash = new AtomicReference<>();
|
||||
// This will only result in a successful completion, never completes in other ways
|
||||
private final Empty<Void> pingFail = Sinks.empty();
|
||||
|
||||
private long botId;
|
||||
private String botAddress;
|
||||
private String botAlias;
|
||||
private boolean local;
|
||||
|
||||
public AsyncTdMiddleEventBusClient(TdClusterManager clusterManager) {
|
||||
this.logger = LoggerFactory.getLogger(AsyncTdMiddleEventBusClient.class);
|
||||
this.cluster = clusterManager;
|
||||
this.deliveryOptions = cluster.newDeliveryOpts().setLocalOnly(local);
|
||||
this.deliveryOptionsWithTimeout = cluster.newDeliveryOpts().setLocalOnly(local).setSendTimeout(30000);
|
||||
this.pingDeliveryOptions = cluster.newDeliveryOpts().setLocalOnly(local).setSendTimeout(60000);
|
||||
}
|
||||
|
||||
private Mono<AsyncTdMiddleEventBusClient> initializeEb() {
|
||||
return Mono.just(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> initialize() {
|
||||
// Do nothing here.
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
public static Mono<AsyncTdMiddle> getAndDeployInstance(TdClusterManager clusterManager,
|
||||
long botId,
|
||||
String botAlias,
|
||||
boolean local,
|
||||
JsonObject implementationDetails,
|
||||
Path binlogsArchiveDirectory) {
|
||||
return new AsyncTdMiddleEventBusClient(clusterManager)
|
||||
.initializeEb()
|
||||
.flatMap(instance -> retrieveBinlog(clusterManager.getVertx(), binlogsArchiveDirectory, botId)
|
||||
.flatMap(binlog -> binlog
|
||||
.getLastModifiedTime()
|
||||
.filter(modTime -> modTime == 0)
|
||||
.doOnNext(v -> LoggerFactory
|
||||
.getLogger(AsyncTdMiddleEventBusClient.class)
|
||||
.error("Can't retrieve binlog of bot " + botId + " " + botAlias + ". Creating a new one..."))
|
||||
.thenReturn(binlog)).<AsyncTdMiddle>flatMap(binlog -> instance
|
||||
.start(botId, botAlias, local, implementationDetails, binlog)
|
||||
.thenReturn(instance)
|
||||
)
|
||||
.single()
|
||||
)
|
||||
.single();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return optional result
|
||||
*/
|
||||
public static Mono<BinlogAsyncFile> retrieveBinlog(Vertx vertx, Path binlogsArchiveDirectory, long botId) {
|
||||
return BinlogUtils.retrieveBinlog(vertx.fileSystem(), binlogsArchiveDirectory.resolve(botId + ".binlog"));
|
||||
}
|
||||
|
||||
private Mono<Void> saveBinlog(Buffer data) {
|
||||
return Mono.fromSupplier(this.binlog::get).flatMap(binlog -> BinlogUtils.saveBinlog(binlog, data));
|
||||
}
|
||||
|
||||
public Mono<Void> start(long botId,
|
||||
String botAlias,
|
||||
boolean local,
|
||||
JsonObject implementationDetails,
|
||||
BinlogAsyncFile binlog) {
|
||||
this.botId = botId;
|
||||
this.botAlias = botAlias;
|
||||
this.botAddress = "bots.bot." + this.botId;
|
||||
this.local = local;
|
||||
this.logger = LoggerFactory.getLogger(this.botId + " " + botAlias);
|
||||
|
||||
return Mono
|
||||
.fromRunnable(() -> this.binlog.set(binlog))
|
||||
.then(binlog.getLastModifiedTime())
|
||||
.zipWith(binlog.readFully().map(Buffer::getDelegate))
|
||||
.single()
|
||||
.flatMap(tuple -> {
|
||||
var binlogLastModifiedTime = tuple.getT1();
|
||||
var binlogData = tuple.getT2();
|
||||
|
||||
var msg = new StartSessionMessage(this.botId,
|
||||
this.botAlias,
|
||||
Buffer.newInstance(binlogData),
|
||||
binlogLastModifiedTime,
|
||||
implementationDetails
|
||||
);
|
||||
|
||||
Mono<Void> startBotRequest;
|
||||
|
||||
if (local) {
|
||||
startBotRequest = Mono.empty();
|
||||
} else {
|
||||
startBotRequest = cluster
|
||||
.getEventBus()
|
||||
.<byte[]>rxRequest("bots.start-bot", msg)
|
||||
.to(RxJava2Adapter::singleToMono)
|
||||
.doOnSuccess(s -> logger.trace("bots.start-bot returned successfully"))
|
||||
.doFirst(() -> logger.trace("Requesting bots.start-bot"))
|
||||
.onErrorMap(ex -> {
|
||||
if (ex instanceof ReplyException) {
|
||||
if (((ReplyException) ex).failureType() == ReplyFailure.NO_HANDLERS) {
|
||||
return new NoClustersAvailableException("Can't start bot "
|
||||
+ botId + " " + botAlias);
|
||||
}
|
||||
}
|
||||
return ex;
|
||||
})
|
||||
.then()
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
return setupUpdatesListener().then(startBotRequest).then(setupPing());
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> setupPing() {
|
||||
// Disable ping on local servers
|
||||
if (local) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
var pingRequest = cluster.getEventBus()
|
||||
.<byte[]>rxRequest(botAddress + ".ping", EMPTY, pingDeliveryOptions)
|
||||
.to(RxJava2Adapter::singleToMono)
|
||||
.doFirst(() -> logger.trace("Requesting ping..."));
|
||||
|
||||
return Mono
|
||||
.fromRunnable(() -> pinger.set(pingRequest
|
||||
.flatMap(msg -> Mono.fromCallable(msg::body).subscribeOn(Schedulers.boundedElastic()))
|
||||
.repeatWhen(l -> l.delayElements(Duration.ofSeconds(10)).takeWhile(x -> true))
|
||||
.doOnNext(s -> logger.trace("PING"))
|
||||
.then()
|
||||
.onErrorResume(ex -> {
|
||||
logger.warn("Ping failed: {}", ex.getMessage());
|
||||
return Mono.empty();
|
||||
})
|
||||
.doOnNext(s -> logger.debug("END PING"))
|
||||
.then(Mono.fromRunnable(() -> {
|
||||
while (this.pingFail.tryEmitEmpty() == EmitResult.FAIL_NON_SERIALIZED) {
|
||||
// 10ms
|
||||
LockSupport.parkNanos(10000000);
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic()))
|
||||
.subscribeOn(Schedulers.parallel())
|
||||
.subscribe())
|
||||
)
|
||||
.then()
|
||||
.doFirst(() -> logger.trace("Setting up ping"))
|
||||
.doOnSuccess(s -> logger.trace("Ping setup success"))
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
private Mono<Void> setupUpdatesListener() {
|
||||
return Mono
|
||||
.fromRunnable(() -> logger.trace("Setting up updates listener..."))
|
||||
.then(Mono.<MessageConsumer<TdResultList>>fromSupplier(() -> MessageConsumer
|
||||
.newInstance(cluster.getEventBus().<TdResultList>consumer(botAddress + ".updates")
|
||||
.setMaxBufferedMessages(5000)
|
||||
.getDelegate()
|
||||
))
|
||||
)
|
||||
.flatMap(updateConsumer -> {
|
||||
// Return when the registration of all the consumers has been done across the cluster
|
||||
return Mono
|
||||
.fromRunnable(() -> logger.trace("Emitting updates flux to sink"))
|
||||
.then(Mono.fromRunnable(() -> {
|
||||
var previous = this.updates.getAndSet(updateConsumer);
|
||||
if (previous != null) {
|
||||
logger.error("Already subscribed a consumer to the updates");
|
||||
}
|
||||
}))
|
||||
.doOnSuccess(s -> logger.trace("Emitted updates flux to sink"))
|
||||
.doOnSuccess(s -> logger.trace("Waiting to register update consumer across the cluster"))
|
||||
.doOnSuccess(s -> logger.trace("Registered update consumer across the cluster"));
|
||||
})
|
||||
.doOnSuccess(s ->logger.trace("Set up updates listener"))
|
||||
.then();
|
||||
}
|
||||
|
||||
@SuppressWarnings("Convert2MethodRef")
|
||||
@Override
|
||||
public Flux<TdApi.Object> receive() {
|
||||
// Here the updates will be received
|
||||
|
||||
return Mono
|
||||
.fromRunnable(() -> logger.trace("Called receive() from parent"))
|
||||
.then(Mono.fromCallable(() -> updates.get()))
|
||||
.doOnSuccess(s -> logger.trace("Registering updates flux"))
|
||||
.flatMapMany(updatesMessageConsumer -> MonoUtils.fromMessageConsumer(Mono
|
||||
.empty()
|
||||
.doOnSuccess(s -> logger.trace("Sending ready-to-receive"))
|
||||
.then(cluster.getEventBus().<byte[]>rxRequest(botAddress + ".ready-to-receive",
|
||||
EMPTY,
|
||||
deliveryOptionsWithTimeout
|
||||
).to(RxJava2Adapter::singleToMono))
|
||||
.doOnSuccess(s -> logger.trace("Sent ready-to-receive, received reply"))
|
||||
.doOnSuccess(s -> logger.trace("About to read updates flux"))
|
||||
.then(), updatesMessageConsumer)
|
||||
)
|
||||
.takeUntilOther(pingFail.asMono())
|
||||
.takeUntil(a -> a.succeeded() && a.value().stream().anyMatch(item -> {
|
||||
if (item.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
|
||||
return ((UpdateAuthorizationState) item).authorizationState.getConstructor()
|
||||
== AuthorizationStateClosed.CONSTRUCTOR;
|
||||
}
|
||||
return false;
|
||||
}))
|
||||
.flatMapSequential(updates -> {
|
||||
if (updates.succeeded()) {
|
||||
return Flux.fromIterable(updates.value());
|
||||
} else {
|
||||
return Mono.fromCallable(() -> TdResult.failed(updates.error()).orElseThrow());
|
||||
}
|
||||
})
|
||||
.concatMap(update -> interceptUpdate(update))
|
||||
// Redirect errors to crash sink
|
||||
.doOnError(error -> crash.compareAndSet(null, error))
|
||||
.onErrorResume(ex -> {
|
||||
logger.trace("Absorbing the error, the error has been published using the crash sink", ex);
|
||||
return Mono.empty();
|
||||
})
|
||||
.doOnCancel(() -> {
|
||||
})
|
||||
.doFinally(s -> {
|
||||
var pinger = this.pinger.get();
|
||||
if (pinger != null) {
|
||||
pinger.dispose();
|
||||
}
|
||||
updatesStreamEnd.tryEmitEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<TdApi.Object> interceptUpdate(Object update) {
|
||||
logger.trace("Received update {}", update.getClass().getSimpleName());
|
||||
if (update.getConstructor() == TdApi.UpdateAuthorizationState.CONSTRUCTOR) {
|
||||
var updateAuthorizationState = (TdApi.UpdateAuthorizationState) update;
|
||||
if (updateAuthorizationState.authorizationState.getConstructor() == AuthorizationStateClosed.CONSTRUCTOR) {
|
||||
logger.info("Received AuthorizationStateClosed from tdlib");
|
||||
|
||||
var pinger = this.pinger.get();
|
||||
if (pinger != null) {
|
||||
pinger.dispose();
|
||||
}
|
||||
|
||||
return cluster.getEventBus()
|
||||
.<EndSessionMessage>rxRequest(this.botAddress + ".read-binlog", EMPTY)
|
||||
.to(RxJava2Adapter::singleToMono)
|
||||
.mapNotNull(Message::body)
|
||||
.doOnNext(latestBinlog -> logger.info("Received binlog from server. Size: {}",
|
||||
BinlogUtils.humanReadableByteCountBin(latestBinlog.binlog().length())
|
||||
))
|
||||
.flatMap(latestBinlog -> this.saveBinlog(latestBinlog.binlog()))
|
||||
.doOnSuccess(s -> logger.info("Overwritten binlog from server"))
|
||||
.doFirst(() -> logger.info("Asking binlog to server"))
|
||||
.thenReturn(update);
|
||||
}
|
||||
}
|
||||
return Mono.just(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends TdApi.Object> Mono<TdResult<T>> execute(Function<T> request, Duration timeout,
|
||||
boolean executeSync) {
|
||||
var req = new ExecuteObject<>(executeSync, request, timeout);
|
||||
var deliveryOptions = new DeliveryOptions(this.deliveryOptions)
|
||||
// Timeout + 5s (5 seconds extra are used to wait the graceful server-side timeout response)
|
||||
.setSendTimeout(timeout.toMillis() + 5000);
|
||||
|
||||
var executionMono = cluster.getEventBus()
|
||||
.<TdResultMessage>rxRequest(botAddress + ".execute", req, deliveryOptions)
|
||||
.to(RxJava2Adapter::singleToMono)
|
||||
.onErrorMap(ex -> ResponseError.newResponseError(request, botAlias, ex))
|
||||
.<TdResult<T>>handle((resp, sink) -> {
|
||||
if (resp.body() == null) {
|
||||
var tdError = new TdError(500, "Response is empty");
|
||||
sink.error(ResponseError.newResponseError(request, botAlias, tdError));
|
||||
} else {
|
||||
sink.next(resp.body().toTdResult());
|
||||
}
|
||||
})
|
||||
.doFirst(() -> logger.trace("Executing request {}", request))
|
||||
.doOnSuccess(s -> logger.trace("Executed request {}", request))
|
||||
.doOnError(ex -> logger.debug("Failed request {}: {}", req, ex));
|
||||
|
||||
return executionMono
|
||||
.transformDeferred(mono -> {
|
||||
var crash = this.crash.get();
|
||||
if (crash != null) {
|
||||
logger.debug("Failed request {} because the TDLib session was already crashed", request);
|
||||
return Mono.empty();
|
||||
} else {
|
||||
return mono;
|
||||
}
|
||||
})
|
||||
.switchIfEmpty(Mono.error(() -> ResponseError.newResponseError(request, botAlias,
|
||||
new TdError(500, "The client is closed or the response is empty"))));
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle.client;
|
||||
|
||||
public class NoClustersAvailableException extends Throwable {
|
||||
|
||||
public NoClustersAvailableException(String error) {
|
||||
super(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "No clusters are available. " + this.getMessage();
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle.direct;
|
||||
|
||||
import static it.tdlight.tdlibsession.td.middle.server.AsyncTdMiddleEventBusServer.WAIT_DURATION;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.reactivex.core.AbstractVerticle;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.tdlibsession.td.ResponseError;
|
||||
import it.tdlight.tdlibsession.td.TdResult;
|
||||
import it.tdlight.tdlibsession.td.direct.AsyncTdDirectImpl;
|
||||
import it.tdlight.tdlibsession.td.direct.AsyncTdDirectOptions;
|
||||
import it.tdlight.tdlibsession.td.direct.TelegramClientFactory;
|
||||
import it.tdlight.tdlibsession.td.middle.AsyncTdMiddle;
|
||||
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
|
||||
import it.tdlight.utils.MonoUtils;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.warp.commonutils.log.Logger;
|
||||
import org.warp.commonutils.log.LoggerFactory;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.Empty;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
public class AsyncTdMiddleDirect extends AbstractVerticle implements AsyncTdMiddle {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AsyncTdMiddleDirect.class);
|
||||
|
||||
private final TelegramClientFactory clientFactory;
|
||||
|
||||
protected AsyncTdDirectImpl td;
|
||||
private String botAddress;
|
||||
private String botAlias;
|
||||
private final Empty<Object> closeRequest = Sinks.empty();
|
||||
|
||||
public AsyncTdMiddleDirect() {
|
||||
this.clientFactory = new TelegramClientFactory();
|
||||
}
|
||||
|
||||
public static Mono<AsyncTdMiddle> getAndDeployInstance(TdClusterManager clusterManager,
|
||||
String botAlias,
|
||||
String botAddress,
|
||||
JsonObject implementationDetails) {
|
||||
var instance = new AsyncTdMiddleDirect();
|
||||
var options = clusterManager.newDeploymentOpts().setConfig(new JsonObject()
|
||||
.put("botAlias", botAlias)
|
||||
.put("botAddress", botAddress)
|
||||
.put("implementationDetails", implementationDetails));
|
||||
return clusterManager.getVertx()
|
||||
.rxDeployVerticle(instance, options)
|
||||
.as(MonoUtils::toMono)
|
||||
.doOnNext(_v -> logger.trace("Deployed verticle for bot " + botAlias + ", address: " + botAddress))
|
||||
.thenReturn(instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable rxStart() {
|
||||
var botAddress = config().getString("botAddress");
|
||||
if (botAddress == null || botAddress.isEmpty()) {
|
||||
throw new IllegalArgumentException("botAddress is not set!");
|
||||
}
|
||||
this.botAddress = botAddress;
|
||||
var botAlias = config().getString("botAlias");
|
||||
if (botAlias == null || botAlias.isEmpty()) {
|
||||
throw new IllegalArgumentException("botAlias is not set!");
|
||||
}
|
||||
this.botAlias = botAlias;
|
||||
var implementationDetails = config().getJsonObject("implementationDetails");
|
||||
if (implementationDetails == null) {
|
||||
throw new IllegalArgumentException("implementationDetails is not set!");
|
||||
}
|
||||
|
||||
this.td = new AsyncTdDirectImpl(clientFactory, implementationDetails, botAlias);
|
||||
|
||||
return Completable.complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable rxStop() {
|
||||
return Completable.fromRunnable(closeRequest::tryEmitEmpty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> initialize() {
|
||||
return td.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<TdApi.Object> receive() {
|
||||
return td
|
||||
.receive(new AsyncTdDirectOptions(WAIT_DURATION, 100))
|
||||
.takeUntilOther(closeRequest.asMono())
|
||||
.doOnNext(s -> logger.trace("Received update from tdlib: {}", s.getClass().getSimpleName()))
|
||||
.doOnError(ex -> logger.info("TdMiddle verticle error", ex))
|
||||
.doOnTerminate(() -> logger.debug("TdMiddle verticle stopped"))
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Object> Mono<TdResult<T>> execute(Function<T> requestFunction,
|
||||
Duration timeout,
|
||||
boolean executeDirectly) {
|
||||
return td
|
||||
.<T>execute(requestFunction, timeout, executeDirectly)
|
||||
.onErrorMap(error -> ResponseError.newResponseError(requestFunction, botAlias, error));
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle.direct;
|
||||
|
||||
import io.vertx.core.DeploymentOptions;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.reactivex.core.Vertx;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import it.tdlight.tdlibsession.td.TdResult;
|
||||
import it.tdlight.tdlibsession.td.middle.AsyncTdMiddle;
|
||||
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
|
||||
import it.tdlight.tdlibsession.td.middle.client.AsyncTdMiddleEventBusClient;
|
||||
import it.tdlight.tdlibsession.td.middle.server.AsyncTdMiddleEventBusServer;
|
||||
import it.tdlight.utils.MonoUtils;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.warp.commonutils.error.InitializationException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.One;
|
||||
|
||||
public class AsyncTdMiddleLocal implements AsyncTdMiddle {
|
||||
|
||||
private final TdClusterManager masterClusterManager;
|
||||
private final String botAlias;
|
||||
private final int botId;
|
||||
private final DeploymentOptions deploymentOptions;
|
||||
private final Vertx vertx;
|
||||
|
||||
private final AsyncTdMiddleEventBusServer srv;
|
||||
private final AtomicReference<AsyncTdMiddle> cli = new AtomicReference<>(null);
|
||||
private final AtomicReference<Throwable> startError = new AtomicReference<>(null);
|
||||
private final JsonObject implementationDetails;
|
||||
|
||||
|
||||
public AsyncTdMiddleLocal(TdClusterManager masterClusterManager,
|
||||
String botAlias,
|
||||
int botId,
|
||||
JsonObject implementationDetails) {
|
||||
this.masterClusterManager = masterClusterManager;
|
||||
this.botAlias = botAlias;
|
||||
this.botId = botId;
|
||||
this.implementationDetails = implementationDetails;
|
||||
this.vertx = masterClusterManager.getVertx();
|
||||
this.deploymentOptions = masterClusterManager
|
||||
.newDeploymentOpts()
|
||||
.setConfig(new JsonObject()
|
||||
.put("botId", botId)
|
||||
.put("botAlias", botAlias)
|
||||
.put("local", true)
|
||||
.put("implementationDetails", implementationDetails)
|
||||
);
|
||||
this.srv = new AsyncTdMiddleEventBusServer();
|
||||
}
|
||||
|
||||
public Mono<AsyncTdMiddleLocal> start() {
|
||||
return vertx
|
||||
.rxDeployVerticle(srv, deploymentOptions).as(MonoUtils::toMono)
|
||||
.single()
|
||||
.then(Mono.fromSupplier(() -> new AsyncTdMiddleEventBusClient(masterClusterManager)))
|
||||
.zipWith(AsyncTdMiddleEventBusClient.retrieveBinlog(vertx, Path.of("binlogs"), botId))
|
||||
.flatMap(tuple -> tuple
|
||||
.getT1()
|
||||
.start(botId, botAlias, true, implementationDetails, tuple.getT2())
|
||||
.thenReturn(tuple.getT1()))
|
||||
.onErrorMap(InitializationException::new)
|
||||
.doOnNext(this.cli::set)
|
||||
.doOnError(this.startError::set)
|
||||
.thenReturn(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> initialize() {
|
||||
var startError = this.startError.get();
|
||||
if (startError != null) {
|
||||
return Mono.error(startError);
|
||||
}
|
||||
return Mono.fromCallable(cli::get).single().flatMap(AsyncTdMiddle::initialize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<TdApi.Object> receive() {
|
||||
var startError = this.startError.get();
|
||||
if (startError != null) {
|
||||
return Flux.error(startError);
|
||||
}
|
||||
return Mono.fromCallable(cli::get).single().flatMapMany(AsyncTdMiddle::receive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Object> Mono<TdResult<T>> execute(Function<T> request, Duration timeout, boolean executeDirectly) {
|
||||
var startError = this.startError.get();
|
||||
if (startError != null) {
|
||||
return Mono.error(startError);
|
||||
}
|
||||
return Mono.fromCallable(cli::get).single().flatMap(c -> c.execute(request, timeout, executeDirectly));
|
||||
}
|
||||
}
|
@ -1,402 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle.server;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.processors.BehaviorProcessor;
|
||||
import io.vertx.core.eventbus.DeliveryOptions;
|
||||
import io.vertx.core.eventbus.ReplyException;
|
||||
import io.vertx.core.eventbus.ReplyFailure;
|
||||
import io.vertx.core.streams.Pipe;
|
||||
import io.vertx.core.streams.Pump;
|
||||
import io.vertx.ext.reactivestreams.impl.ReactiveWriteStreamImpl;
|
||||
import io.vertx.reactivex.RxHelper;
|
||||
import io.vertx.reactivex.WriteStreamObserver;
|
||||
import io.vertx.reactivex.core.AbstractVerticle;
|
||||
import io.vertx.reactivex.core.eventbus.Message;
|
||||
import io.vertx.reactivex.core.eventbus.MessageConsumer;
|
||||
import io.vertx.reactivex.core.eventbus.MessageProducer;
|
||||
import io.vertx.reactivex.core.streams.WriteStream;
|
||||
import io.vertx.reactivex.impl.FlowableReadStream;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
|
||||
import it.tdlight.jni.TdApi.Error;
|
||||
import it.tdlight.jni.TdApi.Function;
|
||||
import it.tdlight.jni.TdApi.SetTdlibParameters;
|
||||
import it.tdlight.jni.TdApi.Update;
|
||||
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
|
||||
import it.tdlight.tdlibsession.remoteclient.TDLibRemoteClient;
|
||||
import it.tdlight.tdlibsession.td.TdError;
|
||||
import it.tdlight.tdlibsession.td.direct.AsyncTdDirectImpl;
|
||||
import it.tdlight.tdlibsession.td.direct.AsyncTdDirectOptions;
|
||||
import it.tdlight.tdlibsession.td.direct.TelegramClientFactory;
|
||||
import it.tdlight.tdlibsession.td.middle.ExecuteObject;
|
||||
import it.tdlight.tdlibsession.td.middle.TdResultList;
|
||||
import it.tdlight.tdlibsession.td.middle.TdResultListMessageCodec;
|
||||
import it.tdlight.tdlibsession.td.middle.TdResultMessage;
|
||||
import it.tdlight.utils.BinlogUtils;
|
||||
import java.net.ConnectException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.warp.commonutils.log.Logger;
|
||||
import org.warp.commonutils.log.LoggerFactory;
|
||||
import reactor.adapter.rxjava.RxJava2Adapter;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
public class AsyncTdMiddleEventBusServer extends AbstractVerticle {
|
||||
|
||||
// Static values
|
||||
protected static final Logger logger = LoggerFactory.getLogger("TdMiddleServer");
|
||||
public static final byte[] EMPTY = new byte[0];
|
||||
public static final Duration WAIT_DURATION = Duration.ofSeconds(1);
|
||||
|
||||
// Values configured from constructor
|
||||
private final AsyncTdDirectOptions tdOptions;
|
||||
private final TelegramClientFactory clientFactory;
|
||||
|
||||
// Variables configured by the user at startup
|
||||
private final AtomicReference<Integer> botId = new AtomicReference<>();
|
||||
private final AtomicReference<String> botAddress = new AtomicReference<>();
|
||||
private final AtomicReference<String> botAlias = new AtomicReference<>();
|
||||
|
||||
// Variables configured at startup
|
||||
private final AtomicReference<AsyncTdDirectImpl> td = new AtomicReference<>();
|
||||
private final AtomicReference<Disposable> executeConsumer = new AtomicReference<>();
|
||||
private final AtomicReference<Disposable> readBinlogConsumer = new AtomicReference<>();
|
||||
private final AtomicReference<Disposable> readyToReceiveConsumer = new AtomicReference<>();
|
||||
private final AtomicReference<Disposable> pingConsumer = new AtomicReference<>();
|
||||
private final AtomicReference<Disposable> clusterPropagationWaiter = new AtomicReference<>();
|
||||
private final AtomicReference<Flux<Void>> pipeFlux = new AtomicReference<>();
|
||||
|
||||
public AsyncTdMiddleEventBusServer() {
|
||||
this.tdOptions = new AsyncTdDirectOptions(WAIT_DURATION, 100);
|
||||
this.clientFactory = new TelegramClientFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable rxStart() {
|
||||
return Mono
|
||||
.fromCallable(() -> {
|
||||
logger.trace("Stating verticle");
|
||||
|
||||
System.setProperty("it.tdlight.enableShutdownHooks", "false");
|
||||
|
||||
var botId = config().getInteger("botId");
|
||||
if (botId == null || botId <= 0) {
|
||||
throw new IllegalArgumentException("botId is not set!");
|
||||
}
|
||||
this.botId.set(botId);
|
||||
var botAddress = "bots.bot." + botId;
|
||||
this.botAddress.set(botAddress);
|
||||
var botAlias = config().getString("botAlias");
|
||||
if (botAlias == null || botAlias.isEmpty()) {
|
||||
throw new IllegalArgumentException("botAlias is not set!");
|
||||
}
|
||||
this.botAlias.set(botAlias);
|
||||
var local = config().getBoolean("local");
|
||||
if (local == null) {
|
||||
throw new IllegalArgumentException("local is not set!");
|
||||
}
|
||||
var implementationDetails = config().getJsonObject("implementationDetails");
|
||||
if (implementationDetails == null) {
|
||||
throw new IllegalArgumentException("implementationDetails is not set!");
|
||||
}
|
||||
|
||||
var td = new AsyncTdDirectImpl(clientFactory, implementationDetails, botAlias);
|
||||
this.td.set(td);
|
||||
return new OnSuccessfulStartRequestInfo(td, botAddress, botAlias, botId, local);
|
||||
})
|
||||
.flatMap(r -> onSuccessfulStartRequest(r.td, r.botAddress, r.botAlias, r.botId, r.local))
|
||||
.doOnSuccess(s -> logger.trace("Started verticle"))
|
||||
.as(RxJava2Adapter::monoToCompletable);
|
||||
}
|
||||
|
||||
private static class OnSuccessfulStartRequestInfo {
|
||||
|
||||
public final AsyncTdDirectImpl td;
|
||||
public final String botAddress;
|
||||
public final String botAlias;
|
||||
public final int botId;
|
||||
public final boolean local;
|
||||
|
||||
public OnSuccessfulStartRequestInfo(AsyncTdDirectImpl td, String botAddress, String botAlias, int botId,
|
||||
boolean local) {
|
||||
this.td = td;
|
||||
this.botAddress = botAddress;
|
||||
this.botAlias = botAlias;
|
||||
this.botId = botId;
|
||||
this.local = local;
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<Void> onSuccessfulStartRequest(AsyncTdDirectImpl td, String botAddress, String botAlias, int botId,
|
||||
boolean local) {
|
||||
return td
|
||||
.initialize()
|
||||
.then(this.pipe(td, botAddress, local))
|
||||
.then(this.listen(td, botAddress, botId, local))
|
||||
.doOnSuccess(s -> logger.info("Deploy and start of bot \"" + botAlias + "\": ✅ Succeeded"))
|
||||
.doOnError(ex -> logger.error("Deploy and start of bot \"" + botAlias + "\": ❌ Failed", ex));
|
||||
}
|
||||
|
||||
private Mono<Void> listen(AsyncTdDirectImpl td, String botAddress, int botId, boolean local) {
|
||||
return Mono.create(registrationSink -> {
|
||||
logger.trace("Preparing listeners");
|
||||
|
||||
MessageConsumer<ExecuteObject<?>> executeConsumer = vertx.eventBus().consumer(botAddress + ".execute");
|
||||
this.executeConsumer.set(executeConsumer
|
||||
.toFlowable()
|
||||
.to(RxJava2Adapter::flowableToFlux)
|
||||
.flatMap(msg -> {
|
||||
var body = msg.body();
|
||||
var request = overrideRequest(body.getRequest(), botId);
|
||||
var timeout = body.getTimeout();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Received execute request {}", request);
|
||||
}
|
||||
return td
|
||||
.execute(request, timeout, body.isExecuteDirectly())
|
||||
.single()
|
||||
.doOnSuccess(s -> logger.trace("Executed successfully. Request was {}", request))
|
||||
.onErrorResume(ex -> Mono.fromRunnable(() -> msg.fail(500, ex.getLocalizedMessage())))
|
||||
.map(response -> {
|
||||
var replyOpts = new DeliveryOptions().setLocalOnly(local);
|
||||
var replyValue = new TdResultMessage(response.result(), response.cause());
|
||||
try {
|
||||
logger.trace("Replying with success response. Request was {}", request);
|
||||
msg.reply(replyValue, replyOpts);
|
||||
return response;
|
||||
} catch (Exception ex) {
|
||||
logger.debug("Replying with error response: {}. Request was {}", ex.getLocalizedMessage(), request);
|
||||
msg.fail(500, ex.getLocalizedMessage());
|
||||
throw ex;
|
||||
}
|
||||
});
|
||||
})
|
||||
.then()
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.subscribe(v -> {},
|
||||
ex -> logger.error("Fatal error when processing an execute request."
|
||||
+ " Can't process further requests since the subscription has been broken", ex),
|
||||
() -> logger.trace("Finished handling execute requests")
|
||||
));
|
||||
|
||||
MessageConsumer<byte[]> readBinlogConsumer = vertx.eventBus().consumer(botAddress + ".read-binlog");
|
||||
this.readBinlogConsumer.set(BinlogUtils
|
||||
.readBinlogConsumer(vertx, readBinlogConsumer, botId, local)
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.subscribe(v -> {}, ex -> logger.error("Error when processing a read-binlog request", ex)));
|
||||
|
||||
MessageConsumer<byte[]> readyToReceiveConsumer = vertx.eventBus().consumer(botAddress + ".ready-to-receive");
|
||||
|
||||
// Pipe the data
|
||||
this.readyToReceiveConsumer.set(readyToReceiveConsumer
|
||||
.toFlowable()
|
||||
.to(RxJava2Adapter::flowableToFlux)
|
||||
.take(1, true)
|
||||
.single()
|
||||
.doOnNext(s -> logger.trace("Received ready-to-receive request from client"))
|
||||
.map(msg -> Tuples.of(msg, Objects.requireNonNull(pipeFlux.get(), "PipeFlux is empty")))
|
||||
.doOnError(ex -> logger.error("Error when processing a ready-to-receive request", ex))
|
||||
.doOnNext(s -> logger.trace("Replying to ready-to-receive request"))
|
||||
.flatMapMany(tuple -> {
|
||||
var opts = new DeliveryOptions().setLocalOnly(local).setSendTimeout(Duration.ofSeconds(10).toMillis());
|
||||
|
||||
tuple.getT1().reply(EMPTY, opts);
|
||||
logger.trace("Replied to ready-to-receive");
|
||||
|
||||
logger.trace("Start piping data");
|
||||
|
||||
// Start piping the data
|
||||
return tuple.getT2().doOnSubscribe(s -> logger.trace("Subscribed to updates pipe"));
|
||||
})
|
||||
.then()
|
||||
.doOnSuccess(s -> logger.trace("Finished handling ready-to-receive requests (updates pipe ended)"))
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
// Don't handle errors here. Handle them in pipeFlux
|
||||
.subscribe(v -> {}));
|
||||
|
||||
MessageConsumer<byte[]> pingConsumer = vertx.eventBus().consumer(botAddress + ".ping");
|
||||
|
||||
this.pingConsumer.set(pingConsumer
|
||||
.toFlowable()
|
||||
.to(RxJava2Adapter::flowableToFlux)
|
||||
.doOnNext(msg -> {
|
||||
var opts = new DeliveryOptions().setLocalOnly(local).setSendTimeout(Duration.ofSeconds(10).toMillis());
|
||||
msg.reply(EMPTY, opts);
|
||||
})
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.subscribe(unused -> logger.trace("Finished handling ping requests"),
|
||||
ex -> logger.error("Error when processing a ping request", ex)
|
||||
));
|
||||
|
||||
var executorPropagated = executeConsumer.rxCompletionHandler().to(RxJava2Adapter::completableToMono);
|
||||
var readyToReceivePropagated = executeConsumer.rxCompletionHandler().to(RxJava2Adapter::completableToMono);
|
||||
var readBinLogPropagated = executeConsumer.rxCompletionHandler().to(RxJava2Adapter::completableToMono);
|
||||
var pingPropagated = executeConsumer.rxCompletionHandler().to(RxJava2Adapter::completableToMono);
|
||||
|
||||
var allPropagated = Mono.when(executorPropagated, readyToReceivePropagated, readBinLogPropagated, pingPropagated);
|
||||
this.clusterPropagationWaiter.set(allPropagated
|
||||
.doOnSuccess(s -> logger.trace("Finished preparing listeners"))
|
||||
.subscribeOn(Schedulers.boundedElastic())
|
||||
.subscribe(v -> {}, registrationSink::error, registrationSink::success));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Override some requests
|
||||
*/
|
||||
private <T extends TdApi.Object> Function<T> overrideRequest(Function<T> request, int botId) {
|
||||
if (request.getConstructor() == SetTdlibParameters.CONSTRUCTOR) {
|
||||
// Fix session directory locations
|
||||
var setTdlibParamsObj = (SetTdlibParameters) request;
|
||||
setTdlibParamsObj.parameters.databaseDirectory = TDLibRemoteClient.getSessionDirectory(botId).toString();
|
||||
setTdlibParamsObj.parameters.filesDirectory = TDLibRemoteClient.getMediaDirectory(botId).toString();
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable rxStop() {
|
||||
return Mono
|
||||
.fromRunnable(() -> {
|
||||
logger.info("Undeploy of bot \"" + botAlias.get() + "\": stopping");
|
||||
var executeConsumer = this.executeConsumer.get();
|
||||
if (executeConsumer != null) {
|
||||
executeConsumer.dispose();
|
||||
logger.trace("Unregistered execute consumer");
|
||||
}
|
||||
var pingConsumer = this.pingConsumer.get();
|
||||
if (pingConsumer != null) {
|
||||
pingConsumer.dispose();
|
||||
}
|
||||
var readBinlogConsumer = this.readBinlogConsumer.getAndSet(null);
|
||||
Schedulers.boundedElastic().schedule(() -> {
|
||||
if (readBinlogConsumer != null) {
|
||||
readBinlogConsumer.dispose();
|
||||
}
|
||||
}, 10, TimeUnit.MINUTES);
|
||||
var readyToReceiveConsumer = this.readyToReceiveConsumer.get();
|
||||
if (readyToReceiveConsumer != null) {
|
||||
readyToReceiveConsumer.dispose();
|
||||
}
|
||||
var clusterPropagationWaiter = this.clusterPropagationWaiter.get();
|
||||
if (clusterPropagationWaiter != null) {
|
||||
clusterPropagationWaiter.dispose();
|
||||
}
|
||||
})
|
||||
.doOnError(ex -> logger.error("Undeploy of bot \"" + botAlias.get() + "\": stop failed", ex))
|
||||
.doOnTerminate(() -> logger.info("Undeploy of bot \"" + botAlias.get() + "\": stopped"))
|
||||
.as(RxJava2Adapter::monoToCompletable);
|
||||
}
|
||||
|
||||
private Mono<Void> pipe(AsyncTdDirectImpl td, String botAddress, boolean local) {
|
||||
logger.trace("Preparing to pipe requests");
|
||||
Flux<TdResultList> updatesFlux = td
|
||||
.receive(tdOptions)
|
||||
.takeUntil(item -> {
|
||||
if (item instanceof Update) {
|
||||
var tdUpdate = (Update) item;
|
||||
if (tdUpdate.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
|
||||
var updateAuthorizationState = (UpdateAuthorizationState) tdUpdate;
|
||||
return updateAuthorizationState.authorizationState.getConstructor()
|
||||
== AuthorizationStateClosed.CONSTRUCTOR;
|
||||
}
|
||||
} else
|
||||
return item instanceof Error;
|
||||
return false;
|
||||
})
|
||||
.flatMap(update -> Mono.fromCallable(() -> {
|
||||
if (update.getConstructor() == TdApi.Error.CONSTRUCTOR) {
|
||||
var error = (Error) update;
|
||||
throw new TdError(error.code, error.message);
|
||||
} else {
|
||||
return update;
|
||||
}
|
||||
}))
|
||||
.limitRate(Math.max(1, tdOptions.getEventsSize()))
|
||||
//.transform(normal -> new BufferTimeOutPublisher<>(normal,Math.max(1, tdOptions.getEventsSize()),
|
||||
// local ? Duration.ofMillis(1) : Duration.ofMillis(100), false))
|
||||
//.bufferTimeout(Math.max(1, tdOptions.getEventsSize()), local ? Duration.ofMillis(1) : Duration.ofMillis(100))
|
||||
.map(List::of)
|
||||
.limitRate(Math.max(1, tdOptions.getEventsSize()))
|
||||
.map(TdResultList::new);
|
||||
|
||||
var fluxCodec = new TdResultListMessageCodec();
|
||||
var opts = new DeliveryOptions()
|
||||
.setLocalOnly(local)
|
||||
.setSendTimeout(Duration.ofSeconds(30).toMillis())
|
||||
.setCodecName(fluxCodec.name());
|
||||
|
||||
MessageProducer<TdResultList> updatesSender = vertx
|
||||
.eventBus()
|
||||
.sender(botAddress + ".updates", opts);
|
||||
|
||||
var pipeFlux = updatesFlux
|
||||
.concatMap(updatesList -> updatesSender
|
||||
.rxWrite(updatesList)
|
||||
.to(RxJava2Adapter::completableToMono)
|
||||
.thenReturn(updatesList)
|
||||
)
|
||||
.concatMap(updatesList -> Flux
|
||||
.fromIterable(updatesList.value())
|
||||
.concatMap(item -> {
|
||||
if (item instanceof Update) {
|
||||
var tdUpdate = (Update) item;
|
||||
if (tdUpdate.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
|
||||
var tdUpdateAuthorizationState = (UpdateAuthorizationState) tdUpdate;
|
||||
if (tdUpdateAuthorizationState.authorizationState.getConstructor()
|
||||
== AuthorizationStateClosed.CONSTRUCTOR) {
|
||||
logger.info("Undeploying after receiving AuthorizationStateClosed");
|
||||
return rxStop().to(RxJava2Adapter::completableToMono).thenReturn(item);
|
||||
}
|
||||
}
|
||||
} else if (item instanceof Error) {
|
||||
// An error in updates means that a fatal error occurred
|
||||
logger.info("Undeploying after receiving a fatal error");
|
||||
return rxStop().to(RxJava2Adapter::completableToMono).thenReturn(item);
|
||||
}
|
||||
return Mono.just(item);
|
||||
})
|
||||
.then()
|
||||
)
|
||||
.doOnTerminate(() -> updatesSender.close(h -> {
|
||||
if (h.failed()) {
|
||||
logger.error("Failed to close \"updates\" message sender");
|
||||
}
|
||||
}))
|
||||
.onErrorResume(ex -> {
|
||||
boolean printDefaultException = true;
|
||||
if (ex instanceof ReplyException) {
|
||||
ReplyException replyException = (ReplyException) ex;
|
||||
if (replyException.failureCode() == -1 && replyException.failureType() == ReplyFailure.NO_HANDLERS) {
|
||||
logger.warn("Undeploying, the flux has been terminated because no more"
|
||||
+ " handlers are available on the event bus. {}", replyException.getMessage());
|
||||
printDefaultException = false;
|
||||
}
|
||||
} else if (ex instanceof ConnectException || ex instanceof java.nio.channels.ClosedChannelException) {
|
||||
logger.warn("Undeploying, the flux has been terminated because the consumer"
|
||||
+ " disconnected from the event bus. {}", ex.getMessage());
|
||||
printDefaultException = false;
|
||||
}
|
||||
if (printDefaultException) {
|
||||
logger.warn("Undeploying after a fatal error in a served flux", ex);
|
||||
}
|
||||
return td
|
||||
.execute(new TdApi.Close(), Duration.ofDays(1), false)
|
||||
.doOnError(ex2 -> logger.error("Unexpected error", ex2))
|
||||
.doOnSuccess(s -> logger.debug("Emergency Close() signal has been sent successfully"))
|
||||
.then(rxStop().to(RxJava2Adapter::completableToMono));
|
||||
});
|
||||
|
||||
return Mono.fromRunnable(() -> {
|
||||
this.pipeFlux.set(pipeFlux);
|
||||
logger.trace("Prepared piping requests successfully");
|
||||
});
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle.server;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class RequestId {
|
||||
|
||||
public static Mono<Long> create() {
|
||||
AtomicLong _requestId = new AtomicLong(1);
|
||||
|
||||
return Mono.fromCallable(() -> _requestId.updateAndGet(n -> {
|
||||
if (n > Long.MAX_VALUE - 100) {
|
||||
return 1;
|
||||
} else {
|
||||
return n + 1;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package it.tdlight.tdlibsession.td.middle.server;
|
||||
|
||||
import io.vertx.core.Promise;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.jetbrains.annotations.Async.Execute;
|
||||
import org.jetbrains.annotations.Async.Schedule;
|
||||
|
||||
public class RequestIdToReplyAddress {
|
||||
private final ConcurrentHashMap<Long, Promise<Object>> reqIdToReplyAddress = new ConcurrentHashMap<>();;
|
||||
|
||||
public RequestIdToReplyAddress() {
|
||||
|
||||
}
|
||||
|
||||
public void schedule(@Schedule Long requestId, Promise<Object> replyPromise) {
|
||||
reqIdToReplyAddress.put(requestId, replyPromise);
|
||||
}
|
||||
|
||||
public void failed(@Execute Long requestId, Promise<Object> replyPromise) {
|
||||
reqIdToReplyAddress.remove(requestId, replyPromise);
|
||||
}
|
||||
|
||||
public void complete(@Execute Long id, Object item) {
|
||||
var replyPromise = reqIdToReplyAddress.remove(id);
|
||||
Objects.requireNonNull(replyPromise, () -> "Reply promise must be not empty");
|
||||
replyPromise.complete(item);
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
package it.tdlight.utils;
|
||||
|
||||
import io.vertx.core.file.OpenOptions;
|
||||
import io.vertx.reactivex.core.buffer.Buffer;
|
||||
import io.vertx.reactivex.core.file.AsyncFile;
|
||||
import io.vertx.reactivex.core.file.FileProps;
|
||||
import io.vertx.reactivex.core.file.FileSystem;
|
||||
import org.warp.commonutils.log.Logger;
|
||||
import org.warp.commonutils.log.LoggerFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class BinlogAsyncFile {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BinlogAsyncFile.class);
|
||||
|
||||
private final FileSystem filesystem;
|
||||
private final String path;
|
||||
private final OpenOptions openOptions;
|
||||
|
||||
public BinlogAsyncFile(FileSystem fileSystem, String path) {
|
||||
this.filesystem = fileSystem;
|
||||
this.path = path;
|
||||
this.openOptions = new OpenOptions().setWrite(true).setRead(true).setCreate(false).setDsync(true);
|
||||
}
|
||||
|
||||
private Mono<AsyncFile> openRW() {
|
||||
return filesystem.rxOpen(path, openOptions).as(MonoUtils::toMono);
|
||||
}
|
||||
|
||||
public Mono<Buffer> readFully() {
|
||||
return openRW()
|
||||
.flatMap(asyncFile -> filesystem
|
||||
.rxProps(path)
|
||||
.map(props -> (int) props.size())
|
||||
.as(MonoUtils::toMono)
|
||||
.flatMap(size -> {
|
||||
var buf = Buffer.buffer(size);
|
||||
logger.debug("Reading binlog from disk. Size: " + BinlogUtils.humanReadableByteCountBin(size));
|
||||
return asyncFile.rxRead(buf, 0, 0, size).as(MonoUtils::toMono).thenReturn(buf);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public Mono<byte[]> readFullyBytes() {
|
||||
return this.readFully().map(Buffer::getBytes);
|
||||
}
|
||||
|
||||
public Mono<Void> overwrite(Buffer newData) {
|
||||
return openRW().flatMap(asyncFile -> this
|
||||
.getSize()
|
||||
.doOnNext(size -> logger.debug("Preparing to overwrite binlog. Initial size: " + BinlogUtils.humanReadableByteCountBin(size)))
|
||||
.then(asyncFile.rxWrite(newData, 0)
|
||||
.andThen(asyncFile.rxFlush())
|
||||
.andThen(filesystem.rxTruncate(path, newData.length()))
|
||||
.as(MonoUtils::toMono)
|
||||
)
|
||||
.then(getSize())
|
||||
.doOnNext(size -> logger.debug("Overwritten binlog. Final size: " + BinlogUtils.humanReadableByteCountBin(size)))
|
||||
.then()
|
||||
);
|
||||
}
|
||||
|
||||
public Mono<Void> overwrite(byte[] newData) {
|
||||
return this.overwrite(Buffer.buffer(newData));
|
||||
}
|
||||
|
||||
public FileSystem getFilesystem() {
|
||||
return filesystem;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public Mono<Long> getLastModifiedTime() {
|
||||
return filesystem
|
||||
.rxProps(path)
|
||||
.map(fileProps -> fileProps.size() == 0 ? 0 : fileProps.lastModifiedTime())
|
||||
.as(MonoUtils::toMono);
|
||||
}
|
||||
|
||||
public Mono<Long> getSize() {
|
||||
return filesystem
|
||||
.rxProps(path)
|
||||
.map(FileProps::size)
|
||||
.as(MonoUtils::toMono);
|
||||
}
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
package it.tdlight.utils;
|
||||
|
||||
import io.vertx.core.eventbus.DeliveryOptions;
|
||||
import io.vertx.core.file.OpenOptions;
|
||||
import io.vertx.reactivex.core.Vertx;
|
||||
import io.vertx.reactivex.core.buffer.Buffer;
|
||||
import io.vertx.reactivex.core.eventbus.Message;
|
||||
import io.vertx.reactivex.core.eventbus.MessageConsumer;
|
||||
import io.vertx.reactivex.core.file.FileSystem;
|
||||
import it.tdlight.tdlibsession.remoteclient.TDLibRemoteClient;
|
||||
import it.tdlight.tdlibsession.td.middle.EndSessionMessage;
|
||||
import java.nio.file.Path;
|
||||
import java.text.CharacterIterator;
|
||||
import java.text.StringCharacterIterator;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import org.warp.commonutils.log.Logger;
|
||||
import org.warp.commonutils.log.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
public class BinlogUtils {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BinlogUtils.class);
|
||||
|
||||
public static Mono<BinlogAsyncFile> retrieveBinlog(FileSystem vertxFilesystem, Path binlogPath) {
|
||||
var path = binlogPath.toString();
|
||||
return vertxFilesystem
|
||||
// Create file if not exist to avoid errors
|
||||
.rxExists(path).filter(exists -> exists).as(MonoUtils::toMono)
|
||||
.switchIfEmpty(Mono.defer(() -> vertxFilesystem.rxMkdirs(binlogPath.getParent().toString()).as(MonoUtils::toMono))
|
||||
.then(vertxFilesystem.rxCreateFile(path).as(MonoUtils::toMono))
|
||||
.thenReturn(true)
|
||||
)
|
||||
// Open file
|
||||
.map(x -> new BinlogAsyncFile(vertxFilesystem, path))
|
||||
.single();
|
||||
}
|
||||
|
||||
public static Mono<Void> saveBinlog(BinlogAsyncFile binlog, Buffer data) {
|
||||
return binlog.overwrite(data);
|
||||
}
|
||||
|
||||
public static Mono<Void> chooseBinlog(FileSystem vertxFilesystem,
|
||||
Path binlogPath,
|
||||
Buffer remoteBinlog,
|
||||
long remoteBinlogDate) {
|
||||
var path = binlogPath.toString();
|
||||
return retrieveBinlog(vertxFilesystem, binlogPath)
|
||||
.flatMap(binlog -> Mono
|
||||
.just(binlog)
|
||||
.zipWith(binlog.getLastModifiedTime())
|
||||
)
|
||||
.doOnSuccess(s -> logger.info("Local binlog: " + binlogPath + ". Local date: " + Instant.ofEpochMilli(s == null ? 0 : s.getT2()).atZone(ZoneOffset.UTC).toString() + " Remote date: " + Instant.ofEpochMilli(remoteBinlogDate).atZone(ZoneOffset.UTC).toString()))
|
||||
// Files older than the remote file will be overwritten
|
||||
.filter(tuple -> tuple.getT2() >= remoteBinlogDate)
|
||||
.doOnNext(v -> logger.info("Using local binlog: " + binlogPath))
|
||||
.map(Tuple2::getT1)
|
||||
.switchIfEmpty(Mono.defer(() -> Mono.fromRunnable(() -> logger.info("Using remote binlog. Overwriting " + binlogPath)))
|
||||
.then(vertxFilesystem.rxWriteFile(path, remoteBinlog.copy()).as(MonoUtils::toMono))
|
||||
.then(retrieveBinlog(vertxFilesystem, binlogPath))
|
||||
)
|
||||
.single()
|
||||
.then();
|
||||
}
|
||||
|
||||
public static Mono<Void> cleanSessionPath(FileSystem vertxFilesystem,
|
||||
Path binlogPath,
|
||||
Path sessionPath,
|
||||
Path mediaPath) {
|
||||
return vertxFilesystem
|
||||
.rxReadFile(binlogPath.toString()).as(MonoUtils::toMono)
|
||||
.flatMap(buffer -> vertxFilesystem
|
||||
.rxReadDir(sessionPath.toString(), "^(?!td.binlog$).*").as(MonoUtils::toMono)
|
||||
.flatMapIterable(list -> list)
|
||||
.doOnNext(file -> logger.debug("Deleting session file {}", file))
|
||||
.flatMap(file -> vertxFilesystem.rxDeleteRecursive(file, true).as(MonoUtils::toMono))
|
||||
.then(vertxFilesystem.rxReadDir(mediaPath.toString(), "^(?!td.binlog$).*").as(MonoUtils::toMono))
|
||||
.flatMapIterable(list -> list)
|
||||
.doOnNext(file -> logger.debug("Deleting media file {}", file))
|
||||
.flatMap(file -> vertxFilesystem.rxDeleteRecursive(file, true).as(MonoUtils::toMono))
|
||||
.onErrorResume(ex -> Mono.empty())
|
||||
.then()
|
||||
);
|
||||
}
|
||||
|
||||
public static String humanReadableByteCountBin(long bytes) {
|
||||
long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes);
|
||||
if (absB < 1024) {
|
||||
return bytes + " B";
|
||||
}
|
||||
long value = absB;
|
||||
CharacterIterator ci = new StringCharacterIterator("KMGTPE");
|
||||
for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) {
|
||||
value >>= 10;
|
||||
ci.next();
|
||||
}
|
||||
value *= Long.signum(bytes);
|
||||
return String.format("%.1f %ciB", value / 1024.0, ci.current());
|
||||
}
|
||||
|
||||
public static Mono<Void> readBinlogConsumer(Vertx vertx,
|
||||
MessageConsumer<byte[]> readBinlogConsumer,
|
||||
int botId,
|
||||
boolean local) {
|
||||
return Flux
|
||||
.<Message<byte[]>>create(sink -> {
|
||||
readBinlogConsumer.handler(sink::next);
|
||||
readBinlogConsumer.endHandler(h -> sink.complete());
|
||||
})
|
||||
.flatMapSequential(req -> BinlogUtils
|
||||
.retrieveBinlog(vertx.fileSystem(), TDLibRemoteClient.getSessionBinlogDirectory(botId))
|
||||
.flatMap(BinlogAsyncFile::readFully)
|
||||
.map(Buffer::copy)
|
||||
.single()
|
||||
.map(binlog -> Tuples.of(req, binlog))
|
||||
)
|
||||
.doOnNext(tuple -> {
|
||||
var opts = new DeliveryOptions().setLocalOnly(local).setSendTimeout(Duration.ofSeconds(10).toMillis());
|
||||
tuple.getT1().reply(new EndSessionMessage(botId, tuple.getT2()), opts);
|
||||
})
|
||||
.then();
|
||||
}
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
package it.tdlight.utils;
|
||||
|
||||
/** Based on:
|
||||
* https://gist.github.com/glandais-sparklane/e38834aa9df0c56f23e2d8d2e6899c78
|
||||
*/
|
||||
|
||||
import java.time.Duration;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import reactor.core.CoreSubscriber;
|
||||
|
||||
@SuppressWarnings("ReactiveStreamsPublisherImplementation")
|
||||
public class BufferTimeOutPublisher<T> implements Publisher<List<T>> {
|
||||
|
||||
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
private final Publisher<T> source;
|
||||
private final int size;
|
||||
private final long duration;
|
||||
private final boolean discardOnError;
|
||||
|
||||
public BufferTimeOutPublisher(Publisher<T> source, int size, Duration duration, boolean discardOnError) {
|
||||
this.source = source;
|
||||
this.size = size;
|
||||
this.duration = duration.toMillis();
|
||||
this.discardOnError = discardOnError;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(Subscriber<? super List<T>> subscriber) {
|
||||
subscriber.onSubscribe(new BufferTimeOutSubscription<T>(source, subscriber, size, duration, discardOnError));
|
||||
}
|
||||
|
||||
protected static class BufferTimeOutSubscription<T> implements Subscription, CoreSubscriber<T> {
|
||||
|
||||
private final Subscriber<? super List<T>> subscriber;
|
||||
private final int size;
|
||||
private final long duration;
|
||||
private final boolean discardOnError;
|
||||
private Subscription subscription;
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
private List<T> buffer;
|
||||
private ScheduledFuture<?> scheduledFuture;
|
||||
|
||||
private long downstreamRequests = 0;
|
||||
private long downstreamTransmit = 0;
|
||||
|
||||
private long upstreamRequests = 0;
|
||||
private long upstreamTransmit = 0;
|
||||
private boolean upstreamCompleted = false;
|
||||
|
||||
public BufferTimeOutSubscription(Publisher<T> source,
|
||||
Subscriber<? super List<T>> subscriber,
|
||||
int size,
|
||||
long duration,
|
||||
boolean discardOnError) {
|
||||
this.subscriber = subscriber;
|
||||
this.size = size;
|
||||
this.duration = duration;
|
||||
this.discardOnError = discardOnError;
|
||||
this.buffer = new ArrayList<>(size);
|
||||
source.subscribe(this);
|
||||
}
|
||||
|
||||
// downstream
|
||||
@Override
|
||||
public void request(long n) {
|
||||
lock.lock();
|
||||
downstreamRequests = downstreamRequests + n;
|
||||
|
||||
checkSend();
|
||||
|
||||
long downstreamMax = (downstreamRequests - downstreamTransmit) * size;
|
||||
long upstreamRequested = upstreamRequests - upstreamTransmit;
|
||||
long toRequest = downstreamMax - upstreamRequested;
|
||||
|
||||
if (toRequest > 0) {
|
||||
subscription.request(toRequest);
|
||||
upstreamRequests = upstreamRequests + toRequest;
|
||||
}
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
subscription.cancel();
|
||||
}
|
||||
|
||||
// upstream
|
||||
@Override
|
||||
public void onSubscribe(@NotNull Subscription s) {
|
||||
this.subscription = s;
|
||||
scheduledFuture = EXECUTOR.scheduleAtFixedRate(this::timeout, 0, this.duration, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void timeout() {
|
||||
checkSend();
|
||||
}
|
||||
|
||||
private void checkSend() {
|
||||
lock.lock();
|
||||
if (!this.buffer.isEmpty() && downstreamRequests > downstreamTransmit) {
|
||||
List<T> output = prepareOutput();
|
||||
subscriber.onNext(output);
|
||||
downstreamTransmit++;
|
||||
if (!this.buffer.isEmpty()) {
|
||||
checkSend();
|
||||
}
|
||||
}
|
||||
if (upstreamCompleted && downstreamRequests > downstreamTransmit) {
|
||||
scheduledFuture.cancel(false);
|
||||
subscriber.onComplete();
|
||||
}
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
private List<T> prepareOutput() {
|
||||
if (this.buffer.size() > size) {
|
||||
List<T> output = new ArrayList<>(this.buffer.subList(0, size));
|
||||
this.buffer = new ArrayList<>(this.buffer.subList(size, this.buffer.size()));
|
||||
return output;
|
||||
} else {
|
||||
List<T> output = this.buffer;
|
||||
this.buffer = new ArrayList<>(size);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(T t) {
|
||||
lock.lock();
|
||||
this.buffer.add(t);
|
||||
upstreamTransmit++;
|
||||
if (this.buffer.size() == size) {
|
||||
checkSend();
|
||||
}
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
if (discardOnError) {
|
||||
scheduledFuture.cancel(false);
|
||||
subscriber.onError(t);
|
||||
} else {
|
||||
lock.lock();
|
||||
try {
|
||||
checkSend();
|
||||
scheduledFuture.cancel(false);
|
||||
subscriber.onError(t);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
lock.lock();
|
||||
upstreamCompleted = true;
|
||||
checkSend();
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package it.tdlight.utils;
|
||||
|
||||
import io.netty.buffer.ByteBufInputStream;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.buffer.impl.BufferImpl;
|
||||
import java.io.IOException;
|
||||
import org.apache.commons.lang3.SerializationException;
|
||||
|
||||
public class BufferUtils {
|
||||
|
||||
private static final int CHUNK_SIZE = 8192;
|
||||
|
||||
public static void writeBuf(ByteBufOutputStream os, io.vertx.reactivex.core.buffer.Buffer dataToWrite)
|
||||
throws IOException {
|
||||
var len = dataToWrite.length();
|
||||
os.writeInt(len);
|
||||
byte[] part = new byte[CHUNK_SIZE];
|
||||
for (int i = 0; i < len; i += CHUNK_SIZE) {
|
||||
var end = Math.min(i + CHUNK_SIZE, len);
|
||||
dataToWrite.getBytes(i, end, part, 0);
|
||||
os.write(part, 0, end - i);
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeBuf(ByteBufOutputStream os, io.vertx.core.buffer.Buffer dataToWrite) throws IOException {
|
||||
var len = dataToWrite.length();
|
||||
os.writeInt(len);
|
||||
byte[] part = new byte[CHUNK_SIZE];
|
||||
for (int i = 0; i < len; i += CHUNK_SIZE) {
|
||||
var end = Math.min(i + CHUNK_SIZE, len);
|
||||
dataToWrite.getBytes(i, end, part, 0);
|
||||
os.write(part, 0, end - i);
|
||||
}
|
||||
}
|
||||
|
||||
public static io.vertx.core.buffer.Buffer readBuf(ByteBufInputStream is) throws IOException {
|
||||
int len = is.readInt();
|
||||
Buffer buf = Buffer.buffer(len);
|
||||
byte[] part = new byte[1024];
|
||||
int readPart = 0;
|
||||
for (int i = 0; i < len; i += 1024) {
|
||||
var lenx = (Math.min(i + 1024, len)) - i;
|
||||
if (lenx > 0) {
|
||||
readPart = is.readNBytes(part, 0, lenx);
|
||||
buf.appendBytes(part, 0, readPart);
|
||||
}
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
public static io.vertx.reactivex.core.buffer.Buffer rxReadBuf(ByteBufInputStream is) throws IOException {
|
||||
int len = is.readInt();
|
||||
io.vertx.reactivex.core.buffer.Buffer buf = io.vertx.reactivex.core.buffer.Buffer.buffer(len);
|
||||
byte[] part = new byte[1024];
|
||||
int readPart = 0;
|
||||
for (int i = 0; i < len; i += 1024) {
|
||||
var lenx = (Math.min(i + 1024, len)) - i;
|
||||
if (lenx > 0) {
|
||||
readPart = is.readNBytes(part, 0, lenx);
|
||||
buf.appendBytes(part, 0, readPart);
|
||||
}
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
public interface Writer {
|
||||
|
||||
void write(ByteBufOutputStream os) throws IOException;
|
||||
}
|
||||
|
||||
public interface Reader<T> {
|
||||
|
||||
T read(ByteBufInputStream is) throws IOException;
|
||||
}
|
||||
|
||||
public static void encode(Buffer buffer, Writer writer) {
|
||||
try (var os = new ByteBufOutputStream(((BufferImpl) buffer).byteBuf())) {
|
||||
writer.write(os);
|
||||
} catch (IOException ex) {
|
||||
throw new SerializationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static <T> T decode(int pos, Buffer buffer, Reader<T> reader) {
|
||||
try (var is = new ByteBufInputStream(buffer.slice(pos, buffer.length()).getByteBuf())) {
|
||||
return reader.read(is);
|
||||
} catch (IOException ex) {
|
||||
throw new SerializationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package it.tdlight.utils;
|
||||
|
||||
public interface EmptyCallable {
|
||||
void call() throws Exception;
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
package it.tdlight.utils;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.vertx.core.AsyncResult;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.reactivex.RxHelper;
|
||||
import io.vertx.reactivex.core.eventbus.Message;
|
||||
import io.vertx.reactivex.core.eventbus.MessageConsumer;
|
||||
import io.vertx.reactivex.core.streams.Pipe;
|
||||
import io.vertx.reactivex.core.streams.ReadStream;
|
||||
import io.vertx.reactivex.core.streams.WriteStream;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Chat;
|
||||
import it.tdlight.tdlibsession.td.TdError;
|
||||
import it.tdlight.tdlibsession.td.TdResult;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.warp.commonutils.concurrency.future.CompletableFutureUtils;
|
||||
import org.warp.commonutils.functional.IOConsumer;
|
||||
import org.warp.commonutils.log.Logger;
|
||||
import org.warp.commonutils.log.LoggerFactory;
|
||||
import reactor.adapter.rxjava.RxJava2Adapter;
|
||||
import reactor.core.CoreSubscriber;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.FluxSink.OverflowStrategy;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.MonoSink;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.core.publisher.Sinks.EmissionException;
|
||||
import reactor.core.publisher.Sinks.EmitResult;
|
||||
import reactor.core.publisher.Sinks.Empty;
|
||||
import reactor.core.publisher.Sinks.Many;
|
||||
import reactor.core.publisher.Sinks.One;
|
||||
import reactor.core.publisher.SynchronousSink;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.concurrent.Queues;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
public class MonoUtils {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MonoUtils.class);
|
||||
|
||||
public static <T> Mono<T> notImplemented() {
|
||||
return Mono.fromCallable(() -> {
|
||||
throw new NotImplementedException();
|
||||
});
|
||||
}
|
||||
|
||||
public static <T> Mono<T> fromBlockingMaybe(Callable<T> callable) {
|
||||
return Mono.fromCallable(callable).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
public static Mono<Void> fromBlockingEmpty(EmptyCallable callable) {
|
||||
return Mono.<Void>fromCallable(() -> {
|
||||
callable.call();
|
||||
return null;
|
||||
}).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
public static <T> Mono<T> fromBlockingSingle(Callable<T> callable) {
|
||||
return fromBlockingMaybe(callable).single();
|
||||
}
|
||||
|
||||
public static <T extends TdApi.Object> void orElseThrow(TdResult<T> value, SynchronousSink<T> sink) {
|
||||
if (value.succeeded()) {
|
||||
sink.next(value.result());
|
||||
} else {
|
||||
sink.error(new TdError(value.cause().code, value.cause().message));
|
||||
}
|
||||
}
|
||||
|
||||
public static <T extends TdApi.Object> Mono<Void> thenOrError(Mono<TdResult<T>> optionalMono) {
|
||||
return optionalMono.handle((optional, sink) -> {
|
||||
if (optional.succeeded()) {
|
||||
sink.complete();
|
||||
} else {
|
||||
sink.error(new TdError(optional.cause().code, optional.cause().message));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static <T> Mono<T> toMono(Future<T> future) {
|
||||
return Mono.fromCompletionStage(future.toCompletionStage());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@NotNull
|
||||
public static <T> Mono<T> toMono(Single<T> single) {
|
||||
return RxJava2Adapter.singleToMono(single);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@NotNull
|
||||
public static <T> Mono<T> toMono(Maybe<T> maybe) {
|
||||
return RxJava2Adapter.maybeToMono(maybe);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@NotNull
|
||||
public static <T> Mono<T> toMono(Completable completable) {
|
||||
//noinspection unchecked
|
||||
return (Mono<T>) RxJava2Adapter.completableToMono(completable);
|
||||
}
|
||||
|
||||
public static <T> Flux<T> fromMessageConsumer(Mono<Void> onRegistered, MessageConsumer<T> messageConsumer) {
|
||||
return fromReplyableMessageConsumer(onRegistered, messageConsumer).map(Message::body);
|
||||
}
|
||||
|
||||
public static <T> Flux<Message<T>> fromReplyableMessageConsumer(Mono<Void> onRegistered,
|
||||
MessageConsumer<T> messageConsumer) {
|
||||
var registration = messageConsumer
|
||||
.rxCompletionHandler().to(RxJava2Adapter::completableToMono)
|
||||
.doFirst(() -> logger.trace("Waiting for consumer registration completion..."))
|
||||
.doOnSuccess(s -> logger.trace("Consumer registered"))
|
||||
.then(onRegistered);
|
||||
var messages = messageConsumer.toFlowable().to(RxJava2Adapter::flowableToFlux);
|
||||
|
||||
return messages.mergeWith(registration.then(Mono.empty()));
|
||||
}
|
||||
|
||||
public static Scheduler newBoundedSingle(String name) {
|
||||
return newBoundedSingle(name, false);
|
||||
}
|
||||
|
||||
public static Scheduler newBoundedSingle(String name, boolean daemon) {
|
||||
return Schedulers.newBoundedElastic(1,
|
||||
Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE,
|
||||
name,
|
||||
Integer.MAX_VALUE,
|
||||
daemon
|
||||
);
|
||||
}
|
||||
|
||||
public static <R> Mono<Optional<R>> toOptional(Mono<R> mono) {
|
||||
return mono.map(Optional::of).defaultIfEmpty(Optional.empty());
|
||||
}
|
||||
|
||||
public static <T> Mono<Boolean> isSet(Mono<T> mono) {
|
||||
return mono
|
||||
.map(res -> true)
|
||||
.defaultIfEmpty(false);
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package it.tdlight.utils;
|
||||
|
||||
import io.vertx.core.Promise;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import reactor.core.publisher.SynchronousSink;
|
||||
import reactor.util.context.Context;
|
||||
|
||||
public abstract class PromiseSink<T> implements SynchronousSink<T> {
|
||||
|
||||
private final Promise<T> promise;
|
||||
|
||||
private PromiseSink(Promise<T> promise) {
|
||||
this.promise = promise;
|
||||
}
|
||||
|
||||
public static <K> PromiseSink<K> of(Context context, Promise<K> promise) {
|
||||
return new PromiseSinkImpl<>(promise, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void complete() {
|
||||
promise.complete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(@NotNull Throwable error) {
|
||||
promise.fail(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void next(@NotNull T value) {
|
||||
promise.complete(value);
|
||||
}
|
||||
|
||||
private static class PromiseSinkImpl<K> extends PromiseSink<K> {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public PromiseSinkImpl(Promise<K> promise, Context context) {
|
||||
super(promise);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Context currentContext() {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package it.tdlight.utils;
|
||||
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Error;
|
||||
import it.tdlight.tdlibsession.td.ResponseError;
|
||||
import it.tdlight.tdlibsession.td.TdError;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class TdLightUtils {
|
||||
|
||||
@SuppressWarnings("RedundantIfStatement")
|
||||
public static boolean errorEquals(Throwable ex, @Nullable Integer errorCode, @Nullable String errorText) {
|
||||
while (ex != null) {
|
||||
TdApi.Error error = null;
|
||||
if (ex instanceof TdError) {
|
||||
error = ((TdError) ex).getTdError();
|
||||
}
|
||||
if (ex instanceof ResponseError) {
|
||||
error = new Error(((ResponseError) ex).getErrorCode(), ((ResponseError) ex).getErrorMessage());
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
if (errorCode != null) {
|
||||
if (error.code != errorCode) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (errorText != null) {
|
||||
if (error.message == null || !error.message.contains(errorText)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ex = ex.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package it.tdlight.utils;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import it.tdlight.jni.TdApi;
|
||||
import it.tdlight.jni.TdApi.Object;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.warp.commonutils.moshi.MoshiPolymorphic;
|
||||
|
||||
public class TdMoshiPolymorphic extends MoshiPolymorphic<Object> {
|
||||
|
||||
|
||||
private final Set<Class<TdApi.Object>> abstractClasses = new HashSet<>();
|
||||
private final Set<Class<TdApi.Object>> concreteClasses = new HashSet<>();
|
||||
|
||||
public TdMoshiPolymorphic() {
|
||||
super();
|
||||
var declaredClasses = TdApi.class.getDeclaredClasses();
|
||||
for (Class<?> declaredClass : declaredClasses) {
|
||||
var modifiers = declaredClass.getModifiers();
|
||||
if (Modifier.isPublic(modifiers) && Modifier
|
||||
.isStatic(modifiers)) {
|
||||
if (Modifier.isAbstract(modifiers)) {
|
||||
//noinspection unchecked
|
||||
this.abstractClasses.add((Class<TdApi.Object>) declaredClass);
|
||||
} else {
|
||||
//noinspection unchecked
|
||||
this.concreteClasses.add((Class<TdApi.Object>) declaredClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<TdApi.Object>> getAbstractClasses() {
|
||||
return abstractClasses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<TdApi.Object>> getConcreteClasses() {
|
||||
return concreteClasses;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldIgnoreField(String fieldName) {
|
||||
return fieldName.equals("CONSTRUCTOR");
|
||||
}
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
package it.tdlight.utils;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import org.warp.commonutils.stream.SafeMeasurableInputStream;
|
||||
import org.warp.commonutils.stream.SafeRepositionableStream;
|
||||
|
||||
public class VertxBufferInputStream extends SafeMeasurableInputStream implements SafeRepositionableStream {
|
||||
|
||||
private final Buffer buffer;
|
||||
|
||||
/** The first valid entry. */
|
||||
public int offset;
|
||||
|
||||
/** The number of valid bytes in {@link #buffer} starting from {@link #offset}. */
|
||||
public int length;
|
||||
|
||||
/** The current position as a distance from {@link #offset}. */
|
||||
private int position;
|
||||
|
||||
/** The current mark as a position, or -1 if no mark exists. */
|
||||
private int mark;
|
||||
|
||||
/** Creates a new buffer input stream using a given buffer fragment.
|
||||
*
|
||||
* @param buffer the backing buffer.
|
||||
* @param offset the first valid entry of the buffer.
|
||||
* @param length the number of valid bytes.
|
||||
*/
|
||||
public VertxBufferInputStream(final Buffer buffer, final int offset, final int length) {
|
||||
this.buffer = buffer;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
/** Creates a new buffer input stream using a given buffer fragment.
|
||||
*
|
||||
* @param buffer the backing buffer.
|
||||
* @param offset the first valid entry of the buffer.
|
||||
*/
|
||||
public VertxBufferInputStream(final Buffer buffer, final int offset) {
|
||||
this.buffer = buffer;
|
||||
this.offset = offset;
|
||||
this.length = buffer.length();
|
||||
}
|
||||
|
||||
/** Creates a new buffer input stream using a given buffer.
|
||||
*
|
||||
* @param buffer the backing buffer.
|
||||
*/
|
||||
public VertxBufferInputStream(final Buffer buffer) {
|
||||
this(buffer, 0, buffer.length());
|
||||
}
|
||||
|
||||
/** Creates a new buffer input stream using a given buffer.
|
||||
*
|
||||
* @param in the backing buffer.
|
||||
*/
|
||||
public VertxBufferInputStream(final VertxBufferInputStream in) {
|
||||
this(in.buffer, in.offset + in.position, in.buffer.length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
position = mark;
|
||||
}
|
||||
|
||||
/** Closing a fast byte buffer input stream has no effect. */
|
||||
@Override
|
||||
public void close() {}
|
||||
|
||||
@Override
|
||||
public void mark(final int dummy) {
|
||||
mark = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() {
|
||||
return length - position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) {
|
||||
if (n <= length - position) {
|
||||
position += (int)n;
|
||||
return n;
|
||||
}
|
||||
n = length - position;
|
||||
position = length;
|
||||
return n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
if (length == position) return -1;
|
||||
return buffer.getByte(offset + position++) & 0xFF;
|
||||
}
|
||||
|
||||
/** Reads bytes from this byte-buffer input stream as
|
||||
* specified in {@link java.io.InputStream#read(byte[], int, int)}.
|
||||
* Note that the implementation given in {@link java.io.ByteArrayInputStream#read(byte[], int, int)}
|
||||
* will return -1 on a zero-length read at EOF, contrarily to the specification. We won't.
|
||||
*/
|
||||
|
||||
@Override
|
||||
public int read(final byte b[], final int offset, final int length) {
|
||||
if (this.length == this.position) return length == 0 ? 0 : -1;
|
||||
final int n = Math.min(length, this.length - this.position);
|
||||
buffer.getBytes(this.offset + this.position, this.offset + this.position + n, b, offset);
|
||||
this.position += n;
|
||||
return n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(final long newPosition) {
|
||||
position = (int)Math.min(newPosition, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long length() {
|
||||
return length;
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package it.tdlight.utils;
|
||||
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import it.unimi.dsi.fastutil.bytes.ByteArrays;
|
||||
import org.warp.commonutils.stream.SafeMeasurableOutputStream;
|
||||
import org.warp.commonutils.stream.SafeRepositionableStream;
|
||||
|
||||
public class VertxBufferOutputStream extends SafeMeasurableOutputStream implements SafeRepositionableStream {
|
||||
|
||||
/** The buffer backing the output stream. */
|
||||
public Buffer buffer;
|
||||
|
||||
/** Creates a new buffer output stream with an initial capacity of 0 bytes. */
|
||||
public VertxBufferOutputStream() {
|
||||
this(0);
|
||||
}
|
||||
|
||||
/** Creates a new buffer output stream with a given initial capacity.
|
||||
*
|
||||
* @param initialCapacity the initial length of the backing buffer.
|
||||
*/
|
||||
public VertxBufferOutputStream(final int initialCapacity) {
|
||||
buffer = Buffer.buffer(initialCapacity);
|
||||
}
|
||||
|
||||
/** Creates a new buffer output stream wrapping a given byte buffer.
|
||||
*
|
||||
* @param a the byte buffer to wrap.
|
||||
*/
|
||||
public VertxBufferOutputStream(final Buffer a) {
|
||||
buffer = a;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final int b) {
|
||||
buffer.appendByte((byte) b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final byte[] b, final int off, final int len) {
|
||||
ByteArrays.ensureOffsetLength(b, off, len);
|
||||
buffer.appendBytes(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void position(long newPosition) {
|
||||
throw new UnsupportedOperationException("Can't change position of a vertx buffer output stream");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() {
|
||||
return this.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long length() {
|
||||
return buffer.length();
|
||||
}
|
||||
}
|
23
src/main/resources/log4j2.xml
Normal file
23
src/main/resources/log4j2.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration>
|
||||
<Appenders>
|
||||
<TerminalConsole name="ConsoleAppender">
|
||||
<PatternLayout disableAnsi="false"
|
||||
pattern="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} %highlight{${LOG_LEVEL_PATTERN:-%5p}}{FATAL=red blink, ERROR=red, WARN=yellow bold, INFO=green, DEBUG=green bold, TRACE=blue} %style{%processId}{magenta} [%15.15t] %style{%-20.20c{1}}{cyan} : %m%n%ex"/>
|
||||
</TerminalConsole>
|
||||
|
||||
<Async name="Async">
|
||||
<AppenderRef ref="ConsoleAppender"/>
|
||||
<LinkedTransferQueue/>
|
||||
</Async>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="INFO">
|
||||
<filters>
|
||||
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY"
|
||||
onMismatch="NEUTRAL"/>
|
||||
</filters>
|
||||
<AppenderRef ref="Async"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- log4j2.xml - Example log4j configuration file
|
||||
Place this file in the same directory as your server.jar, edit
|
||||
to taste, and add -Dlog4j.configurationFile=log4j2.xml to your
|
||||
server startup flags.
|
||||
More log4j example configs can be found at
|
||||
<http://logging.apache.org/log4j/2.x/manual/appenders.html>.
|
||||
-->
|
||||
|
||||
<Configuration>
|
||||
<Appenders>
|
||||
|
||||
<!-- DEFAULT APPENDERS -->
|
||||
|
||||
<!-- console logging - logs to stdout -->
|
||||
<Console name="ConsoleAppender" target="SYSTEM_OUT" follow="true">
|
||||
<ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="ACCEPT"/>
|
||||
<PatternLayout disableAnsi="false"
|
||||
pattern="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} %highlight{${LOG_LEVEL_PATTERN:-%5p}}{FATAL=red blink, ERROR=red, WARN=yellow bold, INFO=green, DEBUG=green bold, TRACE=blue} %style{%processId}{magenta} [%15.15t] %style{%-20.20c{1}}{cyan} : %m%n%ex"/>
|
||||
</Console>
|
||||
<!-- console logging - logs to stderr -->
|
||||
<Console name="ConsoleAppenderErr" target="SYSTEM_ERR" follow="true">
|
||||
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
|
||||
<PatternLayout disableAnsi="false"
|
||||
pattern="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} %highlight{${LOG_LEVEL_PATTERN:-%5p}}{FATAL=red blink, ERROR=red, WARN=yellow bold, INFO=green, DEBUG=green bold, TRACE=blue} %style{%processId}{magenta} [%15.15t] %style{%-20.20c{1}}{cyan} : %m%n%ex"/>
|
||||
</Console>
|
||||
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="com.hazelcast.internal.diagnostics.HealthMonitor" level="WARN"/>
|
||||
<Logger name="it.cavallium.dbengine.database.disk.LLLocalDictionary" level="TRACE"/>
|
||||
<Logger name="it.cavallium.dbengine.database.disk.LLLocalKeyValueDatabase" level="TRACE"/>
|
||||
<Logger name="it.cavallium.dbengine.database.disk.LLLocalLuceneIndex" level="TRACE"/>
|
||||
<Logger name="com.hazelcast" level="INFO"/>
|
||||
<Root level="INFO">
|
||||
<filters>
|
||||
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY"
|
||||
onMismatch="NEUTRAL"/>
|
||||
</filters>
|
||||
<AppenderRef ref="ConsoleAppender"/>
|
||||
<AppenderRef ref="ConsoleAppenderErr"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
Loading…
x
Reference in New Issue
Block a user