Update pom.xml, LibraryVersion.java, and 32 more files...

This commit is contained in:
Andrea Cavalli 2020-10-14 01:38:44 +02:00
parent d67685c679
commit f3eed20bf7
34 changed files with 3577 additions and 0 deletions

207
pom.xml Normal file
View File

@ -0,0 +1,207 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>it.tdlight</groupId>
<artifactId>tdlib-session-container</artifactId>
<version>3.169.50</version>
<name>TDLib Session Container</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- required for jdk9 -->
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>protoarch</id>
<name>protoarch</name>
<url>http://home.apache.org/~aajisaka/repository</url>
</repository>
<repository>
<id>mchv-release</id>
<name>MCHV Release Apache Maven Packages</name>
<url>https://mvn.mchv.eu/repository/mchv</url>
</repository>
<repository>
<id>mchv-snapshot</id>
<name>MCHV Snapshot Apache Maven Packages</name>
<url>https://mvn.mchv.eu/repository/mchv-snapshot</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>3.9.3</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-hazelcast</artifactId>
<version>3.9.3</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-reactive-streams</artifactId>
<version>3.9.3</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-circuit-breaker</artifactId>
<version>3.9.3</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.3.10.RELEASE</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-tools</artifactId>
<version>3.3.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.12.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.12.1</version>
</dependency>
<dependency>
<groupId>org.warp</groupId>
<artifactId>common-utils</artifactId>
<version>1.0.8</version>
</dependency>
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-java</artifactId>
<version>3.169.50</version>
</dependency>
<dependency>
<groupId>it.cavallium</groupId>
<artifactId>concurrent-locks</artifactId>
<version>1.0.5</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>it.unimi.dsi</groupId>
<artifactId>fastutil</artifactId>
<version>8.3.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>RELEASE</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
</dependencies>
<build>
<testSourceDirectory>src/test/java</testSourceDirectory>
<resources>
<resource>
<directory>../src/main/libs</directory>
<excludes>
<exclude>**/*.jar</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>**/*.jar</exclude>
</excludes>
</resource>
</resources>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.5.0.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>3.0.0-M1</version>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>templating-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>filtering-java-templates</id>
<goals>
<goal>filter-sources</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.7.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,4 @@
package org.tdlibsessioncontainer.utils.generated;
public final class LibraryVersion {
public static final String VERSION = "${project.version}";
}

View File

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

View File

@ -0,0 +1,65 @@
package it.tdlight.tdlibsession;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import it.tdlight.jni.TdApi;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
public class TdGson {
private static final TdApiGenericSerializer<?> tdApiGenericSerializerInstance = new TdApiGenericSerializer<>();
private static final ArrayList<Class<?>> abstractClassesSerializers = new ArrayList<>();
static {
for (Class<?> declaredClass : TdApi.class.getDeclaredClasses()) {
var modifiers = declaredClass.getModifiers();
if (Modifier.isAbstract(modifiers) && Modifier.isPublic(modifiers) && Modifier
.isStatic(modifiers)) {
abstractClassesSerializers.add(declaredClass);
}
}
}
public static GsonBuilder registerAdapters(GsonBuilder gsonBuilder) {
for (Class<?> abstractClassesSerializer : abstractClassesSerializers) {
gsonBuilder.registerTypeAdapter(abstractClassesSerializer, tdApiGenericSerializerInstance);
}
return gsonBuilder;
}
public static class TdApiGenericSerializer<T> implements JsonSerializer<T>, JsonDeserializer<T> {
@Override
public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject result = new JsonObject();
result.add("type", new JsonPrimitive(src.getClass().getSimpleName()));
result.add("properties", context.serialize(src, src.getClass()));
return result;
}
@Override
public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String type = jsonObject.get("type").getAsString().replaceAll("[^a-zA-Z0-9]", "");
JsonElement element = jsonObject.get("properties");
try {
return context
.deserialize(element, Class.forName(TdApi.class.getCanonicalName() + "$" + type));
} catch (ClassNotFoundException cnfe) {
throw new JsonParseException("Unknown element type: " + type, cnfe);
}
}
}
}

View File

@ -0,0 +1,10 @@
package it.tdlight.tdlibsession;
public class VariableWrapper<T> {
public volatile T var;
public VariableWrapper(T value) {
this.var = value;
}
}

View File

@ -0,0 +1,74 @@
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() {
try {
if (Files.isReadable(keyStorePasswordPath) && Files.size(keyStorePasswordPath) >= 6) {
return Files.readString(keyStorePasswordPath, StandardCharsets.UTF_8).split("\n")[0];
} else {
throw new NoSuchElementException("No keystore password is set on '" + keyStorePasswordPath.toString() + "'");
}
} catch (IOException ex) {
throw new FileSystemException(ex);
}
}
public Path getTrustStorePath() {
return trustStorePath;
}
public Path getTrustStorePasswordPath() {
return trustStorePasswordPath;
}
public String getTrustStorePassword() {
try {
if (Files.isReadable(trustStorePasswordPath) && Files.size(trustStorePasswordPath) >= 6) {
return Files.readString(trustStorePasswordPath, StandardCharsets.UTF_8).split("\n")[0];
} else {
throw new NoSuchElementException("No truststore password is set on '" + trustStorePasswordPath.toString() + "'");
}
} catch (IOException ex) {
throw new FileSystemException(ex);
}
}
@Override
public String toString() {
return new StringJoiner(", ", SecurityInfo.class.getSimpleName() + "[", "]")
.add("keyStorePath=" + keyStorePath)
.add("keyStorePasswordPath=" + keyStorePasswordPath)
.add("trustStorePath=" + trustStorePath)
.add("trustStorePasswordPath=" + trustStorePasswordPath)
.toString();
}
}

View File

@ -0,0 +1,155 @@
package it.tdlight.tdlibsession.remoteclient;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.JksOptions;
import it.tdlight.common.Init;
import it.tdlight.common.utils.CantLoadLibrary;
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
import it.tdlight.tdlibsession.td.middle.server.AsyncTdMiddleEventBusServer;
import java.io.IOException;
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.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import it.tdlight.utils.MonoUtils;
import reactor.core.publisher.Mono;
import reactor.core.publisher.ReplayProcessor;
public class TDLibRemoteClient implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(TDLibRemoteClient.class);
private final SecurityInfo securityInfo;
private final String masterHostname;
private final String netInterface;
private final int port;
private final Set<String> membersAddresses;
private final LinkedHashSet<String> botIds;
private final ReplayProcessor<TdClusterManager> clusterManager = ReplayProcessor.cacheLast();
public TDLibRemoteClient(SecurityInfo securityInfo, String masterHostname, String netInterface, int port, Set<String> membersAddresses, Set<String> botIds) {
this.securityInfo = securityInfo;
this.masterHostname = masterHostname;
this.netInterface = netInterface;
this.port = port;
this.membersAddresses = membersAddresses;
this.botIds = new LinkedHashSet<>(botIds);
try {
Init.start();
} 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(","));
Set<String> botIds = Set.of(args[3].split(","));
Path keyStorePath = Paths.get(args[4]);
Path keyStorePasswordPath = Paths.get(args[5]);
Path trustStorePath = Paths.get(args[6]);
Path trustStorePasswordPath = Paths.get(args[7]);
var loggerContext = (org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false);
loggerContext.setConfigLocation(TDLibRemoteClient.class.getResource("/tdlib-session-container-log4j2.xml").toURI());
var securityInfo = new SecurityInfo(keyStorePath, keyStorePasswordPath, trustStorePath, trustStorePasswordPath);
new TDLibRemoteClient(securityInfo, masterHostname, netInterface, port, membersAddresses, botIds).run(x -> {});
}
public void start(Handler<Void> startedEventHandler) throws IllegalStateException {
run(startedEventHandler);
}
public void run(Handler<Void> startedEventHandler) {
try {
// 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);
var keyStoreOptions = new JksOptions()
.setPath(securityInfo.getKeyStorePath().toAbsolutePath().toString())
.setPassword(securityInfo.getKeyStorePassword());
var trustStoreOptions = new JksOptions()
.setPath(securityInfo.getTrustStorePath().toAbsolutePath().toString())
.setPassword(securityInfo.getTrustStorePassword());
Mono<TdClusterManager> flux;
if (!botIds.isEmpty()) {
flux = TdClusterManager.ofNodes(keyStoreOptions,
trustStoreOptions,
false,
masterHostname,
netInterface,
port,
membersAddresses
);
} else {
flux = Mono.empty();
}
flux
.doOnNext(clusterManager::onNext)
.doOnTerminate(clusterManager::onComplete)
.doOnError(clusterManager::onError)
.flatMapIterable(clusterManager -> botIds
.stream()
.map(id -> Map.entry(clusterManager, id))
.collect(Collectors.toList()))
.flatMap(entry -> Mono.<String>create(sink -> {
entry
.getKey()
.getVertx()
.deployVerticle(new AsyncTdMiddleEventBusServer(entry.getKey()),
new DeploymentOptions().setConfig(new JsonObject()
.put("botAddress", entry.getValue())
.put("botAlias", entry.getValue())
.put("local", false)),
MonoUtils.toHandler(sink)
);
}))
.doOnError(ex -> {
logger.error(ex.getLocalizedMessage(), ex);
}).subscribe(i -> {}, e -> {}, () -> startedEventHandler.handle(null));
} catch (IOException ex) {
logger.error("Remote client error", ex);
}
}
@Override
public void close() {
clusterManager.blockFirst();
}
}

View File

@ -0,0 +1,126 @@
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 final Throwable cause;
public ResponseError(@NotNull Function function, @NotNull String botName, @NotNull TdApi.Error cause) {
super("Bot '" + botName + "' failed the request '" + functionToInlineString(function) + "': " + cause.code + " " + cause.message);
this.botName = botName;
this.tag = functionToInlineString(function);
this.code = cause.code;
this.message = cause.message;
this.cause = null;
}
public ResponseError(@NotNull String tag, @NotNull String botName, @NotNull TdApi.Error cause) {
super("Bot '" + botName + "' failed the request '" + tag + "': " + cause.code + " " + cause.message);
this.botName = botName;
this.tag = tag;
this.code = cause.code;
this.message = cause.message;
this.cause = null;
}
public ResponseError(@NotNull Function function, @NotNull String botName, @NotNull Throwable cause) {
super("Bot '" + botName + "' failed the request '" + functionToInlineString(function) + "': " + cause.getMessage());
this.botName = botName;
this.tag = functionToInlineString(function);
this.code = 500;
this.message = cause.getMessage();
this.cause = cause;
}
public ResponseError(@NotNull String tag, @NotNull String botName, @NotNull Throwable cause) {
super("Bot '" + botName + "' failed the request '" + tag + "': " + cause.getMessage());
this.botName = botName;
this.tag = tag;
this.code = 500;
this.message = cause.getMessage();
this.cause = cause;
}
public static ResponseError newResponseError(@NotNull Function function, @NotNull String botName, @NotNull TdApi.Error cause) {
return new ResponseError(function, botName, cause);
}
public static ResponseError newResponseError(@NotNull String tag, @NotNull String botName, @NotNull TdApi.Error cause) {
return new ResponseError(tag, botName, cause);
}
public static ResponseError newResponseError(@NotNull Function function, @NotNull String botName, @NotNull Throwable cause) {
return new ResponseError(function, botName, cause);
}
public static ResponseError newResponseError(@NotNull String tag, @NotNull String botName, @NotNull 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;
}
@NotNull
public String getErrorMessage() {
return message;
}
private static String functionToInlineString(Function function) {
return function
.toString()
.replace("\n", " ")
.replace("\t", "")
.replace(" ", "")
.replace(" = ", "=");
}
}

View File

@ -0,0 +1,12 @@
package it.tdlight.tdlibsession.td;
public class TdError extends RuntimeException {
public TdError(int code, String message) {
super(code + " " + message);
}
public TdError(int code, String message, Throwable cause) {
super(code + " " + message, cause);
}
}

View File

@ -0,0 +1,277 @@
package it.tdlight.tdlibsession.td;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Error;
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) {
return new TdResultImpl<T>(value, null);
}
static <T extends TdApi.Object> TdResult<T> failed(@NotNull TdApi.Error 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;
}
}
}

View File

@ -0,0 +1,24 @@
package it.tdlight.tdlibsession.td;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.Object;
public class TdResultMessage {
public final TdApi.Object value;
public final TdApi.Error cause;
public TdResultMessage(Object value, Error cause) {
this.value = value;
this.cause = cause;
}
public <T extends Object> TdResult<T> toTdResult() {
if (value != null) {
//noinspection unchecked
return TdResult.succeeded((T) value);
} else {
return TdResult.failed(cause);
}
}
}

View File

@ -0,0 +1,43 @@
package it.tdlight.tdlibsession.td.direct;
import io.vertx.core.AsyncResult;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.Update;
import it.tdlight.tdlibsession.td.TdResult;
import java.time.Duration;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface AsyncTdDirect {
/**
* Receives incoming updates and request responses from TDLib. May be called from any thread, but
* shouldn't be called simultaneously from two different threads.
*
* @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
* @return An incoming update or request response list. The object returned in the response may be
* an empty list if the timeout expires.
*/
Flux<AsyncResult<TdResult<Update>>> getUpdates(Duration receiveDuration, int eventsSize);
/**
* Sends request to TDLib. May be called from any thread.
*
* @param request Request to TDLib.
* @param synchronous Execute synchronously.
* @return The request response or {@link it.tdlight.jni.TdApi.Error}.
*/
<T extends TdApi.Object> Mono<TdResult<T>> execute(Function request, boolean synchronous);
/**
* Initializes the client and TDLib instance.
*/
Mono<Void> initializeClient();
/**
* Destroys the client and TDLib instance.
*/
Mono<Void> destroyClient();
}

View File

@ -0,0 +1,113 @@
package it.tdlight.tdlibsession.td.direct;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import it.tdlight.common.TelegramClient;
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.Update;
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
import it.tdlight.tdlibsession.td.TdResult;
import it.tdlight.tdlight.ClientManager;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.EmitterProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
public class AsyncTdDirectImpl implements AsyncTdDirect {
private static final Logger logger = LoggerFactory.getLogger(AsyncTdDirect.class);
private final AtomicReference<TelegramClient> td = new AtomicReference<>();
private final Scheduler tdScheduler = Schedulers.newSingle("TdMain");
private final Scheduler tdPollScheduler = Schedulers.newSingle("TdPoll");
private final Scheduler tdUpdatesScheduler = Schedulers.newSingle("TdUpdate");
private final Scheduler tdResponsesScheduler = Schedulers.newSingle("TdResponse");
private final EmitterProcessor<AsyncResult<TdResult<Update>>> updatesProcessor = EmitterProcessor.create();
private final String botAlias;
public AsyncTdDirectImpl(String botAlias) {
this.botAlias = botAlias;
}
@Override
public <T extends TdApi.Object> Mono<TdResult<T>> execute(Function request, boolean synchronous) {
if (synchronous) {
return Mono.just(TdResult.of(this.td.get().execute(request)));
} else {
return Mono.<TdResult<T>>create(sink -> {
try {
this.td.get().send(request, v -> {
sink.success(TdResult.of(v));
}, sink::error);
} catch (Throwable t) {
sink.error(t);
}
}).subscribeOn(tdResponsesScheduler);
}
}
@Override
public Flux<AsyncResult<TdResult<Update>>> getUpdates(Duration receiveDuration, int eventsSize) {
return Flux.from(updatesProcessor.subscribeOn(tdUpdatesScheduler));
}
public Scheduler getTdUpdatesScheduler() {
return tdUpdatesScheduler;
}
public Scheduler getTdResponsesScheduler() {
return tdResponsesScheduler;
}
@Override
public Mono<Void> initializeClient() {
return Mono.<Boolean>create(sink -> {
Flux.<AsyncResult<TdResult<Update>>>create(emitter -> {
var client = ClientManager.create((Object object) -> {
emitter.next(Future.succeededFuture(TdResult.of(object)));
// Close the emitter if receive closed state
if (object.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR
&& ((UpdateAuthorizationState) object).authorizationState.getConstructor()
== AuthorizationStateClosed.CONSTRUCTOR) {
emitter.complete();
}
}, updateError -> {
emitter.next(Future.failedFuture(updateError));
}, error -> {
emitter.next(Future.failedFuture(error));
});
this.td.set(client);
emitter.onDispose(() -> {
this.td.set(null);
});
}).subscribeOn(tdPollScheduler).subscribe(next -> {
updatesProcessor.onNext(next);
sink.success(true);
}, error -> {
updatesProcessor.onError(error);
sink.error(error);
}, () -> {
updatesProcessor.onComplete();
sink.success(true);
});
}).single().then().subscribeOn(tdScheduler);
}
@Override
public Mono<Void> destroyClient() {
return Mono.fromCallable(() -> {
// do nothing
return (Void) null;
}).single().subscribeOn(tdScheduler);
}
}

View File

@ -0,0 +1,460 @@
package it.tdlight.tdlibsession.td.easy;
import it.tdlight.common.utils.ScannerUtils;
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.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.CheckAuthenticationPassword;
import it.tdlight.jni.TdApi.CheckDatabaseEncryptionKey;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.Object;
import it.tdlight.jni.TdApi.OptionValue;
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.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.util.Comparator;
import java.util.Set;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.warp.commonutils.error.InitializationException;
import reactor.core.publisher.EmitterProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.ReplayProcessor;
import reactor.core.scheduler.Schedulers;
public class AsyncTdEasy {
private static final Logger logger = LoggerFactory.getLogger(AsyncTdEasy.class);
private final ReplayProcessor<AuthorizationState> authState = ReplayProcessor.cacheLastOrDefault(new AuthorizationStateClosed());
private final ReplayProcessor<Boolean> requestedDefinitiveExit = ReplayProcessor.cacheLastOrDefault(false);
private final ReplayProcessor<TdEasySettings> settings = ReplayProcessor.cacheLast();
private final EmitterProcessor<Error> globalErrors = EmitterProcessor.create();
private final EmitterProcessor<FatalErrorType> fatalErrors = EmitterProcessor.create();
private final AsyncTdMiddle td;
private final String logName;
private final Flux<Update> incomingUpdatesCo;
public AsyncTdEasy(AsyncTdMiddle td, String logName) {
this.td = td;
this.logName = logName;
var sch = Schedulers.newSingle("TdEasyUpdates");
// todo: use Duration.ZERO instead of 10ms interval
this.incomingUpdatesCo = td.getUpdates()
.filterWhen(update -> Mono.from(requestedDefinitiveExit).map(requestedDefinitiveExit -> !requestedDefinitiveExit))
.subscribeOn(sch)
.publishOn(sch)
.flatMap(this::preprocessUpdates)
.flatMap(update -> Mono.from(this.getState()).single().map(state -> new AsyncTdUpdateObj(state, update)))
.filter(upd -> upd.getState().getConstructor() == AuthorizationStateReady.CONSTRUCTOR)
.map(upd -> (TdApi.Update) upd.getUpdate())
.doOnError(ex -> {
logger.error(ex.getLocalizedMessage(), ex);
}).doOnNext(v -> {
if (logger.isDebugEnabled()) logger.debug(v.toString());
}).doOnComplete(() -> {
authState.onNext(new AuthorizationStateClosed());
})
.publish().refCount(1);
}
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);
}
}
return true;
})
.subscribeOn(Schedulers.boundedElastic())
.flatMap(_v -> {
this.settings.onNext(settings);
return Mono.empty();
});
}
/**
* Get TDLib state
*/
public Flux<AuthorizationState> getState() {
return Flux.from(authState);
}
/**
* Get incoming updates from TDLib.
*/
public Flux<TdApi.Update> getIncomingUpdates() {
return getIncomingUpdates(false);
}
private Flux<TdApi.Update> getIncomingUpdates(boolean includePreAuthUpdates) {
return Flux.from(incomingUpdatesCo);
}
/**
* Get generic error updates from TDLib (When they are not linked to a precise request).
*/
public Flux<TdApi.Error> getIncomingErrors() {
return Flux.from(globalErrors);
}
/**
* Receives fatal errors from TDLib.
*/
public Flux<FatalErrorType> getFatalErrors() {
return Flux.from(fatalErrors);
}
/**
* Sends request to TDLib.
* @return The response or {@link TdApi.Error}.
*/
public <T extends Object> Mono<TdResult<T>> send(TdApi.Function request) {
return td.execute(request, false);
}
private <T extends TdApi.Object> Mono<TdResult<T>> sendDirectly(TdApi.Function obj) {
return td.execute(obj, false);
}
/**
* Set verbosity level
* @param i level
*/
public Mono<Void> setVerbosityLevel(int i) {
return sendDirectly(new TdApi.SetLogVerbosityLevel(i)).then();
}
/**
* Clear option on TDLib
* @param name option name
*/
public Mono<Void> clearOption(String name) {
return sendDirectly(new TdApi.SetOption(name, new TdApi.OptionValueEmpty())).then();
}
/**
* 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))).then();
}
/**
* 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))).then();
}
/**
* 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))).then();
}
/**
* 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)).<OptionValue>handle(MonoUtils::orElseThrow).flatMap((TdApi.OptionValue 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)).<TdApi.OptionValue>handle(MonoUtils::orElseThrow).flatMap((TdApi.OptionValue 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.<TdApi.OptionValue>sendDirectly(new TdApi.GetOption(name)).<TdApi.OptionValue>handle(MonoUtils::orElseThrow).flatMap((TdApi.OptionValue 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.
* @return The request response.
*/
public <T extends Object> Mono<TdResult<T>> execute(TdApi.Function request) {
return td.execute(request, true);
}
/**
* Set if skip updates or not
*/
public Mono<Void> setSkipUpdates(boolean skipUpdates) { //todo: do this
return null;
}
/**
* Closes the client gracefully by sending {@link TdApi.Close}.
*/
public Mono<Void> close() {
return Mono.from(getState())
.filter(state -> {
switch (state.getConstructor()) {
case AuthorizationStateClosing.CONSTRUCTOR:
case AuthorizationStateClosed.CONSTRUCTOR:
return false;
default:
return true;
}
})
.then(Mono.from(requestedDefinitiveExit).single())
.filter(closeRequested -> !closeRequested)
.doOnSuccess(v -> requestedDefinitiveExit.onNext(true))
.then(td.execute(new TdApi.Close(), false))
.then();
}
/**
*
* @param timeout Timeout in seconds when reading data
*/
public void setReadTimeout(int timeout) {
//todo: do this
}
/**
*
* @param timeout Timeout in seconds when listening methods or connecting
*/
public void setMethodTimeout(int timeout) {
//todo: do this
}
private Mono<? extends Object> catchErrors(Object obj) {
if (obj.getConstructor() == Error.CONSTRUCTOR) {
var error = (Error) obj;
switch (error.message) {
case "PHONE_CODE_INVALID":
globalErrors.onNext(error);
return Mono.just(new AuthorizationStateWaitCode());
case "PASSWORD_HASH_INVALID":
globalErrors.onNext(error);
return Mono.just(new AuthorizationStateWaitPassword());
case "PHONE_NUMBER_INVALID":
fatalErrors.onNext(FatalErrorType.PHONE_NUMBER_INVALID);
break;
case "ACCESS_TOKEN_INVALID":
fatalErrors.onNext(FatalErrorType.ACCESS_TOKEN_INVALID);
break;
case "CONNECTION_KILLED":
fatalErrors.onNext(FatalErrorType.CONNECTION_KILLED);
break;
default:
globalErrors.onNext(error);
break;
}
return Mono.empty();
}
return Mono.just(obj);
}
public Mono<Boolean> isBot() {
return Mono.from(settings).single().map(TdEasySettings::isBotTokenSet);
}
private Publisher<Update> preprocessUpdates(Update updateObj) {
return Mono
.just(updateObj)
.flatMap(this::catchErrors)
.filter(obj -> obj.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR)
.map(obj -> ((UpdateAuthorizationState) obj).authorizationState)
.flatMap(obj -> {
this.authState.onNext(new AuthorizationStateReady());
switch (obj.getConstructor()) {
case AuthorizationStateWaitTdlibParameters.CONSTRUCTOR:
return Mono.from(this.settings).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(this::sendDirectly).then();
case AuthorizationStateWaitEncryptionKey.CONSTRUCTOR:
return sendDirectly(new CheckDatabaseEncryptionKey()).then();
case AuthorizationStateWaitPhoneNumber.CONSTRUCTOR:
return Mono.from(this.settings).flatMap(settings -> {
if (settings.isPhoneNumberSet()) {
return sendDirectly(new SetAuthenticationPhoneNumber(String.valueOf(settings.getPhoneNumber()),
new PhoneNumberAuthenticationSettings(false, false, false)
));
} else if (settings.isBotTokenSet()) {
return sendDirectly(new CheckAuthenticationBotToken(settings.getBotToken()));
} else {
return Mono.error(new IllegalArgumentException("A bot is neither an user or a bot"));
}
}).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);
}
while (registerUser.firstName == null || registerUser.firstName.length() <= 0
|| registerUser.firstName.length() > 64 || registerUser.firstName.isBlank()) {
registerUser.firstName = ScannerUtils.askParameter(this.logName, "Enter First Name").trim();
}
while (registerUser.lastName == null || registerUser.firstName.length() > 64) {
registerUser.lastName = ScannerUtils.askParameter(this.logName, "Enter Last Name").trim();
}
return sendDirectly(registerUser).then();
case AuthorizationStateWaitPassword.CONSTRUCTOR:
var authorizationStateWaitPassword = (AuthorizationStateWaitPassword) obj;
String passwordMessage = "Password authorization of '" + this.logName + "':";
if (authorizationStateWaitPassword.passwordHint != null && !authorizationStateWaitPassword.passwordHint.isBlank()) {
passwordMessage += "\n\tHint: " + authorizationStateWaitPassword.passwordHint;
}
logger.info(passwordMessage);
var password = ScannerUtils.askParameter(this.logName, "Enter your password");
return sendDirectly(new CheckAuthenticationPassword(password)).then();
case AuthorizationStateReady.CONSTRUCTOR: {
return Mono.empty();
}
case AuthorizationStateClosed.CONSTRUCTOR:
return Mono.from(requestedDefinitiveExit).doOnNext(closeRequested -> {
if (closeRequested) {
logger.info("AsyncTdEasy closed successfully");
} else {
logger.warn("AsyncTdEasy closed unexpectedly: " + logName);
}
}).flatMap(closeRequested -> {
if (closeRequested) {
return Mono
.from(settings)
.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")
))
.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(closeRequested));
} else {
return Mono.just(closeRequested);
}
}).then();
default:
return Mono.empty();
}
})
.thenReturn(updateObj);
}
}

View File

@ -0,0 +1,49 @@
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();
}
}

View File

@ -0,0 +1,262 @@
package it.tdlight.tdlibsession.td.easy;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
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;
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) {
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");
}
}
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 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 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 TdEasySettings build() {
return new TdEasySettings(useTestDc,
databaseDirectory,
filesDirectory,
useFileDatabase,
useChatInfoDatabase,
useMessageDatabase,
apiId,
apiHash,
systemLanguageCode,
deviceModel,
systemVersion,
applicationVersion,
enableStorageOptimizer,
ignoreFileNames,
phoneNumber,
botToken
);
}
}
}

View File

@ -0,0 +1,24 @@
package it.tdlight.tdlibsession.td.middle;
import it.tdlight.jni.TdApi;
import it.tdlight.tdlibsession.td.TdResult;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface AsyncTdMiddle {
/**
* Receives incoming updates from TDLib.
*
* @return Updates
*/
Flux<TdApi.Update> getUpdates();
/**
* Sends request to TDLib. May be called from any thread.
*
* @param request Request to TDLib.
* @param executeDirectly Execute the function synchronously.
*/
<T extends TdApi.Object> Mono<TdResult<T>> execute(TdApi.Function request, boolean executeDirectly);
}

View File

@ -0,0 +1,10 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.AbstractVerticle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AsyncTdMiddleCommon extends AbstractVerticle {
private static final Logger logger = LoggerFactory.getLogger(AsyncTdMiddleCommon.class);
}

View File

@ -0,0 +1,55 @@
package it.tdlight.tdlibsession.td.middle;
import it.tdlight.jni.TdApi;
import java.util.Objects;
import java.util.StringJoiner;
public class ExecuteObject {
private final boolean executeDirectly;
private final TdApi.Function request;
public ExecuteObject(boolean executeDirectly, TdApi.Function request) {
this.executeDirectly = executeDirectly;
this.request = request;
}
public boolean isExecuteDirectly() {
return executeDirectly;
}
public TdApi.Function getRequest() {
return request;
}
@Override
public boolean equals(Object o) {
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() {
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)
.toString();
}
}

View File

@ -0,0 +1,227 @@
package it.tdlight.tdlibsession.td.middle;
import com.hazelcast.config.Config;
import com.hazelcast.config.EvictionPolicy;
import com.hazelcast.config.GroupConfig;
import com.hazelcast.config.MapConfig;
import com.hazelcast.config.MaxSizeConfig;
import com.hazelcast.config.MaxSizeConfig.MaxSizePolicy;
import com.hazelcast.config.MergePolicyConfig;
import com.hazelcast.config.SemaphoreConfig;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.eventbus.DeliveryOptions;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.eventbus.Message;
import io.vertx.core.eventbus.MessageCodec;
import io.vertx.core.eventbus.MessageConsumer;
import io.vertx.core.http.ClientAuth;
import io.vertx.core.net.JksOptions;
import io.vertx.core.spi.cluster.ClusterManager;
import io.vertx.spi.cluster.hazelcast.HazelcastClusterManager;
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 it.tdlight.utils.MonoUtils;
import reactor.core.publisher.Mono;
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 EventBus eb;
public TdClusterManager(ClusterManager mgr, VertxOptions vertxOptions, Vertx vertx, EventBus eventBus) {
this.mgr = mgr;
this.vertxOptions = vertxOptions;
this.vertx = vertx;
this.eb = eventBus;
}
public static Mono<TdClusterManager> ofMaster(JksOptions keyStoreOptions, 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);
} else {
return Mono.error(new AlreadyBoundException());
}
}
public static Mono<TdClusterManager> ofNodes(JksOptions keyStoreOptions, JksOptions trustStoreOptions, boolean onlyLocal, String masterHostname, String netInterface, int port, Set<String> nodesAddresses) {
if (definedNodesCluster.compareAndSet(false, true)) {
var vertxOptions = new VertxOptions();
netInterface = 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, netInterface, port, nodesAddresses);
} else {
return Mono.error(new AlreadyBoundException());
}
}
public static Mono<TdClusterManager> of(@Nullable Config cfg,
VertxOptions vertxOptions,
JksOptions keyStoreOptions,
JksOptions trustStoreOptions,
String masterHostname,
String netInterface,
int port,
Set<String> nodesAddresses) {
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.subs")
.setBackupCount(1)
.setTimeToLiveSeconds(0)
.setMaxIdleSeconds(0)
.setEvictionPolicy(EvictionPolicy.NONE)
.setMaxSizeConfig(new MaxSizeConfig().setMaxSizePolicy(MaxSizePolicy.PER_NODE).setSize(0))
.setMergePolicyConfig(new MergePolicyConfig().setPolicy("com.hazelcast.map.merge.LatestUpdateMapMergePolicy")));
cfg.setSemaphoreConfigs(Map.of("__vertx.*", new SemaphoreConfig().setInitialPermits(1)));
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.setGroupConfig(new GroupConfig().setName("dev").setPassword("HzPasswordsAreDeprecated"));
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);
vertxOptions.getEventBusOptions().setKeyStoreOptions(keyStoreOptions);
vertxOptions.getEventBusOptions().setTrustStoreOptions(trustStoreOptions);
vertxOptions.getEventBusOptions().setHost(masterHostname);
vertxOptions.getEventBusOptions().setPort(port + 1);
vertxOptions.getEventBusOptions().setSsl(true).setEnabledSecureTransportProtocols(Set.of("TLSv1.3", "TLSv1.2"));
vertxOptions.getEventBusOptions().setClientAuth(ClientAuth.REQUIRED);
} else {
mgr = null;
vertxOptions.setClusterManager(null);
vertxOptions.getEventBusOptions().setClustered(false);
}
return Mono
.<Vertx>create(sink -> {
if (mgr != null) {
Vertx.clusteredVertx(vertxOptions, MonoUtils.toHandler(sink));
} else {
sink.success(Vertx.vertx(vertxOptions));
}
})
.map(vertx -> new TdClusterManager(mgr, vertxOptions, vertx, vertx.eventBus()));
}
public Vertx getVertx() {
return vertx;
}
public EventBus getEventBus() {
return eb;
}
public VertxOptions getVertxOptions() {
return vertxOptions;
}
public DeliveryOptions newDeliveryOpts() {
return new DeliveryOptions().setSendTimeout(120000);
}
/**
*
* @param objectClass
* @param messageCodec
* @param <T>
* @return true if registered, false if already registered
*/
public <T> boolean registerDefaultCodec(Class<T> objectClass, MessageCodec<T, T> messageCodec) {
try {
eb.registerDefaultCodec(objectClass, 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 eb.localConsumer(address);
} else {
return eb.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 eb.localConsumer(address, handler);
} else {
return eb.consumer(address, handler);
}
}
}

View File

@ -0,0 +1,62 @@
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.unimi.dsi.fastutil.io.FastByteArrayInputStream;
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class TdExecuteObjectMessageCodec implements MessageCodec<ExecuteObject, ExecuteObject> {
public TdExecuteObjectMessageCodec() {
super();
}
@Override
public void encodeToWire(Buffer buffer, ExecuteObject t) {
try (var bos = new FastByteArrayOutputStream()) {
try (var dos = new DataOutputStream(bos)) {
dos.writeBoolean(t.isExecuteDirectly());
t.getRequest().serialize(dos);
}
bos.trim();
buffer.appendBytes(bos.array);
} catch (IOException ex) {
ex.printStackTrace();
}
}
@Override
public ExecuteObject decodeFromWire(int pos, Buffer buffer) {
try (var fis = new FastByteArrayInputStream(buffer.getBytes(pos, buffer.length()))) {
try (var dis = new DataInputStream(fis)) {
return new ExecuteObject(dis.readBoolean(), (Function) TdApi.Deserializer.deserialize(dis));
}
} catch (IOException ex) {
ex.printStackTrace();
}
return null;
}
@Override
public ExecuteObject 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;
}
}

View File

@ -0,0 +1,66 @@
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.unimi.dsi.fastutil.io.FastByteArrayInputStream;
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
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 bos = new FastByteArrayOutputStream()) {
try (var dos = new DataOutputStream(bos)) {
t.serialize(dos);
}
bos.trim();
buffer.appendBytes(bos.array);
} catch (IOException ex) {
ex.printStackTrace();
}
}
@Override
public T decodeFromWire(int pos, Buffer buffer) {
try (var fis = new FastByteArrayInputStream(buffer.getBytes(pos, buffer.length()))) {
try (var dis = new DataInputStream(fis)) {
//noinspection unchecked
return (T) TdApi.Deserializer.deserialize(dis);
}
} catch (IOException ex) {
ex.printStackTrace();
}
return null;
}
@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;
}
}

View File

@ -0,0 +1,90 @@
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.jni.TdApi.Update;
import it.tdlight.tdlibsession.td.TdResult;
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream;
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
public class TdOptListMessageCodec implements MessageCodec<TdOptionalList, TdOptionalList> {
public TdOptListMessageCodec() {
super();
}
@Override
public void encodeToWire(Buffer buffer, TdOptionalList ts) {
try (var bos = new FastByteArrayOutputStream()) {
try (var dos = new DataOutputStream(bos)) {
if (ts.isSet()) {
var t = ts.getValues();
dos.writeInt(t.size());
for (TdResult<Update> t1 : t) {
if (t1.succeeded()) {
dos.writeBoolean(true);
t1.result().serialize(dos);
} else {
dos.writeBoolean(false);
t1.cause().serialize(dos);
}
}
} else {
dos.writeInt(-1);
}
}
bos.trim();
buffer.appendBytes(bos.array);
} catch (IOException ex) {
ex.printStackTrace();
}
}
@Override
public TdOptionalList decodeFromWire(int pos, Buffer buffer) {
try (var fis = new FastByteArrayInputStream(buffer.getBytes(pos, buffer.length()))) {
try (var dis = new DataInputStream(fis)) {
var size = dis.readInt();
if (size < 0) {
return new TdOptionalList(false, Collections.emptyList());
} else {
ArrayList<TdResult<TdApi.Update>> list = new ArrayList<>();
for (int i = 0; i < size; i++) {
if (dis.readBoolean()) {
list.add(TdResult.succeeded((Update) TdApi.Deserializer.deserialize(dis)));
} else {
list.add(TdResult.failed((Error) TdApi.Deserializer.deserialize(dis)));
}
}
return new TdOptionalList(true, list);
}
}
} catch (IOException | UnsupportedOperationException ex) {
ex.printStackTrace();
return new TdOptionalList(false, Collections.emptyList());
}
}
@Override
public TdOptionalList transform(TdOptionalList ts) {
return ts;
}
@Override
public String name() {
return "TdOptListCodec";
}
@Override
public byte systemCodecID() {
// Always -1
return -1;
}
}

View File

@ -0,0 +1,57 @@
package it.tdlight.tdlibsession.td.middle;
import it.tdlight.jni.TdApi;
import it.tdlight.tdlibsession.td.TdResult;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
public class TdOptionalList {
private final boolean isSet;
private final List<TdResult<TdApi.Update>> values;
public TdOptionalList(boolean isSet, List<TdResult<TdApi.Update>> values) {
this.isSet = isSet;
this.values = values;
}
public boolean isSet() {
return isSet;
}
public List<TdResult<TdApi.Update>> getValues() {
return values;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TdOptionalList that = (TdOptionalList) o;
if (isSet != that.isSet) {
return false;
}
return Objects.equals(values, that.values);
}
@Override
public int hashCode() {
int result = (isSet ? 1 : 0);
result = 31 * result + (values != null ? values.hashCode() : 0);
return result;
}
@Override
public String toString() {
return new StringJoiner(", ", TdOptionalList.class.getSimpleName() + "[", "]")
.add("isSet=" + isSet)
.add("values=" + values)
.toString();
}
}

View File

@ -0,0 +1,75 @@
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.tdlibsession.td.TdResultMessage;
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream;
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@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) {
try (var bos = new FastByteArrayOutputStream()) {
try (var dos = new DataOutputStream(bos)) {
if (t.value != null) {
dos.writeBoolean(true);
t.value.serialize(dos);
} else {
dos.writeBoolean(false);
t.cause.serialize(dos);
}
}
bos.trim();
buffer.appendBytes(bos.array);
} catch (IOException ex) {
ex.printStackTrace();
}
}
@Override
public TdResultMessage decodeFromWire(int pos, Buffer buffer) {
try (var fis = new FastByteArrayInputStream(buffer.getBytes(pos, buffer.length()))) {
try (var dis = new DataInputStream(fis)) {
if (dis.readBoolean()) {
return new TdResultMessage(TdApi.Deserializer.deserialize(dis), null);
} else {
return new TdResultMessage(null, (TdApi.Error) TdApi.Deserializer.deserialize(dis));
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
return null;
}
@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;
}
}

View File

@ -0,0 +1,337 @@
package it.tdlight.tdlibsession.td.middle.client;
import io.vertx.circuitbreaker.CircuitBreaker;
import io.vertx.circuitbreaker.CircuitBreakerOptions;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Promise;
import io.vertx.core.eventbus.Message;
import io.vertx.core.json.JsonObject;
import it.tdlight.common.ConstructorDetector;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.Update;
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
import it.tdlight.tdlibsession.td.ResponseError;
import it.tdlight.tdlibsession.td.TdResult;
import it.tdlight.tdlibsession.td.TdResultMessage;
import it.tdlight.tdlibsession.td.middle.AsyncTdMiddle;
import it.tdlight.tdlibsession.td.middle.ExecuteObject;
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
import it.tdlight.tdlibsession.td.middle.TdExecuteObjectMessageCodec;
import it.tdlight.tdlibsession.td.middle.TdMessageCodec;
import it.tdlight.tdlibsession.td.middle.TdOptListMessageCodec;
import it.tdlight.tdlibsession.td.middle.TdOptionalList;
import it.tdlight.tdlibsession.td.middle.TdResultMessageCodec;
import it.tdlight.utils.MonoUtils;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.logging.Level;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.warp.commonutils.error.InitializationException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.ReplayProcessor;
public class AsyncTdMiddleEventBusClient extends AbstractVerticle implements AsyncTdMiddle {
private static final Logger logger = LoggerFactory.getLogger(AsyncTdMiddleEventBusClient.class);
public static final boolean OUTPUT_REQUESTS = false;
public static final byte[] EMPTY = new byte[0];
private final ReplayProcessor<Boolean> tdClosed = ReplayProcessor.cacheLastOrDefault(false);
private ReplayProcessor<Flux<Update>> incomingUpdatesCo = ReplayProcessor.cacheLast();
private TdClusterManager cluster;
private String botAddress;
private String botAlias;
private boolean local;
private long initTime;
@SuppressWarnings({"unchecked", "rawtypes"})
public AsyncTdMiddleEventBusClient(TdClusterManager clusterManager) {
cluster = clusterManager;
if (cluster.registerDefaultCodec(TdOptionalList.class, new TdOptListMessageCodec())) {
cluster.registerDefaultCodec(ExecuteObject.class, new TdExecuteObjectMessageCodec());
cluster.registerDefaultCodec(TdResultMessage.class, new TdResultMessageCodec());
for (Class<?> value : ConstructorDetector.getTDConstructorsUnsafe().values()) {
cluster.registerDefaultCodec(value, new TdMessageCodec(value));
}
}
}
public static Mono<AsyncTdMiddleEventBusClient> getAndDeployInstance(TdClusterManager clusterManager, String botAlias, String botAddress, boolean local) throws InitializationException {
try {
var instance = new AsyncTdMiddleEventBusClient(clusterManager);
var options = new DeploymentOptions().setConfig(new JsonObject()
.put("botAddress", botAddress)
.put("botAlias", botAlias)
.put("local", local));
return MonoUtils.<String>executeAsFuture(promise -> {
clusterManager.getVertx().deployVerticle(instance, options, promise);
}).doOnNext(_v -> {
logger.trace("Deployed verticle for bot address: " + botAddress);
}).thenReturn(instance);
} catch (RuntimeException e) {
throw new InitializationException(e);
}
}
@Override
public void start(Promise<Void> startPromise) {
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 local = config().getBoolean("local");
if (local == null) {
throw new IllegalArgumentException("local is not set!");
}
this.local = local;
this.initTime = System.currentTimeMillis();
CircuitBreaker startBreaker = CircuitBreaker.create("bot-" + botAddress + "-server-online-check-circuit-breaker", vertx,
new CircuitBreakerOptions().setMaxFailures(1).setMaxRetries(4).setTimeout(10000)
)
.retryPolicy(policy -> 4000L)
.openHandler(closed -> {
logger.error("Circuit opened! " + botAddress);
})
.closeHandler(closed -> {
logger.error("Circuit closed! " + botAddress);
});
startBreaker.execute(future -> {
try {
logger.error("Requesting " + botAddress + ".ping");
cluster
.getEventBus()
.request(botAddress + ".ping", EMPTY, cluster.newDeliveryOpts().setLocalOnly(local), pingMsg -> {
if (pingMsg.succeeded()) {
logger.error("Received ping reply (succeeded)");
logger.error("Requesting " + botAddress + ".start");
cluster
.getEventBus()
.request(botAddress + ".start", EMPTY, cluster.newDeliveryOpts().setLocalOnly(local).setSendTimeout(10000), startMsg -> {
if (startMsg.succeeded()) {
logger.error("Requesting " + botAddress + ".isWorking");
cluster
.getEventBus()
.request(botAddress + ".isWorking", EMPTY, cluster.newDeliveryOpts().setLocalOnly(local).setSendTimeout(10000), msg -> {
if (msg.succeeded()) {
this.listen().then(this.pipe()).timeout(Duration.ofSeconds(10)).subscribe(v -> {}, future::fail, future::complete);
} else {
future.fail(msg.cause());
}
});
} else {
future.fail(startMsg.cause());
}
});
} else {
logger.error("Received ping reply (failed) (local=" + local + ")", pingMsg.cause());
future.fail(pingMsg.cause());
}
}
);
} catch (Exception ex) {
future.fail(ex);
}
})
.onFailure(ex -> {
logger.error("Failure when starting bot " + botAddress, ex);
startPromise.fail(new InitializationException("Can't connect tdlib middle client to tdlib middle server!"));
})
.onSuccess(v -> startPromise.complete());
}
@Override
public void stop(Promise<Void> stopPromise) {
tdClosed.onNext(true);
stopPromise.complete();
}
private Mono<Void> listen() {
// Nothing to listen for now
return Mono.empty();
}
private Mono<Void> pipe() {
incomingUpdatesCo.onNext(this.requestUpdatesBatchFromNetwork()
.repeatWhen(nFlux -> {
return Flux.push(emitter -> {
var dispos = Flux.combineLatest(nFlux, tdClosed, Pair::of).subscribe(val -> {
//noinspection PointlessBooleanExpression
if (val.getRight() == true) {
emitter.complete();
} else {
if (val.getLeft() == 0) {
emitter.complete();
} else {
emitter.next(val);
}
}
});
emitter.onDispose(dispos);
});
}) // Repeat when there is one batch with a flux of updates
.flatMap(batch -> batch)
.flatMap(update -> {
return Mono.<Update>create(sink -> {
if (update.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
var state = (UpdateAuthorizationState) update;
if (state.authorizationState.getConstructor() == AuthorizationStateClosed.CONSTRUCTOR) {
tdClosed.onNext(true);
this.getVertx().undeploy(this.deploymentID(), undeployed -> {
if (undeployed.failed()) {
logger.error("Error when undeploying td verticle", undeployed.cause());
}
sink.success(update);
});
} else {
sink.success(update);
}
} else {
sink.success(update);
}
});
})
.log("TdMiddle", Level.FINEST).publish().autoConnect(1));
return Mono.empty();
}
private static class UpdatesBatchResult {
public final Flux<Update> updatesFlux;
public final boolean completed;
private UpdatesBatchResult(Flux<Update> updatesFlux, boolean completed) {
this.updatesFlux = updatesFlux;
this.completed = completed;
}
@Override
public String toString() {
return new StringJoiner(", ", UpdatesBatchResult.class.getSimpleName() + "[", "]")
.add("updatesFlux=" + updatesFlux)
.add("completed=" + completed)
.toString();
}
}
private Mono<Flux<TdApi.Update>> requestUpdatesBatchFromNetwork() {
return Mono
.from(tdClosed)
.single()
.filter(tdClosed -> !tdClosed)
.flatMap(_x -> Mono.<Flux<TdApi.Update>>create(sink -> {
cluster.getEventBus().<TdOptionalList>request(botAddress + ".getNextUpdatesBlock",
EMPTY,
cluster.newDeliveryOpts().setLocalOnly(local),
msg -> {
if (msg.failed()) {
//if (System.currentTimeMillis() - initTime <= 30000) {
// // The serve has not been started
// sink.success(Flux.empty());
//} else {
// // Timeout
sink.error(msg.cause());
//}
} else {
var result = msg.result();
if (result.body() == null) {
sink.success();
} else {
var resultBody = msg.result().body();
if (resultBody.isSet()) {
List<TdResult<Update>> updates = resultBody.getValues();
for (TdResult<Update> updateObj : updates) {
if (updateObj.succeeded()) {
if (OUTPUT_REQUESTS) {
System.out.println(" <- " + updateObj.result()
.toString()
.replace("\n", " ")
.replace("\t", "")
.replace(" ", "")
.replace(" = ", "="));
}
} else {
logger.error("Received an errored update",
ResponseError.newResponseError("incoming update", botAlias, updateObj.cause())
);
}
}
sink.success(Flux.fromIterable(updates).filter(TdResult::succeeded).map(TdResult::result));
} else {
// the stream has ended
sink.success();
}
}
}
}
);
}));
}
@Override
public Flux<Update> getUpdates() {
return incomingUpdatesCo.filter(Objects::nonNull).take(1).single().flatMapMany(v -> v);
}
@Override
public <T extends TdApi.Object> Mono<TdResult<T>> execute(Function request, boolean executeDirectly) {
var req = new ExecuteObject(executeDirectly, request);
if (OUTPUT_REQUESTS) {
System.out.println(" -> " + request.toString()
.replace("\n", " ")
.replace("\t", "")
.replace(" ", "")
.replace(" = ", "="));
}
return Mono.from(tdClosed).single()
.filter(tdClosed -> !tdClosed)
.flatMap(_x -> Mono.<TdResult<T>>create(sink -> {
cluster.getEventBus().request(botAddress + ".execute", req, cluster.newDeliveryOpts().setLocalOnly(local), (AsyncResult<Message<TdResultMessage>> event) -> {
if (event.succeeded()) {
if (event.result().body() == null) {
sink.success();
} else {
sink.success(Objects.requireNonNull(event.result().body()).toTdResult());
}
} else {
sink.error(ResponseError.newResponseError(request, botAlias, event.cause()));
}
});
})).flatMap(response -> {
try {
Objects.requireNonNull(response);
if (OUTPUT_REQUESTS) {
System.out.println(" <- " + response.toString()
.replace("\n", " ")
.replace("\t", "")
.replace(" ", "")
.replace(" = ", "="));
}
return Mono.just((TdResult<T>) response);
} catch (ClassCastException | NullPointerException e) {
return Mono.error(e);
}
});
}
}

View File

@ -0,0 +1,117 @@
package it.tdlight.tdlibsession.td.middle.direct;
import static it.tdlight.tdlibsession.td.middle.server.AsyncTdMiddleEventBusServer.WAIT_DURATION;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.Object;
import it.tdlight.jni.TdApi.Update;
import it.tdlight.tdlibsession.td.ResponseError;
import it.tdlight.tdlibsession.td.TdResult;
import it.tdlight.tdlibsession.td.direct.AsyncTdDirectImpl;
import it.tdlight.tdlibsession.td.middle.AsyncTdMiddle;
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
import it.tdlight.utils.MonoUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.warp.commonutils.error.InitializationException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.ReplayProcessor;
public class AsyncTdMiddleDirect extends AbstractVerticle implements AsyncTdMiddle {
private static final Logger logger = LoggerFactory.getLogger(AsyncTdMiddleDirect.class);
protected final ReplayProcessor<Boolean> tdClosed = ReplayProcessor.cacheLastOrDefault(false);
protected AsyncTdDirectImpl td;
private String botAddress;
private String botAlias;
public AsyncTdMiddleDirect() {
}
public static Mono<AsyncTdMiddleDirect> getAndDeployInstance(TdClusterManager clusterManager,
String botAlias,
String botAddress) throws InitializationException {
try {
var instance = new AsyncTdMiddleDirect();
var options = new DeploymentOptions().setConfig(new JsonObject()
.put("botAlias", botAlias)
.put("botAddress", botAddress));
return MonoUtils.<String>executeAsFuture(promise -> {
clusterManager.getVertx().deployVerticle(instance, options, promise);
}).doOnNext(_v -> {
logger.trace("Deployed verticle for bot " + botAlias + ", address: " + botAddress);
}).thenReturn(instance);
} catch (RuntimeException e) {
throw new InitializationException(e);
}
}
@Override
public void start(Promise<Void> startPromise) {
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;
this.td = new AsyncTdDirectImpl(botAlias);
td.initializeClient().doOnSuccess(v -> startPromise.complete()).subscribe(success -> {
}, (ex) -> {
logger.error("Failure when starting bot " + botAlias + ", address " + botAddress, ex);
startPromise.fail(new InitializationException("Can't connect tdlib middle client to tdlib middle server!"));
}, () -> {});
}
@Override
public void stop(Promise<Void> stopPromise) {
tdClosed.onNext(true);
td.destroyClient().onErrorResume(ex -> {
logger.error("Can't destroy client", ex);
return Mono.empty();
}).doOnTerminate(() -> {
logger.debug("TdMiddle verticle stopped");
}).subscribe(MonoUtils.toSubscriber(stopPromise));
}
@Override
public Flux<Update> getUpdates() {
return Mono.from(tdClosed).filter(closed -> !closed).flatMapMany(_x -> td.getUpdates(WAIT_DURATION, 1000).flatMap(result -> {
if (result.succeeded()) {
if (result.result().succeeded()) {
return Mono.just(result.result().result());
} else {
logger.error("Received an errored update",
ResponseError.newResponseError("incoming update", botAlias, result.result().cause())
);
return Mono.empty();
}
} else {
logger.error("Received an errored update", result.cause());
return Mono.empty();
}
}));
}
@Override
public <T extends Object> Mono<TdResult<T>> execute(Function requestFunction, boolean executeDirectly) {
return td.<T>execute(requestFunction, executeDirectly).onErrorMap(error -> {
return ResponseError.newResponseError(
requestFunction,
botAlias,
error
);
});
}
}

View File

@ -0,0 +1,67 @@
package it.tdlight.tdlibsession.td.middle.direct;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.json.JsonObject;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.Object;
import it.tdlight.jni.TdApi.Update;
import it.tdlight.tdlibsession.td.TdResult;
import it.tdlight.tdlibsession.td.middle.AsyncTdMiddle;
import it.tdlight.tdlibsession.td.middle.client.AsyncTdMiddleEventBusClient;
import it.tdlight.tdlibsession.td.middle.server.AsyncTdMiddleEventBusServer;
import java.util.Objects;
import org.warp.commonutils.error.InitializationException;
import it.tdlight.tdlibsession.td.direct.AsyncTdDirectImpl;
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
import it.tdlight.utils.MonoUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.ReplayProcessor;
public class AsyncTdMiddleLocal implements AsyncTdMiddle {
private final AsyncTdDirectImpl td;
private final AsyncTdMiddleEventBusServer srv;
private final TdClusterManager masterClusterManager;
private ReplayProcessor<AsyncTdMiddleEventBusClient> cli = ReplayProcessor.cacheLast();
private final String botAlias;
private final String botAddress;
public AsyncTdMiddleLocal(TdClusterManager masterClusterManager, String botAlias, String botAddress) throws InitializationException {
this.td = new AsyncTdDirectImpl(botAlias);
this.srv = new AsyncTdMiddleEventBusServer(masterClusterManager);
this.masterClusterManager = masterClusterManager;
this.botAlias = botAlias;
this.botAddress = botAddress;
}
public Mono<AsyncTdMiddleLocal> start() {
return Mono.<String>create(sink -> {
masterClusterManager
.getVertx()
.deployVerticle(srv,
new DeploymentOptions().setConfig(new JsonObject().put("botAddress", botAddress).put("local", true)),
MonoUtils.toHandler(sink)
);
}).onErrorMap(InitializationException::new).flatMap(_x -> {
try {
return AsyncTdMiddleEventBusClient.getAndDeployInstance(masterClusterManager, botAlias, botAddress, true).doOnNext(cli -> {
this.cli.onNext(cli);
}).doOnError(error -> this.cli.onError(error)).doFinally(_v -> this.cli.onComplete());
} catch (InitializationException e) {
this.cli.onError(e);
return Mono.error(e);
}
}).map(v -> this);
}
@Override
public Flux<Update> getUpdates() {
return cli.filter(Objects::nonNull).single().flatMapMany(AsyncTdMiddleEventBusClient::getUpdates);
}
@Override
public <T extends Object> Mono<TdResult<T>> execute(Function request, boolean executeDirectly) {
return cli.filter(Objects::nonNull).single().flatMap(c -> c.execute(request, executeDirectly));
}
}

View File

@ -0,0 +1,239 @@
package it.tdlight.tdlibsession.td.middle.server;
import static it.tdlight.tdlibsession.td.middle.client.AsyncTdMiddleEventBusClient.OUTPUT_REQUESTS;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
import io.vertx.core.Promise;
import io.vertx.core.eventbus.Message;
import it.tdlight.common.ConstructorDetector;
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
import it.tdlight.jni.TdApi.Update;
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
import it.tdlight.tdlibsession.td.TdResult;
import it.tdlight.tdlibsession.td.TdResultMessage;
import it.tdlight.tdlibsession.td.direct.AsyncTdDirectImpl;
import it.tdlight.tdlibsession.td.middle.ExecuteObject;
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
import it.tdlight.tdlibsession.td.middle.TdExecuteObjectMessageCodec;
import it.tdlight.tdlibsession.td.middle.TdMessageCodec;
import it.tdlight.tdlibsession.td.middle.TdOptListMessageCodec;
import it.tdlight.tdlibsession.td.middle.TdOptionalList;
import it.tdlight.tdlibsession.td.middle.TdResultMessageCodec;
import it.tdlight.utils.MonoUtils;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.ReplayProcessor;
import reactor.util.concurrent.Queues;
public class AsyncTdMiddleEventBusServer extends AbstractVerticle {
private static final Logger logger = LoggerFactory.getLogger(AsyncTdMiddleEventBusServer.class);
private static final byte[] EMPTY = new byte[0];
// todo: restore duration to 2 seconds instead of 10 millis, when the bug of tdlight double queue wait is fixed
public static final Duration WAIT_DURATION = Duration.ofSeconds(1);// Duration.ofMillis(10);
private final TdClusterManager cluster;
private String botAlias;
private String botAddress;
private boolean local;
protected final ReplayProcessor<Boolean> tdClosed = ReplayProcessor.cacheLastOrDefault(false);
protected AsyncTdDirectImpl td;
protected final Queue<AsyncResult<TdResult<Update>>> queue = Queues.<AsyncResult<TdResult<Update>>>unbounded().get();
@SuppressWarnings({"unchecked", "rawtypes"})
public AsyncTdMiddleEventBusServer(TdClusterManager clusterManager) {
this.cluster = clusterManager;
if (cluster.registerDefaultCodec(TdOptionalList.class, new TdOptListMessageCodec())) {
cluster.registerDefaultCodec(ExecuteObject.class, new TdExecuteObjectMessageCodec());
cluster.registerDefaultCodec(TdResultMessage.class, new TdResultMessageCodec());
for (Class<?> value : ConstructorDetector.getTDConstructorsUnsafe().values()) {
cluster.registerDefaultCodec(value, new TdMessageCodec(value));
}
}
}
@Override
public void start(Promise<Void> startPromise) {
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 local = config().getBoolean("local");
if (local == null) {
throw new IllegalArgumentException("local is not set!");
}
this.local = local;
this.td = new AsyncTdDirectImpl(botAlias);
cluster.getEventBus().consumer(botAddress + ".ping", (Message<byte[]> msg) -> {
logger.error("Received ping. Replying...");
msg.reply(EMPTY);
logger.error("Replied.");
});
AtomicBoolean alreadyDeployed = new AtomicBoolean(false);
cluster.getEventBus().consumer(botAddress + ".start", (Message<byte[]> msg) -> {
if (alreadyDeployed.compareAndSet(false, true)) {
td.initializeClient()
.then(this.listen())
.then(this.pipe())
.then(Mono.<Void>create(registrationSink -> {
cluster.getEventBus().consumer(botAddress + ".isWorking", (Message<byte[]> workingMsg) -> {
workingMsg.reply(EMPTY, cluster.newDeliveryOpts().setLocalOnly(local));
}).completionHandler(MonoUtils.toHandler(registrationSink));
}))
.subscribe(v -> {}, ex -> {
logger.info(botAddress + " server deployed and started. succeeded: false");
logger.error(ex.getLocalizedMessage(), ex);
msg.fail(500, ex.getLocalizedMessage());
}, () -> {
logger.info(botAddress + " server deployed and started. succeeded: true");
msg.reply(EMPTY);
});
} else {
msg.reply(EMPTY);
}
}).completionHandler(h -> {
logger.info(botAddress + " server deployed. succeeded: " + h.succeeded());
if (h.succeeded()) {
startPromise.complete(h.result());
} else {
startPromise.fail(h.cause());
}
});
}
@Override
public void stop(Promise<Void> stopPromise) {
tdClosed.onNext(true);
td.destroyClient().onErrorResume(ex -> {
logger.error("Can't destroy client", ex);
return Mono.empty();
}).doOnTerminate(() -> {
logger.debug("TdMiddle verticle stopped");
}).subscribe(MonoUtils.toSubscriber(stopPromise));
}
private Mono<Void> listen() {
return Mono.<Void>create(registrationSink -> {
cluster.getEventBus().consumer(botAddress + ".getNextUpdatesBlock", (Message<byte[]> msg) -> {
Mono
.from(tdClosed)
.single()
.filter(tdClosedVal -> !tdClosedVal)
.subscribeOn(td.getTdUpdatesScheduler())
.map(_v -> {
ArrayList<AsyncResult<TdResult<Update>>> updatesBatch = new ArrayList<>();
while (!queue.isEmpty() && updatesBatch.size() < 1000) {
var item = queue.poll();
if (item == null) break;
updatesBatch.add(item);
}
return updatesBatch;
})
.flatMap(receivedList -> {
return Flux.fromIterable(receivedList).flatMap(result -> {
if (result.succeeded()) {
var received = result.result();
if (OUTPUT_REQUESTS) {
System.out.println("<=: " + received
.toString()
.replace("\n", " ")
.replace("\t", "")
.replace(" ", "")
.replace(" = ", "="));
}
return Mono.create(sink -> {
if (received.succeeded() && received.result().getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
var authState = (UpdateAuthorizationState) received.result();
if (authState.authorizationState.getConstructor() == AuthorizationStateClosed.CONSTRUCTOR) {
tdClosed.onNext(true);
vertx.undeploy(deploymentID(), undeployed -> {
if (undeployed.failed()) {
logger.error("Error when undeploying td verticle", undeployed.cause());
}
sink.success();
});
} else {
sink.success();
}
} else {
sink.success();
}
}).then(Mono.<TdResult<Update>>create(sink -> {
sink.success(received);
}));
} else {
logger.error("Received an error update", result.cause());
return Mono.empty();
}
}).collectList().map(list -> new TdOptionalList(true, list));
})
.defaultIfEmpty(new TdOptionalList(false, Collections.emptyList()))
.subscribe(v -> {
msg.reply(v);
}, ex -> {
logger.error("Error when processing a 'receiveUpdates' request", ex);
msg.fail(500, ex.getLocalizedMessage());
}, () -> {});
}).completionHandler(MonoUtils.toHandler(registrationSink));
}).then(Mono.<Void>create(registrationSink -> {
cluster.getEventBus().<ExecuteObject>consumer(botAddress + ".execute", (Message<ExecuteObject> msg) -> {
try {
if (OUTPUT_REQUESTS) {
System.out.println(":=> " + msg
.body()
.getRequest()
.toString()
.replace("\n", " ")
.replace("\t", "")
.replace(" ", "")
.replace(" = ", "="));
}
td.execute(msg.body().getRequest(), msg.body().isExecuteDirectly()).single().subscribe(response -> {
msg.reply(new TdResultMessage(response.result(), response.cause()), cluster.newDeliveryOpts().setLocalOnly(local));
}, ex -> {
msg.fail(500, ex.getLocalizedMessage());
logger.error("Error when processing a request", ex);
});
} catch (ClassCastException ex) {
msg.fail(500, ex.getMessage());
logger.error("Error when deserializing a request", ex);
}
}).completionHandler(MonoUtils.toHandler(registrationSink));
}));
}
private Mono<Void> pipe() {
return Mono.fromCallable(() -> {
td
.getUpdates(WAIT_DURATION, 1000)
.bufferTimeout(1000, local ? Duration.ofMillis(1) : Duration.ofMillis(100))
.subscribe(nextItems -> {
queue.addAll(nextItems);
});
return (Void) null;
});
}
}

View File

@ -0,0 +1,19 @@
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;
}
}));
}
}

View File

@ -0,0 +1,30 @@
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);
}
}

View File

@ -0,0 +1,128 @@
package it.tdlight.utils;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import it.tdlight.jni.TdApi;
import it.tdlight.tdlibsession.td.TdError;
import it.tdlight.tdlibsession.td.TdResult;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.reactivestreams.Subscription;
import org.warp.commonutils.concurrency.future.CompletableFutureUtils;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
import reactor.core.publisher.SynchronousSink;
import reactor.util.context.Context;
public class MonoUtils {
public static <T> Handler<AsyncResult<T>> toHandler(SynchronousSink<T> sink) {
return event -> {
if (event.succeeded()) {
if (event.result() == null) {
sink.complete();
} else {
sink.next(Objects.requireNonNull(event.result()));
}
} else {
sink.error(event.cause());
}
};
}
public static <T> Handler<AsyncResult<T>> toHandler(MonoSink<T> sink) {
return event -> {
if (event.succeeded()) {
if (event.result() == null) {
sink.success();
} else {
sink.success(Objects.requireNonNull(event.result()));
}
} else {
sink.error(event.cause());
}
};
}
public static <T> SynchronousSink<T> toSink(Context context, Promise<T> promise) {
return PromiseSink.of(context, promise);
}
public static <T, R> BiConsumer<? super T, SynchronousSink<R>> executeBlockingSink(Vertx vertx, BiConsumer<? super T, SynchronousSink<R>> handler) {
return (value, sink) -> {
vertx.executeBlocking((Promise<R> finished) -> {
handler.accept(value, PromiseSink.of(sink.currentContext(), finished));
}, toHandler(sink));
};
}
public static <T> Mono<T> executeBlocking(Vertx vertx, Consumer<SynchronousSink<T>> action) {
return Mono.create((MonoSink<T> sink) -> {
vertx.executeBlocking((Promise<T> finished) -> {
action.accept(toSink(sink.currentContext(), finished));
}, toHandler(sink));
});
}
public static <T> Mono<T> executeAsFuture(Consumer<Handler<AsyncResult<T>>> action) {
return Mono.<T>fromFuture(() -> {
return CompletableFutureUtils.getCompletableFuture(() -> {
var resultFuture = new CompletableFuture<T>();
action.accept(handler -> {
if (handler.failed()) {
resultFuture.completeExceptionally(handler.cause());
} else {
resultFuture.complete(handler.result());
}
});
return resultFuture;
});
});
}
public static <T> CoreSubscriber<? super T> toSubscriber(Promise<T> promise) {
return new CoreSubscriber<T>() {
@Override
public void onSubscribe(Subscription s) {
s.request(1);
}
@Override
public void onNext(T t) {
promise.complete(t);
}
@Override
public void onError(Throwable t) {
promise.fail(t);
}
@Override
public void onComplete() {
promise.tryComplete();
}
};
}
public static <R extends TdApi.Object> void orElseThrowFuture(TdResult<R> value, SynchronousSink<CompletableFuture<R>> sink) {
if (value.succeeded()) {
sink.next(CompletableFuture.completedFuture(value.result()));
} else {
sink.next(CompletableFuture.failedFuture(new TdError(value.cause().code, value.cause().message)));
}
}
public static <R extends TdApi.Object> void orElseThrow(TdResult<R> value, SynchronousSink<R> sink) {
if (value.succeeded()) {
sink.next(value.result());
} else {
sink.complete();
//sink.error(new TdError(value.cause().code, value.cause().message));
}
}
}

View File

@ -0,0 +1,49 @@
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;
}
}
}

View File

@ -0,0 +1,39 @@
<?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 status="INFO">
<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="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>