Rewrite using atomix

This commit is contained in:
Andrea Cavalli 2021-12-05 15:15:28 +01:00
parent c3912c2edf
commit 2dc4a35d9f
86 changed files with 951 additions and 6000 deletions

163
pom.xml
View File

@ -3,23 +3,16 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>it.tdlight</groupId>
<artifactId>tdlib-session-container</artifactId>
<version>5.0.${revision}</version>
<name>TDLib Session Container</name>
<artifactId>tdlib-reactive-api</artifactId>
<version>6.0.${revision}</version>
<name>TDLib Reactive API</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<revision>0-SNAPSHOT</revision>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<vertx.version>4.2.0</vertx.version>
<atomix.version>3.0.6</atomix.version>
</properties>
<repositories>
<repository>
<id>protoarch</id>
<name>protoarch</name>
<url>http://home.apache.org/~aajisaka/repository</url>
</repository>
<repository>
<id>mchv-release</id>
<name>MCHV Release Apache Maven Packages</name>
@ -51,51 +44,68 @@
</scm>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-stack-depchain</artifactId>
<version>${vertx.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>it.unimi.dsi</groupId>
<artifactId>fastutil</artifactId>
<version>8.5.6</version>
</dependency>
<dependency>
<groupId>io.atomix</groupId>
<artifactId>atomix</artifactId>
<version>${atomix.version}</version>
</dependency>
<dependency>
<groupId>io.atomix</groupId>
<artifactId>atomix-raft</artifactId>
<version>${atomix.version}</version>
</dependency>
<dependency>
<groupId>io.atomix</groupId>
<artifactId>atomix-primary-backup</artifactId>
<version>${atomix.version}</version>
</dependency>
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-java</artifactId>
<version>2.7.9.4</version>
<version>2.7.9.5</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.4.12</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>net.minecrell</groupId>
<artifactId>terminalconsoleappender</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-hazelcast</artifactId>
<version>${vertx.version}</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-junit5</artifactId>
<version>${vertx.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-reactive-streams</artifactId>
<version>${vertx.version}</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-rx-java2</artifactId>
<version>${vertx.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.1</version>
<version>5.8.2</version>
<scope>test</scope>
<exclusions>
<exclusion>
@ -107,48 +117,24 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.1</version>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-circuit-breaker</artifactId>
<version>${vertx.version}</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.4.11</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-tools</artifactId>
<version>3.4.11</version>
</dependency>
<dependency>
<groupId>io.projectreactor.addons</groupId>
<artifactId>reactor-adapter</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>com.akaita.java</groupId>
<artifactId>rxjava2-debug</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.warp</groupId>
@ -158,38 +144,39 @@
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
<version>2.0.45.Final</version>
<version>2.0.46.Final</version>
</dependency>
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-java</artifactId>
</dependency>
<dependency>
<groupId>it.cavallium</groupId>
<artifactId>concurrent-locks</artifactId>
<version>1.0.8</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>io.atomix</groupId>
<artifactId>atomix</artifactId>
</dependency>
<dependency>
<groupId>io.atomix</groupId>
<artifactId>atomix-raft</artifactId>
</dependency>
<dependency>
<groupId>io.atomix</groupId>
<artifactId>atomix-primary-backup</artifactId>
</dependency>
<dependency>
<groupId>it.unimi.dsi</groupId>
<artifactId>fastutil</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>1.12.0</version>
</dependency>
<dependency>
<groupId>dev.zacsweers.moshix</groupId>
<artifactId>moshi-records-reflect</artifactId>
<version>0.14.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>net.minecrell</groupId>
<artifactId>terminalconsoleappender</artifactId>
</dependency>
</dependencies>
<build>
<resources>
@ -224,7 +211,7 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
<release>17</release>
<useIncrementalCompilation>false</useIncrementalCompilation>
</configuration>
</plugin>

View File

@ -0,0 +1,104 @@
package it.tdlight.reactiveapi;
import io.atomix.core.Atomix;
import io.atomix.core.AtomixBuilder;
import it.tdlight.reactiveapi.CreateSessionRequest.CreateBotSessionRequest;
import it.tdlight.reactiveapi.CreateSessionRequest.CreateUserSessionRequest;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import net.minecrell.terminalconsole.SimpleTerminalConsole;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Cli {
private static final Logger LOG = LoggerFactory.getLogger(Cli.class);
public static void main(String[] args) throws IOException {
var validArgs = Entrypoint.parseArguments(args);
var atomixBuilder = Atomix.builder();
var api = Entrypoint.start(validArgs, atomixBuilder);
AtomicBoolean alreadyShutDown = new AtomicBoolean(false);
AtomicBoolean acceptInputs = new AtomicBoolean(true);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
acceptInputs.set(false);
if (alreadyShutDown.compareAndSet(false, true)) {
api.getAtomix().stop().join();
}
}));
var console = new SimpleTerminalConsole() {
private static final Set<String> commands = Set.of("exit", "stop", "createsession", "help", "man", "?");
@Override
protected LineReader buildReader(LineReaderBuilder builder) {
var reader = super.buildReader(builder);
reader.addCommandsInBuffer(commands);
return reader;
}
@Override
protected boolean isRunning() {
return acceptInputs.get();
}
@Override
protected void runCommand(String command) {
var parts = command.split(" ", 2);
var commandName = parts[0].trim().toLowerCase();
String commandArgs;
if (parts.length > 1) {
commandArgs = parts[1].trim();
} else {
commandArgs = "";
}
switch (commandName) {
case "exit", "stop" -> acceptInputs.set(false);
case "createsession" -> createSession(api, commandArgs);
case "help", "?", "man" -> LOG.info("Commands: {}", commands);
default -> LOG.info("Unknown command \"{}\"", command);
}
}
@Override
protected void shutdown() {
acceptInputs.set(false);
if (alreadyShutDown.compareAndSet(false, true)) {
api.getAtomix().stop().join();
}
}
};
console.start();
}
private static void createSession(ReactiveApi api, String commandArgs) {
var parts = commandArgs.split(" ");
boolean invalid = false;
if (parts.length == 3) {
CreateSessionRequest request = switch (parts[0]) {
case "bot" -> new CreateBotSessionRequest(Long.parseLong(parts[1]), parts[2]);
case "user" -> new CreateUserSessionRequest(Long.parseLong(parts[1]),
Long.parseLong(parts[2]));
default -> {
invalid = true;
yield null;
}
};
if (!invalid) {
CreateSessionResponse response = api.createSession(request).join();
LOG.info("Created a session with session id \"{}\"", response.sessionId());
}
} else {
invalid = true;
}
if (invalid) {
LOG.error("Syntax: CreateSession <\"bot\"|\"user\"> <userid> <token|phoneNumber>");
}
}
}

View File

@ -0,0 +1,21 @@
package it.tdlight.reactiveapi;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* Define the cluster structure
*/
public class ClusterSettings {
public String id;
public List<NodeSettings> nodes;
@JsonCreator
public ClusterSettings(@JsonProperty(required = true, value = "id") String id,
@JsonProperty(required = true, value = "nodes") List<NodeSettings> nodes) {
this.id = id;
this.nodes = nodes;
}
}

View File

@ -0,0 +1,64 @@
package it.tdlight.reactiveapi;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import it.tdlight.reactiveapi.CreateSessionRequest.CreateBotSessionRequest;
import it.tdlight.reactiveapi.CreateSessionRequest.CreateUserSessionRequest;
import it.tdlight.reactiveapi.CreateSessionRequest.LoadSessionFromDiskRequest;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import org.apache.commons.lang3.SerializationException;
public sealed interface CreateSessionRequest permits CreateUserSessionRequest, CreateBotSessionRequest,
LoadSessionFromDiskRequest {
long userId();
static CreateSessionRequest deserializeBytes(byte[] bytes) {
byte type = bytes[0];
long userId = Longs.fromBytes(bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8]);
return switch (type) {
case 0 -> new CreateUserSessionRequest(userId,
Longs.fromBytes(bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], bytes[16])
);
case 1 -> new CreateBotSessionRequest(userId,
new String(bytes, 1 + Long.BYTES + Integer.BYTES, Ints.fromBytes(bytes[9], bytes[10], bytes[11], bytes[12]))
);
case 2 -> {
var dis = new DataInputStream(new ByteArrayInputStream(bytes, 1 + Long.BYTES, bytes.length - (1 + Long.BYTES)));
try {
var pathName = dis.readUTF();
var isBot = dis.readBoolean();
String token;
Long phoneNumber;
if (isBot) {
token = dis.readUTF();
phoneNumber = null;
} else {
token = null;
phoneNumber = dis.readLong();
}
yield new LoadSessionFromDiskRequest(userId, pathName, token, phoneNumber);
} catch (IOException e) {
throw new SerializationException(e);
}
}
default -> throw new IllegalStateException("Unexpected value: " + type);
};
}
final record CreateUserSessionRequest(long userId, long phoneNumber) implements CreateSessionRequest {}
final record CreateBotSessionRequest(long userId, String token) implements CreateSessionRequest {}
final record LoadSessionFromDiskRequest(long userId, String pathName, String token, Long phoneNumber) implements
CreateSessionRequest {
public LoadSessionFromDiskRequest {
if ((token == null) == (phoneNumber == null)) {
throw new IllegalArgumentException("This must be either a bot or an user");
}
}
}
}

View File

@ -0,0 +1,11 @@
package it.tdlight.reactiveapi;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
public record CreateSessionResponse(long sessionId) {
public static byte[] serializeBytes(CreateSessionResponse createSessionResponse) {
return Longs.toByteArray(createSessionResponse.sessionId);
}
}

View File

@ -0,0 +1,33 @@
package it.tdlight.reactiveapi;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.jetbrains.annotations.Nullable;
@JsonInclude(Include.NON_NULL)
public class DiskSession {
public long userId;
@Nullable
public String token;
@Nullable
public Long phoneNumber;
@JsonCreator
public DiskSession(@JsonProperty(required = true, value = "userId") long userId,
@JsonProperty("token") @Nullable String token,
@JsonProperty("phoneNumber") @Nullable Long phoneNumber) {
this.userId = userId;
this.token = token;
this.phoneNumber = phoneNumber;
this.validate();
}
public void validate() {
if ((token == null) == (phoneNumber == null)) {
throw new UnsupportedOperationException("You must set either a token or a phone number");
}
}
}

View File

@ -0,0 +1,26 @@
package it.tdlight.reactiveapi;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
public class DiskSessions {
@NotNull
public String path;
/**
* key: session folder name
*/
@NotNull
public Map<String, DiskSession> sessions;
@JsonCreator
public DiskSessions(@JsonProperty(required = true, value = "path") @NotNull String path,
@JsonProperty(required = true, value = "sessions") @NotNull Map<String, DiskSession> sessions) {
this.path = path;
this.sessions = sessions;
}
}

View File

@ -0,0 +1,27 @@
package it.tdlight.reactiveapi;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
public class DiskSessionsManager {
private final ObjectMapper mapper;
private final DiskSessions diskSessionsSettings;
private final File file;
public DiskSessionsManager(ObjectMapper mapper, String diskSessionsConfigPath) throws IOException {
this.mapper = mapper;
this.file = Paths.get(diskSessionsConfigPath).toFile();
diskSessionsSettings = mapper.readValue(file, DiskSessions.class);
}
public DiskSessions getSettings() {
return diskSessionsSettings;
}
public synchronized void save() throws IOException {
mapper.writeValue(file, diskSessionsSettings);
}
}

View File

@ -0,0 +1,131 @@
package it.tdlight.reactiveapi;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.atomix.cluster.Node;
import io.atomix.cluster.discovery.BootstrapDiscoveryProvider;
import io.atomix.core.Atomix;
import io.atomix.core.AtomixBuilder;
import io.atomix.core.profile.Profile;
import io.atomix.protocols.backup.partition.PrimaryBackupPartitionGroup;
import io.atomix.protocols.raft.partition.RaftPartitionGroup;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Entrypoint {
private static final Logger LOG = LoggerFactory.getLogger(Entrypoint.class);
public static record ValidEntrypointArgs(String clusterPath, String instancePath, String diskSessionsPath) {}
public static ValidEntrypointArgs parseArguments(String[] args) {
// Check arguments validity
if (args.length != 3
|| args[0].isBlank()
|| args[1].isBlank()
|| args[2].isBlank()
|| !Files.isRegularFile(Paths.get(args[0]))
|| !Files.isRegularFile(Paths.get(args[1]))
|| !Files.isRegularFile(Paths.get(args[2]))) {
System.err.println("Syntax: executable <path/to/cluster.yaml> <path/to/instance.yaml> <path/to/disk-sessions.yaml");
System.exit(1);
}
return new ValidEntrypointArgs(args[0], args[1], args[2]);
}
public static ReactiveApi start(ValidEntrypointArgs args, AtomixBuilder atomixBuilder) throws IOException {
// Read settings
ClusterSettings clusterSettings;
InstanceSettings instanceSettings;
DiskSessionsManager diskSessions;
{
var mapper = new ObjectMapper(new YAMLFactory());
mapper.findAndRegisterModules();
String clusterConfigPath = args.clusterPath;
String instanceConfigPath = args.instancePath;
String diskSessionsConfigPath = args.diskSessionsPath;
clusterSettings = mapper.readValue(Paths.get(clusterConfigPath).toFile(), ClusterSettings.class);
instanceSettings = mapper.readValue(Paths.get(instanceConfigPath).toFile(), InstanceSettings.class);
diskSessions = new DiskSessionsManager(mapper, diskSessionsConfigPath);
}
atomixBuilder.withClusterId(clusterSettings.id);
if (instanceSettings.client) {
atomixBuilder.withMemberId(instanceSettings.id).withAddress(instanceSettings.clientAddress);
} else {
// Find node settings
var nodeSettingsOptional = clusterSettings.nodes
.stream()
.filter(node -> node.id.equals(instanceSettings.id))
.findAny();
// Check node settings presence
if (nodeSettingsOptional.isEmpty()) {
System.err.printf("Node id \"%s\" has not been described in cluster.yaml nodes list%n", instanceSettings.id);
System.exit(2);
}
var nodeSettings = nodeSettingsOptional.get();
atomixBuilder.withMemberId(instanceSettings.id).withAddress(nodeSettings.address);
}
var bootstrapDiscoveryProviderNodes = new ArrayList<Node>();
List<String> systemPartitionGroupMembers = new ArrayList<>();
for (NodeSettings node : clusterSettings.nodes) {
bootstrapDiscoveryProviderNodes.add(Node.builder().withId(node.id).withAddress(node.address).build());
systemPartitionGroupMembers.add(node.id);
}
var bootstrapDiscoveryProviderBuilder = BootstrapDiscoveryProvider.builder();
bootstrapDiscoveryProviderBuilder.withNodes(bootstrapDiscoveryProviderNodes).build();
atomixBuilder.withMembershipProvider(bootstrapDiscoveryProviderBuilder.build());
atomixBuilder.withManagementGroup(RaftPartitionGroup
.builder("system")
.withNumPartitions(1)
.withMembers(systemPartitionGroupMembers)
.build());
atomixBuilder.withPartitionGroups(PrimaryBackupPartitionGroup.builder("data").withNumPartitions(32).build());
atomixBuilder.withShutdownHook(false);
atomixBuilder.withTypeRegistrationRequired();
if (instanceSettings.client) {
atomixBuilder.addProfile(Profile.consensus(systemPartitionGroupMembers));
atomixBuilder.addProfile(Profile.dataGrid(32));
} else {
atomixBuilder.addProfile(Profile.client());
}
atomixBuilder.withCompatibleSerialization(false);
var atomix = atomixBuilder.build();
TdSerializer.register(atomix.getSerializationService());
atomix.start().join();
var api = new ReactiveApi(atomix, diskSessions);
LOG.info("Starting ReactiveApi...");
api.start();
return api;
}
public static void main(String[] args) throws IOException {
var validArgs = parseArguments(args);
var atomixBuilder = Atomix.builder().withShutdownHookEnabled();
start(validArgs, atomixBuilder);
}
}

View File

@ -0,0 +1,47 @@
package it.tdlight.reactiveapi;
import it.tdlight.jni.TdApi;
import it.tdlight.reactiveapi.Event.AuthenticatedEvent;
/**
* Any event received from a session
*/
public sealed interface Event permits AuthenticatedEvent {
/**
*
* @return temporary unique identifier of the session
*/
long sessionId();
/**
* Event received after choosing the user id of the session
*/
sealed interface AuthenticatedEvent extends Event permits OnLoginCodeRequested, OnUpdate {
/**
*
* @return telegram user id of the session
*/
long userId();
}
/**
* TDLib is asking for an authorization code
*/
sealed interface OnLoginCodeRequested extends AuthenticatedEvent
permits OnBotLoginCodeRequested, OnUserLoginCodeRequested {}
final record OnUserLoginCodeRequested(long sessionId, long userId, long phoneNumber) implements OnLoginCodeRequested {}
final record OnBotLoginCodeRequested(long sessionId, long userId, String token) implements OnLoginCodeRequested {}
/**
* Event received from TDLib
*/
sealed interface OnUpdate extends AuthenticatedEvent permits OnUpdateData, OnUpdateError {}
final record OnUpdateData(long sessionId, long userId, TdApi.Update update) implements OnUpdate {}
final record OnUpdateError(long sessionId, long userId, TdApi.Error error) implements OnUpdate {}
}

View File

@ -0,0 +1,33 @@
package it.tdlight.reactiveapi;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class InstanceSettings {
@NotNull
public String id;
/**
* True if this is just a client, false if this is a complete node
* <p>
* A client is a lightweight node
*/
public boolean client;
/**
* If {@link #client} is true, this will be the address of this client
*/
public @Nullable String clientAddress;
@JsonCreator
public InstanceSettings(@JsonProperty(required = true, value = "id") @NotNull String id,
@JsonProperty(required = true, value = "client") boolean client,
@JsonProperty("clientAddress") @Nullable String clientAddress) {
this.id = id;
this.client = client;
this.clientAddress = clientAddress;
}
}

View File

@ -0,0 +1,18 @@
package it.tdlight.reactiveapi;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import org.jetbrains.annotations.NotNull;
public class NodeSettings {
public @NotNull String id;
public @NotNull String address;
public NodeSettings(@JsonProperty(required = true, value = "id") @NotNull String id,
@JsonProperty(required = true, value = "address") @NotNull String address) {
this.id = id;
this.address = address;
}
}

View File

@ -0,0 +1,199 @@
package it.tdlight.reactiveapi;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.concurrent.CompletableFuture.failedFuture;
import io.atomix.core.Atomix;
import io.atomix.core.idgenerator.AsyncAtomicIdGenerator;
import io.atomix.core.lock.AsyncAtomicLock;
import io.atomix.core.map.AsyncAtomicMap;
import it.tdlight.reactiveapi.CreateSessionRequest.CreateBotSessionRequest;
import it.tdlight.reactiveapi.CreateSessionRequest.CreateUserSessionRequest;
import it.tdlight.reactiveapi.CreateSessionRequest.LoadSessionFromDiskRequest;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.scheduler.Schedulers;
public class ReactiveApi {
private static final Logger LOG = LoggerFactory.getLogger(ReactiveApi.class);
private final Atomix atomix;
private static final SchedulerExecutor SCHEDULER_EXECUTOR = new SchedulerExecutor(Schedulers.parallel());
private static final SchedulerExecutor BOUNDED_ELASTIC_EXECUTOR = new SchedulerExecutor(Schedulers.boundedElastic());
private final AsyncAtomicIdGenerator nextSessionId;
private final AsyncAtomicLock sessionModificationLock;
private final AsyncAtomicMap<Long, Long> sessionIdToUserId;
private final ConcurrentMap<Long, ReactiveApiPublisher> localNodeSessions = new ConcurrentHashMap<>();
private final DiskSessionsManager diskSessions;
public ReactiveApi(Atomix atomix, DiskSessionsManager diskSessions) {
this.atomix = atomix;
this.nextSessionId = atomix.getAtomicIdGenerator("session-id").async();
this.sessionIdToUserId = atomix.<Long, Long>getAtomicMap("session-id-to-user-id").async();
this.sessionModificationLock = atomix.getAtomicLock("session-modification").async();
this.diskSessions = diskSessions;
}
public void start() {
CompletableFuture.runAsync(() -> {
List<CompletableFuture<CreateSessionResponse>> requests = new ArrayList<>();
synchronized (diskSessions) {
for (Entry<String, DiskSession> entry : diskSessions.getSettings().sessions.entrySet()) {
try {
entry.getValue().validate();
} catch (Throwable ex) {
LOG.error("Failed to load disk session {}", entry.getKey(), ex);
}
var sessionFolderName = entry.getKey();
var diskSession = entry.getValue();
requests.add(createSession(new LoadSessionFromDiskRequest(diskSession.userId,
sessionFolderName,
diskSession.token,
diskSession.phoneNumber
)));
}
}
CompletableFuture
.allOf(requests.toArray(CompletableFuture<?>[]::new))
.thenAccept(responses -> LOG.info("Loaded all saved sessions from disk"));
}, BOUNDED_ELASTIC_EXECUTOR);
// Listen for create-session signals
atomix.getEventService().subscribe("create-session", CreateSessionRequest::deserializeBytes, req -> {
if (req instanceof LoadSessionFromDiskRequest) {
return failedFuture(new IllegalArgumentException("Can't pass a local request through the cluster"));
} else {
return createSession(req);
}
}, CreateSessionResponse::serializeBytes);
}
public CompletableFuture<CreateSessionResponse> createSession(CreateSessionRequest req) {
// Lock sessions creation
return sessionModificationLock.lock().thenCompose(lockVersion -> {
// Generate session id
return this.nextFreeId().thenCompose(sessionId -> {
// Create the session instance
ReactiveApiPublisher reactiveApiPublisher;
boolean loadedFromDisk;
long userId;
String botToken;
Long phoneNumber;
if (req instanceof CreateBotSessionRequest createBotSessionRequest) {
loadedFromDisk = false;
userId = createBotSessionRequest.userId();
botToken = createBotSessionRequest.token();
phoneNumber = null;
reactiveApiPublisher = ReactiveApiPublisher.fromToken(atomix, sessionId, userId, botToken);
} else if (req instanceof CreateUserSessionRequest createUserSessionRequest) {
loadedFromDisk = false;
userId = createUserSessionRequest.userId();
botToken = null;
phoneNumber = createUserSessionRequest.phoneNumber();
reactiveApiPublisher = ReactiveApiPublisher.fromPhoneNumber(atomix, sessionId, userId, phoneNumber);
} else if (req instanceof LoadSessionFromDiskRequest loadSessionFromDiskRequest) {
loadedFromDisk = true;
userId = loadSessionFromDiskRequest.userId();
botToken = loadSessionFromDiskRequest.token();
phoneNumber = loadSessionFromDiskRequest.phoneNumber();
if (loadSessionFromDiskRequest.phoneNumber() != null) {
reactiveApiPublisher = ReactiveApiPublisher.fromPhoneNumber(atomix, sessionId, userId, phoneNumber);
} else {
reactiveApiPublisher = ReactiveApiPublisher.fromToken(atomix, sessionId, userId, botToken);
}
} else {
return failedFuture(new UnsupportedOperationException("Unexpected value: " + req));
}
// Register the session instance to the local nodes map
var prev = localNodeSessions.put(sessionId, reactiveApiPublisher);
if (prev != null) {
LOG.error("Session id \"{}\" was already registered locally!", sessionId);
}
// Register the session instance to the distributed nodes map
return sessionIdToUserId.put(sessionId, req.userId()).thenComposeAsync(prevDistributed -> {
if (prevDistributed != null) {
LOG.error("Session id \"{}\" was already registered in the cluster!", sessionId);
}
CompletableFuture<?> saveToDiskFuture;
if (!loadedFromDisk) {
// Load existing session paths
HashSet<String> alreadyExistingPaths = new HashSet<>();
synchronized (diskSessions) {
for (var entry : diskSessions.getSettings().sessions.entrySet()) {
var path = entry.getKey();
var diskSessionSettings = entry.getValue();
if (diskSessionSettings.userId == userId) {
LOG.warn("User id \"{}\" session already exists in path: \"{}\"", userId, path);
}
alreadyExistingPaths.add(entry.getKey());
}
}
// Get a new disk session folder name
String diskSessionFolderName;
do {
diskSessionFolderName = UUID.randomUUID().toString();
} while (alreadyExistingPaths.contains(diskSessionFolderName));
// Create the disk session configuration
var diskSession = new DiskSession(userId, botToken, phoneNumber);
Path path;
synchronized (diskSessions) {
diskSessions.getSettings().sessions.put(diskSessionFolderName, diskSession);
path = Paths.get(diskSessions.getSettings().path).resolve(diskSessionFolderName);
}
// Start the session instance
reactiveApiPublisher.start(path);
saveToDiskFuture = CompletableFuture.runAsync(() -> {
// Save updated sessions configuration to disk
try {
synchronized (diskSessions) {
diskSessions.save();
}
} catch (IOException e) {
throw new CompletionException("Failed to save disk sessions configuration", e);
}
}, BOUNDED_ELASTIC_EXECUTOR);
} else {
saveToDiskFuture = completedFuture(null);
}
return saveToDiskFuture.thenApply(ignored -> new CreateSessionResponse(sessionId));
}, BOUNDED_ELASTIC_EXECUTOR);
});
});
}
public CompletableFuture<Long> nextFreeId() {
return nextSessionId.nextId().thenCompose(id -> sessionIdToUserId.containsKey(id).thenCompose(exists -> {
if (exists) {
return nextFreeId();
} else {
return completedFuture(id);
}
}));
}
public Atomix getAtomix() {
return atomix;
}
}

View File

@ -0,0 +1,44 @@
package it.tdlight.reactiveapi;
import io.atomix.core.Atomix;
import it.tdlight.jni.TdApi;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.Executors;
import org.apache.commons.lang3.SerializationException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink.OverflowStrategy;
import reactor.core.scheduler.Schedulers;
public class ReactiveApiPublisher {
private static final SchedulerExecutor SCHEDULER_EXECUTOR = new SchedulerExecutor(Schedulers.boundedElastic());
private final Atomix atomix;
private final long userId;
private final long sessionId;
private final String botToken;
private final Long phoneNumber;
private ReactiveApiPublisher(Atomix atomix, long sessionId, long userId, String botToken, Long phoneNumber) {
this.atomix = atomix;
this.userId = userId;
this.sessionId = sessionId;
this.botToken = botToken;
this.phoneNumber = phoneNumber;
}
public static ReactiveApiPublisher fromToken(Atomix atomix, Long sessionId, long userId, String token) {
return new ReactiveApiPublisher(atomix, sessionId, userId, token, null);
}
public static ReactiveApiPublisher fromPhoneNumber(Atomix atomix, Long sessionId, long userId, long phoneNumber) {
return new ReactiveApiPublisher(atomix, sessionId, userId, null, phoneNumber);
}
public void start(Path path) {
}
}

View File

@ -0,0 +1,5 @@
package it.tdlight.reactiveapi;
public class ReactiveApiSubscriber {
}

View File

@ -0,0 +1,8 @@
package it.tdlight.reactiveapi;
import it.tdlight.jni.TdApi;
/**
* {@link #sessionUuid} changes every time a session is restarted
*/
public record ReactiveApiUpdate(long sessionUuid, TdApi.Object update) {}

View File

@ -0,0 +1,19 @@
package it.tdlight.reactiveapi;
import java.util.concurrent.Executor;
import org.jetbrains.annotations.NotNull;
import reactor.core.scheduler.Scheduler;
public class SchedulerExecutor implements Executor {
private final Scheduler scheduler;
public SchedulerExecutor(Scheduler scheduler) {
this.scheduler = scheduler;
}
@Override
public void execute(@NotNull Runnable command) {
scheduler.schedule(command);
}
}

View File

@ -0,0 +1,63 @@
package it.tdlight.reactiveapi;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.Serializer;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.KryoDataInput;
import com.esotericsoftware.kryo.io.KryoDataOutput;
import com.esotericsoftware.kryo.io.Output;
import io.atomix.primitive.serialization.SerializationService;
import it.tdlight.common.ConstructorDetector;
import it.tdlight.jni.TdApi;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.stream.Stream;
import org.apache.commons.lang3.SerializationException;
public class TdSerializer extends Serializer<TdApi.Object> {
private TdSerializer() {
}
public static void register(SerializationService serializationService) {
var serializerBuilder = serializationService.newBuilder("TdApi");
var tdApiClasses = TdApi.class.getDeclaredClasses();
// Add types
Class<?>[] classes = Stream
.of(tdApiClasses)
.filter(clazz -> clazz.isAssignableFrom(TdApi.Object.class))
.toArray(Class<?>[]::new);
serializerBuilder.addSerializer(new TdSerializer(), classes);
}
@Override
public void write(Kryo kryo, Output output, TdApi.Object object) {
try {
object.serialize(new KryoDataOutput(output));
} catch (IOException e) {
throw new SerializationException(e);
}
}
@Override
public TdApi.Object read(Kryo kryo, Input input, Class<TdApi.Object> type) {
try {
return TdApi.Deserializer.deserialize(new KryoDataInput(input));
} catch (IOException e) {
throw new SerializationException(e);
}
}
public static TdApi.Object deserializeBytes(byte[] bytes) {
var din = new DataInputStream(new ByteArrayInputStream(bytes));
try {
return TdApi.Deserializer.deserialize(din);
} catch (IOException e) {
throw new SerializationException(e);
}
}
}

View File

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

View File

@ -1,82 +0,0 @@
package it.tdlight.tdlibsession;
import java.util.Objects;
import java.util.StringJoiner;
class SignalMessage<T> {
private final SignalType signalType;
private final T item;
private final String errorMessage;
private SignalMessage(SignalType signalType, T item, String errorMessage) {
this.signalType = signalType;
this.item = item;
this.errorMessage = errorMessage;
}
public static <T> SignalMessage<T> onNext(T item) {
return new SignalMessage<>(SignalType.ITEM, Objects.requireNonNull(item), null);
}
public static <T> SignalMessage<T> onError(Throwable throwable) {
return new SignalMessage<T>(SignalType.ERROR, null, Objects.requireNonNull(throwable.getMessage()));
}
static <T> SignalMessage<T> onDecodedError(String throwable) {
return new SignalMessage<T>(SignalType.ERROR, null, Objects.requireNonNull(throwable));
}
public static <T> SignalMessage<T> onComplete() {
return new SignalMessage<T>(SignalType.COMPLETE, null, null);
}
public SignalType getSignalType() {
return signalType;
}
public String getErrorMessage() {
return Objects.requireNonNull(errorMessage);
}
public T getItem() {
return Objects.requireNonNull(item);
}
@Override
public String toString() {
return new StringJoiner(", ", SignalMessage.class.getSimpleName() + "[", "]")
.add("signalType=" + signalType)
.add("item=" + item)
.add("errorMessage='" + errorMessage + "'")
.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SignalMessage<?> that = (SignalMessage<?>) o;
if (signalType != that.signalType) {
return false;
}
if (!Objects.equals(item, that.item)) {
return false;
}
return Objects.equals(errorMessage, that.errorMessage);
}
@Override
public int hashCode() {
int result = signalType != null ? signalType.hashCode() : 0;
result = 31 * result + (item != null ? item.hashCode() : 0);
result = 31 * result + (errorMessage != null ? errorMessage.hashCode() : 0);
return result;
}
}

View File

@ -1,89 +0,0 @@
package it.tdlight.tdlibsession;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.MessageCodec;
import it.tdlight.utils.VertxBufferInputStream;
import it.tdlight.utils.VertxBufferOutputStream;
import java.nio.charset.StandardCharsets;
import org.warp.commonutils.stream.SafeDataInputStream;
import org.warp.commonutils.stream.SafeDataOutputStream;
public class SignalMessageCodec<T> implements MessageCodec<SignalMessage<T>, SignalMessage<T>> {
private final String codecName;
private final MessageCodec<T, T> typeCodec;
public SignalMessageCodec(MessageCodec<T, T> typeCodec) {
super();
this.codecName = "SignalCodec-" + typeCodec.name();
this.typeCodec = typeCodec;
}
@Override
public void encodeToWire(Buffer buffer, SignalMessage<T> t) {
try (var bos = new VertxBufferOutputStream(buffer)) {
try (var dos = new SafeDataOutputStream(bos)) {
switch (t.getSignalType()) {
case ITEM:
dos.writeByte(0x01);
break;
case ERROR:
dos.writeByte(0x02);
break;
case COMPLETE:
dos.writeByte(0x03);
break;
default:
throw new IllegalStateException("Unexpected value: " + t.getSignalType());
}
}
switch (t.getSignalType()) {
case ITEM:
typeCodec.encodeToWire(buffer, t.getItem());
break;
case ERROR:
var stringBytes = t.getErrorMessage().getBytes(StandardCharsets.UTF_8);
buffer.appendInt(stringBytes.length);
buffer.appendBytes(stringBytes);
break;
}
}
}
@Override
public SignalMessage<T> decodeFromWire(int pos, Buffer buffer) {
try (var fis = new VertxBufferInputStream(buffer, pos)) {
try (var dis = new SafeDataInputStream(fis)) {
switch (dis.readByte()) {
case 0x01:
return SignalMessage.onNext(typeCodec.decodeFromWire(pos + 1, buffer));
case 0x02:
var size = dis.readInt();
return SignalMessage.onDecodedError(new String(dis.readNBytes(size), StandardCharsets.UTF_8));
case 0x03:
return SignalMessage.onComplete();
default:
throw new IllegalStateException("Unexpected value: " + dis.readByte());
}
}
}
}
@Override
public SignalMessage<T> transform(SignalMessage<T> t) {
// If a message is sent *locally* across the event bus.
// This sends message just as is
return t;
}
@Override
public String name() {
return codecName;
}
@Override
public byte systemCodecID() {
// Always -1
return -1;
}
}

View File

@ -1,5 +0,0 @@
package it.tdlight.tdlibsession;
enum SignalType {
COMPLETE, ERROR, ITEM
}

View File

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

View File

@ -1,3 +0,0 @@
package it.tdlight.tdlibsession.remoteclient;
public class BinlogManager {}

View File

@ -1,7 +0,0 @@
package it.tdlight.tdlibsession.remoteclient;
public enum DeployClientResult {
DEPLOYED,
IGNORED,
FAILED
}

View File

@ -1,55 +0,0 @@
package it.tdlight.tdlibsession.remoteclient;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.stream.Collectors;
public class RemoteClientBotAddresses {
private final LinkedHashSet<String> addresses;
private final LinkedHashSet<String> tempAddresses;
private final Path addressesFilePath;
public RemoteClientBotAddresses(Path addressesFilePath) throws IOException {
this.addressesFilePath = addressesFilePath;
if (Files.notExists(addressesFilePath)) {
Files.createFile(addressesFilePath);
}
tempAddresses = new LinkedHashSet<>();
addresses = Files
.readAllLines(addressesFilePath, StandardCharsets.UTF_8)
.stream()
.filter(address -> !address.isBlank())
.collect(Collectors.toCollection(LinkedHashSet::new));
}
public synchronized void putAddress(String address) throws IOException {
tempAddresses.remove(address);
addresses.add(address);
Files.write(addressesFilePath, addresses, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.SYNC);
}
public synchronized void putTempAddress(String address) {
tempAddresses.add(address);
}
public synchronized void removeAddress(String address) throws IOException {
tempAddresses.remove(address);
addresses.remove(address);
Files.write(addressesFilePath, addresses, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.SYNC);
}
public synchronized boolean has(String botAddress) {
return addresses.contains(botAddress) || tempAddresses.contains(botAddress);
}
public synchronized Set<String> values() {
return new HashSet<>(addresses);
}
}

View File

@ -1,76 +0,0 @@
package it.tdlight.tdlibsession.remoteclient;
import io.vertx.core.file.FileSystemException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.NoSuchElementException;
import java.util.StringJoiner;
public class SecurityInfo {
private final Path keyStorePath;
private final Path keyStorePasswordPath;
private final Path trustStorePath;
private final Path trustStorePasswordPath;
public SecurityInfo(Path keyStorePath, Path keyStorePasswordPath, Path trustStorePath, Path trustStorePasswordPath) {
this.keyStorePath = keyStorePath;
this.keyStorePasswordPath = keyStorePasswordPath;
this.trustStorePath = trustStorePath;
this.trustStorePasswordPath = trustStorePasswordPath;
}
public Path getKeyStorePath() {
return keyStorePath;
}
public Path getKeyStorePasswordPath() {
return keyStorePasswordPath;
}
public String getKeyStorePassword(boolean required) {
try {
if (Files.isReadable(keyStorePasswordPath) && Files.size(keyStorePasswordPath) >= 6) {
return Files.readString(keyStorePasswordPath, StandardCharsets.UTF_8).split("\n")[0];
} else if (required) {
throw new NoSuchElementException("No keystore password is set on '" + keyStorePasswordPath.toString() + "'");
}
} catch (IOException ex) {
throw new FileSystemException(ex);
}
return null;
}
public Path getTrustStorePath() {
return trustStorePath;
}
public Path getTrustStorePasswordPath() {
return trustStorePasswordPath;
}
public String getTrustStorePassword(boolean required) {
try {
if (Files.isReadable(trustStorePasswordPath) && Files.size(trustStorePasswordPath) >= 6) {
return Files.readString(trustStorePasswordPath, StandardCharsets.UTF_8).split("\n")[0];
} else if (required) {
throw new NoSuchElementException("No truststore password is set on '" + trustStorePasswordPath.toString() + "'");
}
} catch (IOException ex) {
throw new FileSystemException(ex);
}
return null;
}
@Override
public String toString() {
return new StringJoiner(", ", SecurityInfo.class.getSimpleName() + "[", "]")
.add("keyStorePath=" + keyStorePath)
.add("keyStorePasswordPath=" + keyStorePasswordPath)
.add("trustStorePath=" + trustStorePath)
.add("trustStorePasswordPath=" + trustStorePasswordPath)
.toString();
}
}

View File

@ -1,233 +0,0 @@
package it.tdlight.tdlibsession.remoteclient;
import com.akaita.java.rxjava2debug.RxJava2Debug;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.JksOptions;
import io.vertx.reactivex.core.eventbus.Message;
import io.vertx.reactivex.core.eventbus.MessageConsumer;
import it.tdlight.common.Init;
import it.tdlight.common.Log;
import it.tdlight.common.utils.CantLoadLibrary;
import it.tdlight.tdlibsession.td.middle.StartSessionMessage;
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
import it.tdlight.tdlibsession.td.middle.server.AsyncTdMiddleEventBusServer;
import it.tdlight.tdnative.NativeLog;
import it.tdlight.utils.BinlogUtils;
import it.tdlight.utils.MonoUtils;
import java.net.URISyntaxException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.logging.log4j.LogManager;
import org.jetbrains.annotations.Nullable;
import org.warp.commonutils.log.Logger;
import org.warp.commonutils.log.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Sinks.One;
import reactor.core.scheduler.Schedulers;
import reactor.tools.agent.ReactorDebugAgent;
public class TDLibRemoteClient implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(TDLibRemoteClient.class);
@Nullable
private final SecurityInfo securityInfo;
private final String masterHostname;
private final String netInterface;
private final int port;
private final Set<String> membersAddresses;
private final AtomicReference<TdClusterManager> clusterManager = new AtomicReference<>();
public TDLibRemoteClient(@Nullable SecurityInfo securityInfo,
String masterHostname,
String netInterface,
int port,
Set<String> membersAddresses,
boolean enableAsyncStacktraces,
boolean enableFullAsyncStacktraces) {
this.securityInfo = securityInfo;
this.masterHostname = masterHostname;
this.netInterface = netInterface;
this.port = port;
this.membersAddresses = membersAddresses;
if (enableAsyncStacktraces && enableFullAsyncStacktraces) {
RxJava2Debug.enableRxJava2AssemblyTracking(new String[]{"it.tdlight.utils", "it.tdlight.tdlibsession"});
}
try {
Init.start();
//noinspection deprecation
Log.setVerbosityLevel(2);
} catch (CantLoadLibrary ex) {
throw new RuntimeException(ex);
}
}
public static void main(String[] args) throws URISyntaxException {
if (args.length < 1) {
return;
}
String masterHostname = args[0];
String[] interfaceAndPort = args[1].split(":", 2);
String netInterface = interfaceAndPort[0];
int port = Integer.parseInt(interfaceAndPort[1]);
Set<String> membersAddresses = Set.of(args[2].split(","));
Path keyStorePath = Paths.get(args[3]);
Path keyStorePasswordPath = Paths.get(args[4]);
Path trustStorePath = Paths.get(args[5]);
Path trustStorePasswordPath = Paths.get(args[6]);
boolean enableAsyncStacktraces = Boolean.parseBoolean(args[7]);
boolean enableFullAsyncStacktraces = Boolean.parseBoolean(args[8]);
var loggerContext = (org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false);
loggerContext.setConfigLocation(Objects
.requireNonNull(TDLibRemoteClient.class.getResource("/tdlib-session-container-log4j2.xml"),
"tdlib-session-container-log4j2.xml doesn't exist")
.toURI());
var securityInfo = new SecurityInfo(keyStorePath, keyStorePasswordPath, trustStorePath, trustStorePasswordPath);
var client = new TDLibRemoteClient(securityInfo,
masterHostname,
netInterface,
port,
membersAddresses,
enableAsyncStacktraces,
enableFullAsyncStacktraces
);
client
.start()
.block();
// Close vert.x on shutdown
var vertxMono = Mono.fromCallable(() -> client.clusterManager.get().getVertx());
Runtime
.getRuntime()
.addShutdownHook(new Thread(() -> vertxMono
.flatMap(vertx -> MonoUtils.toMono(vertx.rxClose()))
.blockOptional())
);
}
public Mono<Void> start() {
var ksp = securityInfo == null ? null : securityInfo.getKeyStorePassword(false);
var keyStoreOptions = securityInfo == null || ksp == null ? null : new JksOptions()
.setPath(securityInfo.getKeyStorePath().toAbsolutePath().toString())
.setPassword(ksp);
var tsp = securityInfo == null ? null : securityInfo.getTrustStorePassword(false);
var trustStoreOptions = securityInfo == null || tsp == null ? null : new JksOptions()
.setPath(securityInfo.getTrustStorePath().toAbsolutePath().toString())
.setPassword(tsp);
return MonoUtils
.fromBlockingEmpty(() -> {
// Set verbosity level here, before creating the bots
if (Files.notExists(Paths.get("logs"))) {
try {
Files.createDirectory(Paths.get("logs"));
} catch (FileAlreadyExistsException ignored) {
}
}
logger.info(
"TDLib remote client is being hosted on" + netInterface + ":" + port + ". Master: " + masterHostname);
logger.info(
"TDLib remote client SSL enabled: " + (keyStoreOptions != null && trustStoreOptions != null));
})
.then(TdClusterManager.ofNodes(keyStoreOptions,
trustStoreOptions,
false,
masterHostname,
netInterface,
port,
membersAddresses
))
.doOnSuccess(clusterManager::set)
.single()
.doOnError(ex -> logger.error("Failed to set cluster manager", ex))
.flatMap(clusterManager -> {
MessageConsumer<StartSessionMessage> startBotConsumer
= clusterManager.getEventBus().consumer("bots.start-bot");
return this.listenForStartBotsCommand(
clusterManager,
MonoUtils.fromReplyableMessageConsumer(Mono.empty(), startBotConsumer)
);
})
.then();
}
private Mono<Void> listenForStartBotsCommand(TdClusterManager clusterManager,
Flux<Message<StartSessionMessage>> messages) {
return MonoUtils
.fromBlockingEmpty(() -> messages
.flatMapSequential(msg -> {
StartSessionMessage req = msg.body();
DeploymentOptions deploymentOptions = clusterManager
.newDeploymentOpts()
.setConfig(new JsonObject()
.put("botId", req.id())
.put("botAlias", req.alias())
.put("local", false)
.put("implementationDetails", req.implementationDetails()));
var verticle = new AsyncTdMiddleEventBusServer();
// Binlog path
var sessPath = getSessionDirectory(req.id());
var mediaPath = getMediaDirectory(req.id());
var blPath = getSessionBinlogDirectory(req.id());
return BinlogUtils
.chooseBinlog(clusterManager.getVertx().fileSystem(), blPath, req.binlog(), req.binlogDate())
.then(BinlogUtils.cleanSessionPath(clusterManager.getVertx().fileSystem(), blPath, sessPath, mediaPath))
.then(clusterManager.getVertx().rxDeployVerticle(verticle, deploymentOptions).as(MonoUtils::toMono))
.then(MonoUtils.fromBlockingEmpty(() -> msg.reply(new byte[0])))
.onErrorResume(ex -> {
msg.fail(500, "Failed to deploy bot verticle: " + ex.getMessage());
logger.error("Failed to deploy bot verticle", ex);
return Mono.empty();
});
})
.subscribeOn(Schedulers.parallel())
.subscribe(
v -> {},
ex -> logger.error("Bots starter activity crashed. From now on, no new bots can be started anymore", ex)
)
);
}
public static Path getSessionDirectory(long botId) {
return Paths.get(".sessions-cache").resolve("id" + botId);
}
public static Path getMediaDirectory(long botId) {
return Paths.get(".cache").resolve("media").resolve("id" + botId);
}
public static Path getSessionBinlogDirectory(long botId) {
return getSessionDirectory(botId).resolve("td.binlog");
}
@Override
public void close() {
this.clusterManager.get().getVertx().rxClose().blockingAwait();
}
}

View File

@ -1,18 +0,0 @@
package it.tdlight.tdlibsession.td;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Object;
import java.time.Duration;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ReactorTelegramClient {
Mono<Void> initialize();
Flux<Object> receive();
<T extends TdApi.Object> Mono<TdApi.Object> send(TdApi.Function<T> query, Duration timeout);
<T extends TdApi.Object> TdApi.Object execute(TdApi.Function<T> query);
}

View File

@ -1,148 +0,0 @@
package it.tdlight.tdlibsession.td;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Function;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ResponseError extends IOException {
@NotNull
private final String botName;
@NotNull
private final String tag;
private final int code;
@NotNull
private final String message;
private ResponseError(@NotNull Function<?> function, @NotNull String botName, @NotNull TdApi.Error tdError, @Nullable Throwable cause) {
super("Bot '" + botName + "' failed the request '" + functionToInlineString(function) + "': " + tdError.code + " " + tdError.message, cause);
this.botName = botName;
this.tag = functionToInlineString(function);
this.code = tdError.code;
this.message = tdError.message;
}
private ResponseError(@NotNull String tag, @NotNull String botName, @NotNull TdApi.Error tdError, @Nullable Throwable cause) {
super("Bot '" + botName + "' failed the request '" + tag + "': " + tdError.code + " " + tdError.message, cause);
this.botName = botName;
this.tag = tag;
this.code = tdError.code;
this.message = tdError.message;
}
private ResponseError(@NotNull Function<?> function, @NotNull String botName, @Nullable Throwable cause) {
super("Bot '" + botName + "' failed the request '" + functionToInlineString(function) + "': " + (cause == null ? null : cause.getMessage()), cause);
this.botName = botName;
this.tag = functionToInlineString(function);
this.code = 500;
this.message = (cause == null ? "" : (cause.getMessage() == null ? "" : cause.getMessage()));
}
private ResponseError(@NotNull String tag, @NotNull String botName, @Nullable Throwable cause) {
super("Bot '" + botName + "' failed the request '" + tag + "': " + (cause == null ? null : cause.getMessage()), cause);
this.botName = botName;
this.tag = tag;
this.code = 500;
this.message = (cause == null ? "" : (cause.getMessage() == null ? "" : cause.getMessage()));
}
public static ResponseError newResponseError(@NotNull Function<?> function,
@NotNull String botName,
@NotNull TdApi.Error tdError,
@Nullable Throwable cause) {
return new ResponseError(function, botName, tdError, cause);
}
public static ResponseError newResponseError(@NotNull String tag,
@NotNull String botName,
@NotNull TdApi.Error tdError,
@Nullable Throwable cause) {
return new ResponseError(tag, botName, tdError, cause);
}
public static ResponseError newResponseError(@NotNull String tag,
@NotNull String botName,
@NotNull TdApi.Error tdError,
@Nullable TdError cause) {
return new ResponseError(tag, botName, tdError, cause);
}
public static ResponseError newResponseError(@NotNull Function<?> function,
@NotNull String botName,
@Nullable Throwable cause) {
return new ResponseError(function, botName, cause);
}
public static ResponseError newResponseError(@NotNull String tag,
@NotNull String botName,
@Nullable Throwable cause) {
return new ResponseError(tag, botName, cause);
}
@Nullable
public static <T> T get(@NotNull Function<?> function, @NotNull String botName, CompletableFuture<T> action) throws ResponseError {
try {
return action.get();
} catch (InterruptedException e) {
throw ResponseError.newResponseError(function, botName, e);
} catch (ExecutionException executionException) {
if (executionException.getCause() instanceof ResponseError) {
throw (ResponseError) executionException.getCause();
} else {
throw ResponseError.newResponseError(function, botName, executionException);
}
}
}
@Nullable
public static <T> T get(@NotNull String tag, @NotNull String botName, CompletableFuture<T> action) throws ResponseError {
try {
return action.get();
} catch (InterruptedException e) {
throw ResponseError.newResponseError(tag, botName, e);
} catch (ExecutionException executionException) {
if (executionException.getCause() instanceof ResponseError) {
throw (ResponseError) executionException.getCause();
} else {
throw ResponseError.newResponseError(tag, botName, executionException);
}
}
}
@NotNull
public String getBotName() {
return botName;
}
public int getErrorCode() {
return code;
}
public String getTag() {
return tag;
}
@NotNull
public String getErrorMessage() {
return message;
}
private static String functionToInlineString(Function<?> function) {
return function
.toString()
.replace("\n", " ")
.replace("\t", "")
.replace(" ", "")
.replace(" = ", "=")
.trim();
}
@Override
public String toString() {
return getMessage();
}
}

View File

@ -1,34 +0,0 @@
package it.tdlight.tdlibsession.td;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Error;
public class TdError extends RuntimeException {
private final int code;
private final String message;
public TdError(int code, String message) {
super(code + " " + message);
this.code = code;
this.message = message;
}
public TdError(int code, String message, Throwable cause) {
super(code + " " + message, cause);
this.code = code;
this.message = message;
}
public int getTdCode() {
return code;
}
public String getTdMessage() {
return message;
}
public TdApi.Error getTdError() {
return new Error(code, message);
}
}

View File

@ -1,289 +0,0 @@
package it.tdlight.tdlibsession.td;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Error;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.concurrent.CompletionException;
import java.util.function.Function;
import org.jetbrains.annotations.NotNull;
/**
* Encapsulates the result of an asynchronous operation.
* <p>
* Many operations in Vert.x APIs provide results back by passing an instance of this in a {@link io.vertx.core.Handler}.
* <p>
* The result can either have failed or succeeded.
* <p>
* If it failed then the cause of the failure is available with {@link #cause}.
* <p>
* If it succeeded then the actual result is available with {@link #result}
*
* @author <a href="http://tfox.org">Tim Fox</a>
*/
public interface TdResult<T extends TdApi.Object> {
/**
* The result of the operation. This will be null if the operation failed.
*
* @return the result or null if the operation failed.
*/
T result();
/**
* The result of the operation. This will throw CompletionException if the operation failed.
*
* @return the result.
*/
T orElseThrow() throws CompletionException;
/**
* A TdApi.Error describing failure. This will be null if the operation succeeded.
*
* @return the cause or null if the operation succeeded.
*/
TdApi.Error cause();
/**
* Did it succeed?
*
* @return true if it succeded or false otherwise
*/
boolean succeeded();
/**
* Did it fail?
*
* @return true if it failed or false otherwise
*/
boolean failed();
/**
* Apply a {@code mapper} function on this async result.<p>
*
* The {@code mapper} is called with the completed value and this mapper returns a value. This value will complete the result returned by this method call.<p>
*
* When this async result is failed, the failure will be propagated to the returned async result and the {@code mapper} will not be called.
*
* @param mapper the mapper function
* @return the mapped async result
*/
default <U extends TdApi.Object> TdResult<U> map(Function<T, U> mapper) {
if (mapper == null) {
throw new NullPointerException();
}
return new TdResult<U>() {
@Override
public U result() {
if (succeeded()) {
return mapper.apply(TdResult.this.result());
} else {
return null;
}
}
@Override
public U orElseThrow() throws CompletionException {
if (succeeded()) {
return mapper.apply(TdResult.this.orElseThrow());
} else {
return null;
}
}
@Override
public TdApi.Error cause() {
return TdResult.this.cause();
}
@Override
public boolean succeeded() {
return TdResult.this.succeeded();
}
@Override
public boolean failed() {
return TdResult.this.failed();
}
};
}
/**
* Map the result of this async result to a specific {@code value}.<p>
*
* When this async result succeeds, this {@code value} will succeeed the async result returned by this method call.<p>
*
* When this async result fails, the failure will be propagated to the returned async result.
*
* @param value the value that eventually completes the mapped async result
* @return the mapped async result
*/
default <V extends TdApi.Object> TdResult<V> map(V value) {
return map((Function<T, V>) t -> value);
}
/**
* Map the result of this async result to {@code null}.<p>
*
* This is a convenience for {@code TdResult.map((T) null)} or {@code TdResult.map((Void) null)}.<p>
*
* When this async result succeeds, {@code null} will succeeed the async result returned by this method call.<p>
*
* When this async result fails, the failure will be propagated to the returned async result.
*
* @return the mapped async result
*/
default <V extends TdApi.Object> TdResult<V> mapEmpty() {
return map((V)null);
}
/**
* Apply a {@code mapper} function on this async result.<p>
*
* The {@code mapper} is called with the failure and this mapper returns a value. This value will complete the result returned by this method call.<p>
*
* When this async result is succeeded, the value will be propagated to the returned async result and the {@code mapper} will not be called.
*
* @param mapper the mapper function
* @return the mapped async result
*/
default TdResult<T> otherwise(Function<TdApi.Error, T> mapper) {
if (mapper == null) {
throw new NullPointerException();
}
return new TdResult<T>() {
@Override
public T result() {
if (TdResult.this.succeeded()) {
return TdResult.this.result();
} else if (TdResult.this.failed()) {
return mapper.apply(TdResult.this.cause());
} else {
return null;
}
}
@Override
public T orElseThrow() {
if (TdResult.this.succeeded()) {
return TdResult.this.orElseThrow();
} else if (TdResult.this.failed()) {
return mapper.apply(TdResult.this.cause());
} else {
return null;
}
}
@Override
public TdApi.Error cause() {
return null;
}
@Override
public boolean succeeded() {
return TdResult.this.succeeded() || TdResult.this.failed();
}
@Override
public boolean failed() {
return false;
}
};
}
/**
* Map the failure of this async result to a specific {@code value}.<p>
*
* When this async result fails, this {@code value} will succeeed the async result returned by this method call.<p>
*
* When this async succeeds, the result will be propagated to the returned async result.
*
* @param value the value that eventually completes the mapped async result
* @return the mapped async result
*/
default TdResult<T> otherwise(T value) {
return otherwise(err -> value);
}
/**
* Map the failure of this async result to {@code null}.<p>
*
* This is a convenience for {@code TdResult.otherwise((T) null)}.<p>
*
* When this async result fails, the {@code null} will succeeed the async result returned by this method call.<p>
*
* When this async succeeds, the result will be propagated to the returned async result.
*
* @return the mapped async result
*/
default TdResult<T> otherwiseEmpty() {
return otherwise(err -> null);
}
static <T extends TdApi.Object> TdResult<T> succeeded(@NotNull T value) {
Objects.requireNonNull(value);
return new TdResultImpl<T>(value, null);
}
static <T extends TdApi.Object> TdResult<T> failed(@NotNull TdApi.Error error) {
Objects.requireNonNull(error);
return new TdResultImpl<T>(null, error);
}
static <T extends TdApi.Object> TdResult<T> of(@NotNull TdApi.Object resultOrError) {
if (resultOrError.getConstructor() == TdApi.Error.CONSTRUCTOR) {
return failed((TdApi.Error) resultOrError);
} else {
//noinspection unchecked
return succeeded((T) resultOrError);
}
}
class TdResultImpl<U extends TdApi.Object> implements TdResult<U> {
private final U value;
private final Error error;
public TdResultImpl(U value, Error error) {
this.value = value;
this.error = error;
assert (value == null) != (error == null);
}
@Override
public U result() {
return value;
}
@Override
public U orElseThrow() {
if (error != null) {
throw new TdError(error.code, error.message);
}
return value;
}
@Override
public Error cause() {
return error;
}
@Override
public boolean succeeded() {
return value != null;
}
@Override
public boolean failed() {
return error != null;
}
@Override
public String toString() {
return new StringJoiner(", ", TdResultImpl.class.getSimpleName() + "[", "]")
.add("value=" + value)
.add("error=" + error)
.toString();
}
}
}

View File

@ -1,92 +0,0 @@
package it.tdlight.tdlibsession.td;
import it.tdlight.common.ReactiveTelegramClient;
import it.tdlight.common.Signal;
import it.tdlight.common.SignalListener;
import it.tdlight.common.UpdatesHandler;
import it.tdlight.jni.TdApi;
import it.tdlight.utils.MonoUtils;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicReference;
import org.jetbrains.annotations.NotNull;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink.OverflowStrategy;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
public class WrappedReactorTelegramClient implements ReactorTelegramClient {
private final ReactiveTelegramClient reactiveTelegramClient;
private final AtomicReference<Flux<Signal>> multicastSignals = new AtomicReference<>(null);
public WrappedReactorTelegramClient(ReactiveTelegramClient reactiveTelegramClient) {
this.reactiveTelegramClient = reactiveTelegramClient;
}
public Mono<Void> initialize() {
return MonoUtils
.fromBlockingEmpty(() -> {
reactiveTelegramClient.createAndRegisterClient();
Flux<Signal> signalsFlux = Flux
.<Signal>create(sink -> {
reactiveTelegramClient.setListener(sink::next);
sink.onCancel(reactiveTelegramClient::cancel);
sink.onDispose(reactiveTelegramClient::dispose);
}, OverflowStrategy.BUFFER)
.subscribeOn(Schedulers.boundedElastic())
.takeWhile(Signal::isNotClosed);
Flux<Signal> refCountedSharedSignalsFlux = signalsFlux.publish().refCount();
multicastSignals.set(refCountedSharedSignalsFlux);
});
}
@Override
public Flux<TdApi.Object> receive() {
return Flux
.defer(() -> {
Flux<Signal> flux = multicastSignals.get();
if (flux == null) {
return Flux.error(new IllegalStateException("TDLib session not started"));
} else {
return flux;
}
})
.handle((item, sink) -> {
if (item.isUpdate()) {
sink.next(item.getUpdate());
} else if (item.isException()) {
sink.error(item.getException());
} else {
sink.error(new IllegalStateException("This shouldn't happen. Received unknown ReactiveItem type"));
}
});
}
/**
* Sends a request to the TDLib.
*
* @param query Object representing a query to the TDLib.
* @param timeout Response timeout.
* @return a publisher that will emit exactly one item, or an error
* @throws NullPointerException if query is null.
*/
@Override
public <T extends TdApi.Object> Mono<TdApi.Object> send(TdApi.Function<T> query, Duration timeout) {
return Mono.from(reactiveTelegramClient.send(query, timeout)).single();
}
/**
* Synchronously executes a TDLib request. Only a few marked accordingly requests can be executed synchronously.
*
* @param query Object representing a query to the TDLib.
* @return request result or {@link TdApi.Error}.
* @throws NullPointerException if query is null.
*/
@Override
public <T extends TdApi.Object> TdApi.Object execute(TdApi.Function<T> query) {
return reactiveTelegramClient.execute(query);
}
}

View File

@ -1,32 +0,0 @@
package it.tdlight.tdlibsession.td.direct;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.tdlibsession.td.TdResult;
import java.time.Duration;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface AsyncTdDirect {
Mono<Void> initialize();
/**
* Receives incoming updates and request responses from TDLib.
* Can be called only once.
*
*/
Flux<TdApi.Object> receive(AsyncTdDirectOptions options);
/**
* Sends request to TDLib.
* Should be called after receive.
*
* @param request Request to TDLib.
* @param timeout Response timeout.
* @param synchronous Execute synchronously.
* @return The request response or {@link it.tdlight.jni.TdApi.Error}.
*/
<T extends TdApi.Object> Mono<TdResult<T>> execute(Function<T> request, Duration timeout, boolean synchronous);
}

View File

@ -1,109 +0,0 @@
package it.tdlight.tdlibsession.td.direct;
import io.vertx.core.json.JsonObject;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
import it.tdlight.jni.TdApi.Close;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.Ok;
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
import it.tdlight.tdlibsession.td.ReactorTelegramClient;
import it.tdlight.tdlibsession.td.TdError;
import it.tdlight.tdlibsession.td.TdResult;
import it.tdlight.utils.MonoUtils;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import org.warp.commonutils.log.Logger;
import org.warp.commonutils.log.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Sinks.One;
import reactor.core.scheduler.Schedulers;
public class AsyncTdDirectImpl implements AsyncTdDirect {
private static final Logger logger = LoggerFactory.getLogger(AsyncTdDirect.class);
private final TelegramClientFactory telegramClientFactory;
private final JsonObject implementationDetails;
private final String botAlias;
private final AtomicReference<ReactorTelegramClient> td = new AtomicReference<>(null);
public AsyncTdDirectImpl(TelegramClientFactory telegramClientFactory,
JsonObject implementationDetails,
String botAlias) {
this.telegramClientFactory = telegramClientFactory;
this.implementationDetails = implementationDetails;
this.botAlias = botAlias;
}
@Override
public <T extends TdApi.Object> Mono<TdResult<T>> execute(Function<T> request, Duration timeout, boolean synchronous) {
if (synchronous) {
return MonoUtils.fromBlockingSingle(() -> {
var td = this.td.get();
logger.trace("Sending execute to TDLib {}", request);
Objects.requireNonNull(td, "td is null");
TdResult<T> result = TdResult.of(td.execute(request));
logger.trace("Received execute response from TDLib. Request was {}", request);
return result;
})
.single();
} else {
return Mono.defer(() -> {
var td = this.td.get();
if (td != null) {
return Mono
.fromRunnable(() -> logger.trace("Sending request to TDLib {}", request))
.then(td.send(request, timeout))
.single()
.<TdResult<T>>map(TdResult::of)
.doOnSuccess(s -> logger.trace("Sent request to TDLib {}", request));
} else {
return Mono.fromCallable(() -> {
if (request.getConstructor() == Close.CONSTRUCTOR) {
logger.trace("Sending close success to request {}", request);
return TdResult.of(new Ok());
} else {
logger.trace("Sending close error to request {} ", request);
throw new IllegalStateException("TDLib client is destroyed");
}
});
}
});
}
}
@Override
public Mono<Void> initialize() {
return Mono
.fromRunnable(() -> logger.trace("Initializing"))
.then(telegramClientFactory.create(implementationDetails))
.flatMap(reactorTelegramClient -> reactorTelegramClient.initialize().thenReturn(reactorTelegramClient))
.doOnNext(td::set)
.doOnNext(client -> client.execute(new TdApi.SetLogVerbosityLevel(1)))
.doOnSuccess(s -> logger.trace("Initialized"))
.then();
}
@Override
public Flux<TdApi.Object> receive(AsyncTdDirectOptions options) {
return Mono
.fromCallable(td::get)
.single()
.flatMapMany(ReactorTelegramClient::receive)
.doOnNext(update -> {
// Close the emitter if receive closed state
if (update.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR
&& ((UpdateAuthorizationState) update).authorizationState.getConstructor()
== AuthorizationStateClosed.CONSTRUCTOR) {
logger.debug("Received closed status from tdlib");
}
})
.subscribeOn(Schedulers.boundedElastic());
}
}

View File

@ -1,36 +0,0 @@
package it.tdlight.tdlibsession.td.direct;
import java.time.Duration;
import java.util.StringJoiner;
public class AsyncTdDirectOptions {
private final Duration receiveDuration;
private final int eventsSize;
/**
*
* @param receiveDuration Maximum number of seconds allowed for this function to wait for new records. Default: 1 sec
* @param eventsSize Maximum number of events allowed in list. Default: 350 events
*/
public AsyncTdDirectOptions(Duration receiveDuration, int eventsSize) {
this.receiveDuration = receiveDuration;
this.eventsSize = eventsSize;
}
public Duration getReceiveDuration() {
return receiveDuration;
}
public int getEventsSize() {
return eventsSize;
}
@Override
public String toString() {
return new StringJoiner(", ", AsyncTdDirectOptions.class.getSimpleName() + "[", "]")
.add("receiveDuration=" + receiveDuration)
.add("eventsSize=" + eventsSize)
.toString();
}
}

View File

@ -1,29 +0,0 @@
package it.tdlight.tdlibsession.td.direct;
import io.vertx.core.json.JsonObject;
import it.tdlight.tdlibsession.td.ReactorTelegramClient;
import it.tdlight.tdlibsession.td.WrappedReactorTelegramClient;
import it.tdlight.tdlight.ClientManager;
import it.tdlight.utils.MonoUtils;
import reactor.core.publisher.Mono;
public class TelegramClientFactory {
public TelegramClientFactory() {
}
public Mono<ReactorTelegramClient> create(JsonObject implementationDetails) {
return MonoUtils.fromBlockingSingle(() -> {
var implementationName = implementationDetails.getString("name", "native-client");
switch (implementationName) {
case "native-client":
return new WrappedReactorTelegramClient(ClientManager.createReactive());
case "test-client":
return new TestClient(implementationDetails.getJsonObject("test-client-settings"));
default:
return null;
}
});
}
}

View File

@ -1,156 +0,0 @@
package it.tdlight.tdlibsession.td.direct;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
import it.tdlight.jni.TdApi.AuthorizationStateClosing;
import it.tdlight.jni.TdApi.AuthorizationStateReady;
import it.tdlight.jni.TdApi.Close;
import it.tdlight.jni.TdApi.ConnectionStateReady;
import it.tdlight.jni.TdApi.FormattedText;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.GetMe;
import it.tdlight.jni.TdApi.Message;
import it.tdlight.jni.TdApi.MessageSenderUser;
import it.tdlight.jni.TdApi.MessageText;
import it.tdlight.jni.TdApi.Ok;
import it.tdlight.jni.TdApi.SetLogTagVerbosityLevel;
import it.tdlight.jni.TdApi.SetLogVerbosityLevel;
import it.tdlight.jni.TdApi.SetOption;
import it.tdlight.jni.TdApi.SetTdlibParameters;
import it.tdlight.jni.TdApi.TextEntity;
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
import it.tdlight.jni.TdApi.UpdateConnectionState;
import it.tdlight.jni.TdApi.UpdateNewMessage;
import it.tdlight.jni.TdApi.User;
import it.tdlight.tdlibsession.td.ReactorTelegramClient;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import org.jetbrains.annotations.Nullable;
import org.warp.commonutils.log.Logger;
import org.warp.commonutils.log.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Sinks.Empty;
public class TestClient implements ReactorTelegramClient {
private static final Logger logger = LoggerFactory.getLogger(TestClient.class);
private static final AtomicLong incrementalMessageId = new AtomicLong(1);
private final List<String> features;
private final Empty<java.lang.Object> closedSink = Sinks.empty();
public TestClient(JsonObject testClientSettings) {
JsonArray features = testClientSettings.getJsonArray("features", new JsonArray());
this.features = new ArrayList<>();
for (java.lang.Object feature : features) {
var featureName = (String) feature;
this.features.add(featureName);
}
}
private static Message generateRandomMessage(boolean randomSender, boolean randomChat, boolean randomText) {
var msg = new Message();
msg.sender = new MessageSenderUser(312042);
msg.chatId = 240213;
msg.id = incrementalMessageId.getAndIncrement();
var content = new MessageText();
content.text = new FormattedText("Text", new TextEntity[0]);
msg.content = content;
msg.date = (int) System.currentTimeMillis() / 1000;
return msg;
}
@Override
public Mono<Void> initialize() {
return Mono.empty();
}
@Override
public Flux<TdApi.Object> receive() {
return Flux.fromIterable(features).flatMap(featureName -> {
switch (featureName) {
case "status-update":
return Flux
.<TdApi.Object>just(
new UpdateAuthorizationState(new AuthorizationStateReady()),
new UpdateConnectionState(new ConnectionStateReady())
)
.mergeWith(closedSink
.asMono()
.thenMany(Flux.just(new UpdateAuthorizationState(new AuthorizationStateClosing()),
new UpdateAuthorizationState(new AuthorizationStateClosed())
))
);
case "infinite-messages":
var randomSenders = features.contains("random-senders");
var randomChats = features.contains("random-chats");
var randomTexts = features.contains("random-text");
return Flux
.<TdApi.Object>fromIterable(() -> new Iterator<>() {
@Override
public boolean hasNext() {
return true;
}
@Override
public TdApi.Object next() {
return new UpdateNewMessage(generateRandomMessage(randomSenders, randomChats, randomTexts));
}
}).takeUntilOther(this.closedSink.asMono());
default:
return Mono.fromCallable(() -> {
throw new IllegalArgumentException("Unknown feature name: " + featureName);
});
}
});
}
@Override
public <T extends TdApi.Object> Mono<TdApi.Object> send(Function<T> query, Duration timeout) {
return Mono.fromCallable(() -> {
TdApi.Object result = executeCommon(query);
if (result != null) {
return result;
}
return new TdApi.Error(500, "Unsupported");
});
}
@Override
public <T extends TdApi.Object> TdApi.Object execute(Function<T> query) {
TdApi.Object result = executeCommon(query);
if (result != null) {
return result;
}
return new TdApi.Error(500, "Unsupported");
}
@Nullable
public <T extends TdApi.Object> TdApi.Object executeCommon(Function<T> query) {
switch (query.getConstructor()) {
case SetLogVerbosityLevel.CONSTRUCTOR:
case SetLogTagVerbosityLevel.CONSTRUCTOR:
case SetTdlibParameters.CONSTRUCTOR:
case SetOption.CONSTRUCTOR:
return new Ok();
case GetMe.CONSTRUCTOR:
var user = new User();
user.id = 420;
user.firstName = "Test";
user.lastName = "Test";
user.phoneNumber = "+77";
return user;
case Close.CONSTRUCTOR:
closedSink.tryEmitEmpty();
return new Ok();
}
return null;
}
}

View File

@ -1,657 +0,0 @@
package it.tdlight.tdlibsession.td.easy;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AuthorizationState;
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
import it.tdlight.jni.TdApi.AuthorizationStateClosing;
import it.tdlight.jni.TdApi.AuthorizationStateReady;
import it.tdlight.jni.TdApi.AuthorizationStateWaitCode;
import it.tdlight.jni.TdApi.AuthorizationStateWaitEncryptionKey;
import it.tdlight.jni.TdApi.AuthorizationStateWaitOtherDeviceConfirmation;
import it.tdlight.jni.TdApi.AuthorizationStateWaitPassword;
import it.tdlight.jni.TdApi.AuthorizationStateWaitPhoneNumber;
import it.tdlight.jni.TdApi.AuthorizationStateWaitRegistration;
import it.tdlight.jni.TdApi.AuthorizationStateWaitTdlibParameters;
import it.tdlight.jni.TdApi.CheckAuthenticationBotToken;
import it.tdlight.jni.TdApi.CheckAuthenticationCode;
import it.tdlight.jni.TdApi.CheckAuthenticationPassword;
import it.tdlight.jni.TdApi.CheckDatabaseEncryptionKey;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.Object;
import it.tdlight.jni.TdApi.OptionValueBoolean;
import it.tdlight.jni.TdApi.OptionValueEmpty;
import it.tdlight.jni.TdApi.OptionValueInteger;
import it.tdlight.jni.TdApi.OptionValueString;
import it.tdlight.jni.TdApi.PhoneNumberAuthenticationSettings;
import it.tdlight.jni.TdApi.RegisterUser;
import it.tdlight.jni.TdApi.SetAuthenticationPhoneNumber;
import it.tdlight.jni.TdApi.SetTdlibParameters;
import it.tdlight.jni.TdApi.TdlibParameters;
import it.tdlight.jni.TdApi.Update;
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
import it.tdlight.tdlibsession.FatalErrorType;
import it.tdlight.tdlibsession.td.TdError;
import it.tdlight.tdlibsession.td.TdResult;
import it.tdlight.tdlibsession.td.middle.AsyncTdMiddle;
import it.tdlight.utils.MonoUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Comparator;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import org.reactivestreams.Publisher;
import org.warp.commonutils.log.Logger;
import org.warp.commonutils.log.LoggerFactory;
import org.warp.commonutils.error.InitializationException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Sinks.EmitResult;
import reactor.core.publisher.Sinks.Empty;
import reactor.core.publisher.Sinks.Many;
import reactor.core.publisher.Sinks.One;
import reactor.core.publisher.SynchronousSink;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
@SuppressWarnings("unused")
public class AsyncTdEasy {
private final Logger logger;
private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(1);
private final Empty<Void> closed = Sinks.empty();
private final Many<AuthorizationState> authStateSink = Sinks.many().replay().latest();
private final AtomicReference<AuthorizationState> authState = new AtomicReference<>(new AuthorizationStateClosed());
private final AtomicBoolean requestedDefinitiveExit = new AtomicBoolean();
private final AtomicBoolean canSendCloseRequest = new AtomicBoolean();
private final AtomicReference<TdEasySettings> settings = new AtomicReference<>(null);
private final Many<Error> globalErrors = Sinks.many().multicast().onBackpressureBuffer();
private final One<FatalErrorType> fatalError = Sinks.one();
private final AsyncTdMiddle td;
private final String logName;
private final Flux<Update> incomingUpdates;
private final Scheduler scheduler = Schedulers.parallel();
public AsyncTdEasy(AsyncTdMiddle td, String logName) {
this.td = td;
this.logName = logName;
this.logger = LoggerFactory.getLogger("AsyncTdEasy " + logName);
this.incomingUpdates = td.receive()
.doFirst(() -> {
canSendCloseRequest.set(true);
logger.debug("From now onwards TdApi.Close cannot be called");
})
.doOnTerminate(() -> {
canSendCloseRequest.set(false);
logger.debug("From now onwards TdApi.Close can be called");
})
.flatMapSequential(this::preprocessUpdates)
.map(update -> {
var state = authState.get();
Objects.requireNonNull(state, "State is not set");
return new AsyncTdUpdateObj(state, update);
})
.map(upd -> (TdApi.Update) upd.getUpdate())
.takeUntil(update -> {
if (update.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
var state = ((UpdateAuthorizationState) update).authorizationState;
return state.getConstructor() == AuthorizationStateClosed.CONSTRUCTOR;
}
return false;
})
.doOnError(ex -> {
if (ex instanceof TdError) {
var tdEx = (TdError) ex;
logger.error("Received an error update from telegram: " + tdEx.getTdCode() + " " + tdEx.getTdMessage());
FatalErrorType fatalErrorType;
try {
fatalErrorType = FatalErrorType.valueOf(tdEx.getTdMessage());
} catch (IllegalArgumentException ignored) {
fatalErrorType = FatalErrorType.INVALID_UPDATE;
}
this.fatalError.tryEmitValue(fatalErrorType);
} else {
logger.error(ex.getLocalizedMessage(), ex);
}
})
.doFinally(s -> {
var state = authState.get();
onUpdatesTerminated();
if (state.getConstructor() != AuthorizationStateClosed.CONSTRUCTOR) {
logger.warn("Updates stream has closed while"
+ " the current authorization state is"
+ " still {}. Setting authorization state as closed!", state.getClass().getSimpleName());
this.fatalError.tryEmitValue(FatalErrorType.CONNECTION_KILLED);
}
});
}
private void onUpdatesTerminated() {
logger.debug("Incoming updates flux terminated. Setting requestedDefinitiveExit: true");
requestedDefinitiveExit.set(true);
var newState = new AuthorizationStateClosed();
emitState(newState);
}
public Mono<Void> create(TdEasySettings settings) {
return Mono
.fromCallable(() -> {
// Create session directories
if (Files.notExists(Path.of(settings.databaseDirectory))) {
try {
Files.createDirectories(Path.of(settings.databaseDirectory));
} catch (IOException ex) {
throw new InitializationException(ex);
}
}
// Register fatal error handler
fatalError.asMono().flatMap(settings.getFatalErrorHandler()::onFatalError).subscribeOn(scheduler).subscribe();
return true;
})
.subscribeOn(Schedulers.boundedElastic())
.flatMap(_v -> {
this.settings.set(settings);
return Mono.empty();
})
.then(td.initialize());
}
/**
* Get TDLib state
*/
public Flux<AuthorizationState> state() {
return authStateSink.asFlux().distinct();
}
/**
* Get incoming updates from TDLib.
*/
public Flux<TdApi.Update> getIncomingUpdates() {
return incomingUpdates;
}
/**
* Get generic error updates from TDLib (When they are not linked to a precise request).
*/
public Flux<TdApi.Error> getIncomingErrors() {
return Flux.from(globalErrors.asFlux());
}
/**
* Receives fatal errors from TDLib.
*/
public Mono<FatalErrorType> getFatalErrors() {
return Mono.from(fatalError.asMono());
}
/**
* Sends request to TDLib.
* @param timeout Timeout duration.
* @return The response or {@link TdApi.Error}.
*/
public <T extends Object> Mono<TdResult<T>> send(TdApi.Function<T> request, Duration timeout) {
return td.execute(request, timeout, false);
}
private <T extends TdApi.Object> Mono<TdResult<T>> sendDirectly(Function<T> obj, boolean synchronous) {
return td.execute(obj, AsyncTdEasy.DEFAULT_TIMEOUT, synchronous);
}
/**
* Set verbosity level
* @param i level
*/
public Mono<Void> setVerbosityLevel(int i) {
return sendDirectly(new TdApi.SetLogVerbosityLevel(i), true).transform(this::thenOrFatalError);
}
/**
* Clear option on TDLib
* @param name option name
*/
public Mono<Void> clearOption(String name) {
return sendDirectly(new TdApi.SetOption(name, new TdApi.OptionValueEmpty()), false)
.transform(this::thenOrFatalError);
}
/**
* Set option on TDLib
* @param name option name
* @param value option value
*/
public Mono<Void> setOptionString(String name, String value) {
return sendDirectly(new TdApi.SetOption(name, new TdApi.OptionValueString(value)), false)
.transform(this::thenOrFatalError);
}
/**
* Set option on TDLib
* @param name option name
* @param value option value
*/
public Mono<Void> setOptionInteger(String name, long value) {
return sendDirectly(new TdApi.SetOption(name, new TdApi.OptionValueInteger(value)), false)
.transform(this::thenOrFatalError);
}
/**
* Set option on TDLib
* @param name option name
* @param value option value
*/
public Mono<Void> setOptionBoolean(String name, boolean value) {
return sendDirectly(new TdApi.SetOption(name, new TdApi.OptionValueBoolean(value)), false)
.transform(this::thenOrFatalError);
}
/**
* Get option from TDLib
* @param name option name
* @return The value or nothing
*/
public Mono<String> getOptionString(String name) {
return this
.<TdApi.OptionValue>sendDirectly(new TdApi.GetOption(name), false)
.<TdApi.OptionValue>handle(MonoUtils::orElseThrow)
.flatMap(value -> {
switch (value.getConstructor()) {
case OptionValueString.CONSTRUCTOR:
return Mono.just(((OptionValueString) value).value);
case OptionValueEmpty.CONSTRUCTOR:
return Mono.empty();
default:
return Mono.error(new UnsupportedOperationException("The option " + name + " is of type "
+ value.getClass().getSimpleName()));
}
});
}
/**
* Get option from TDLib
* @param name option name
* @return The value or nothing
*/
public Mono<Long> getOptionInteger(String name) {
return this
.<TdApi.OptionValue>sendDirectly(new TdApi.GetOption(name), false)
.<TdApi.OptionValue>handle(MonoUtils::orElseThrow)
.flatMap(value -> {
switch (value.getConstructor()) {
case OptionValueInteger.CONSTRUCTOR:
return Mono.just(((OptionValueInteger) value).value);
case OptionValueEmpty.CONSTRUCTOR:
return Mono.empty();
default:
return Mono.error(new UnsupportedOperationException(
"The option " + name + " is of type " + value.getClass().getSimpleName()));
}
});
}
/**
* Get option from TDLib
* @param name option name
* @return The value or nothing
*/
public Mono<Boolean> getOptionBoolean(String name) {
return this
.sendDirectly(new TdApi.GetOption(name), false)
.<TdApi.OptionValue>handle(MonoUtils::orElseThrow)
.flatMap(value -> {
switch (value.getConstructor()) {
case OptionValueBoolean.CONSTRUCTOR:
return Mono.just(((OptionValueBoolean) value).value);
case OptionValueEmpty.CONSTRUCTOR:
return Mono.empty();
default:
return Mono.error(new UnsupportedOperationException(
"The option " + name + " is of type " + value.getClass().getSimpleName()));
}
});
}
/**
* Synchronously executes TDLib requests. Only a few requests can be executed synchronously. May
* be called from any thread.
*
* @param request Request to the TDLib.
* @param timeout Timeout.
* @return The request response.
*/
public <T extends Object> Mono<TdResult<T>> execute(TdApi.Function<T> request, Duration timeout) {
return td.execute(request, timeout, true);
}
/**
* Closes the client gracefully by sending {@link TdApi.Close}.
*/
public Mono<Void> close() {
var waitClosed = closed.asMono()
.doFirst(() -> logger.debug("Waiting for AuthorizationStateClosed..."))
.doOnSuccess(s -> logger.debug("Received AuthorizationStateClosed after TdApi.Close"))
.transformDeferred(mono -> {
if (canSendCloseRequest.get()) {
return mono;
} else {
return Mono.fromRunnable(() -> emitState(new AuthorizationStateClosed()));
}
});
return Mono
.fromSupplier(authState::get)
.filter(state -> {
switch (state.getConstructor()) {
case AuthorizationStateClosing.CONSTRUCTOR:
case AuthorizationStateClosed.CONSTRUCTOR:
return false;
default:
return true;
}
})
.then(Mono.fromCallable(requestedDefinitiveExit::get).single())
.filter(closeRequested -> !closeRequested)
.doOnSuccess(s -> {
logger.debug("Setting requestedDefinitiveExit: true");
requestedDefinitiveExit.set(true);
})
.then(td
.execute(new TdApi.Close(), Duration.ofSeconds(5), false)
.doFirst(() -> logger.debug("Sending TdApi.Close"))
.doOnNext(closeResponse -> logger.debug("TdApi.Close response is: \"{}\"",
closeResponse.toString().replace('\n', ' ')
))
.doOnSuccess(s -> logger.debug("Sent TdApi.Close"))
.transformDeferred(closeMono -> {
if (canSendCloseRequest.get()) {
return closeMono;
} else {
return Mono.empty();
}
})
)
.then(waitClosed)
.doOnSuccess(s -> logger.info("AsyncTdEasy closed successfully"))
.then();
}
private Mono<Update> catchErrors(Object obj) {
return Mono.fromCallable(() -> {
if (obj.getConstructor() == Error.CONSTRUCTOR) {
var error = (Error) obj;
switch (error.message) {
case "PHONE_CODE_INVALID":
globalErrors.tryEmitNext(error);
return new UpdateAuthorizationState(new AuthorizationStateWaitCode());
case "PASSWORD_HASH_INVALID":
globalErrors.tryEmitNext(error);
return new UpdateAuthorizationState(new AuthorizationStateWaitPassword());
default:
globalErrors.tryEmitNext(error);
break;
}
analyzeFatalErrors(error);
return null;
} else {
return (Update) obj;
}
});
}
private void analyzeFatalErrors(Object obj) {
if (obj != null && obj.getConstructor() == Error.CONSTRUCTOR) {
var error = (Error) obj;
switch (error.message) {
case "PHONE_NUMBER_INVALID":
fatalError.tryEmitValue(FatalErrorType.PHONE_NUMBER_INVALID);
break;
case "ACCESS_TOKEN_INVALID":
fatalError.tryEmitValue(FatalErrorType.ACCESS_TOKEN_INVALID);
break;
case "CONNECTION_KILLED":
fatalError.tryEmitValue(FatalErrorType.CONNECTION_KILLED);
break;
case "INVALID_UPDATE":
fatalError.tryEmitValue(FatalErrorType.INVALID_UPDATE);
break;
case "PHONE_NUMBER_BANNED":
fatalError.tryEmitValue(FatalErrorType.PHONE_NUMBER_BANNED);
break;
}
}
}
private Publisher<TdApi.Update> preprocessUpdates(TdApi.Object updateObj) {
return Mono
.just(updateObj)
.flatMap(this::catchErrors)
.filter(obj -> obj.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR)
.map(obj -> ((UpdateAuthorizationState) obj).authorizationState)
.flatMap(obj -> {
switch (obj.getConstructor()) {
case AuthorizationStateWaitTdlibParameters.CONSTRUCTOR:
return Mono
.fromCallable(this.settings::get)
.single()
.map(settings -> {
var parameters = new TdlibParameters();
parameters.useTestDc = settings.useTestDc;
parameters.databaseDirectory = settings.databaseDirectory;
parameters.filesDirectory = settings.filesDirectory;
parameters.useFileDatabase = settings.useFileDatabase;
parameters.useChatInfoDatabase = settings.useChatInfoDatabase;
parameters.useMessageDatabase = settings.useMessageDatabase;
parameters.useSecretChats = false;
parameters.apiId = settings.apiId;
parameters.apiHash = settings.apiHash;
parameters.systemLanguageCode = settings.systemLanguageCode;
parameters.deviceModel = settings.deviceModel;
parameters.systemVersion = settings.systemVersion;
parameters.applicationVersion = settings.applicationVersion;
parameters.enableStorageOptimizer = settings.enableStorageOptimizer;
parameters.ignoreFileNames = settings.ignoreFileNames;
return new SetTdlibParameters(parameters);
})
.flatMap((SetTdlibParameters obj1) -> sendDirectly(obj1, false))
.transform(this::thenOrFatalError);
case AuthorizationStateWaitEncryptionKey.CONSTRUCTOR:
return sendDirectly(new CheckDatabaseEncryptionKey(), false)
.transform(this::thenOrFatalError)
.onErrorResume((error) -> {
logger.error("Error while checking TDLib encryption key", error);
return sendDirectly(new TdApi.Close(), false).then();
});
case AuthorizationStateWaitPhoneNumber.CONSTRUCTOR:
return Mono
.fromCallable(this.settings::get).single().flatMap(settings -> {
if (settings.isPhoneNumberSet()) {
return sendDirectly(new SetAuthenticationPhoneNumber(String.valueOf(settings.getPhoneNumber()),
new PhoneNumberAuthenticationSettings(false, false, false)
), false);
} else if (settings.isBotTokenSet()) {
return sendDirectly(new CheckAuthenticationBotToken(settings.getBotToken()), false);
} else {
return Mono.error(new IllegalArgumentException("A bot is neither an user or a bot"));
}
})
.transform(this::thenOrFatalError)
.onErrorResume((error) -> {
logger.error("Error while waiting for phone number", error);
return sendDirectly(new TdApi.Close(), false).then();
});
case AuthorizationStateWaitRegistration.CONSTRUCTOR:
var authorizationStateWaitRegistration = (AuthorizationStateWaitRegistration) obj;
RegisterUser registerUser = new RegisterUser();
if (authorizationStateWaitRegistration.termsOfService != null
&& authorizationStateWaitRegistration.termsOfService.text != null
&& !authorizationStateWaitRegistration.termsOfService.text.text.isBlank()) {
logger.info("Telegram Terms of Service:\n" + authorizationStateWaitRegistration.termsOfService.text.text);
}
return Mono
.fromCallable(this.settings::get)
.single()
.map(TdEasySettings::getParameterRequestHandler)
.flatMap(handler -> handler
.onParameterRequest(Parameter.ASK_FIRST_NAME, new ParameterInfoEmpty())
.filter(Objects::nonNull)
.map(String::trim)
.filter(firstName -> !firstName.isBlank() && firstName.length() <= 64 && firstName.length() > 0)
.repeatWhen(s -> s.takeWhile(n -> n == 0))
.last()
.doOnNext(firstName -> registerUser.firstName = firstName)
.then(handler
.onParameterRequest(Parameter.ASK_LAST_NAME, new ParameterInfoEmpty())
.filter(Objects::nonNull)
.map(String::trim)
.filter(lastName -> lastName.length() <= 64)
.repeatWhen(s -> s.takeWhile(n -> n == 0))
.last()
.defaultIfEmpty("")
.doOnNext(lastName -> registerUser.lastName = lastName)
)
.then(sendDirectly(registerUser, false))
.transform(this::thenOrLogRepeatError)
);
case TdApi.AuthorizationStateWaitOtherDeviceConfirmation.CONSTRUCTOR:
var authorizationStateWaitOtherDeviceConfirmation = (AuthorizationStateWaitOtherDeviceConfirmation) obj;
return Mono
.fromCallable(this.settings::get)
.single()
.map(TdEasySettings::getParameterRequestHandler)
.flatMap(handler -> handler.onParameterRequest(Parameter.NOTIFY_LINK,
new ParameterInfoNotifyLink(authorizationStateWaitOtherDeviceConfirmation.link)
));
case TdApi.AuthorizationStateWaitCode.CONSTRUCTOR:
var authorizationStateWaitCode = (AuthorizationStateWaitCode) obj;
return Mono
.fromCallable(this.settings::get)
.single()
.map(TdEasySettings::getParameterRequestHandler)
.flatMap(handler -> handler
.onParameterRequest(Parameter.ASK_CODE, new ParameterInfoCode(authorizationStateWaitCode.codeInfo.phoneNumber,
authorizationStateWaitCode.codeInfo.nextType,
authorizationStateWaitCode.codeInfo.timeout,
authorizationStateWaitCode.codeInfo.type))
.flatMap(code -> sendDirectly(new CheckAuthenticationCode(code), false))
.transform(this::thenOrLogRepeatError)
);
case AuthorizationStateWaitPassword.CONSTRUCTOR:
var authorizationStateWaitPassword = (AuthorizationStateWaitPassword) obj;
return Mono
.fromCallable(this.settings::get)
.single()
.map(TdEasySettings::getParameterRequestHandler)
.flatMap(handler -> handler
.onParameterRequest(Parameter.ASK_PASSWORD, new ParameterInfoPasswordHint(
authorizationStateWaitPassword.passwordHint))
.flatMap(password -> sendDirectly(new CheckAuthenticationPassword(password), false))
)
.transform(this::thenOrLogRepeatError);
case AuthorizationStateReady.CONSTRUCTOR: {
var state = new AuthorizationStateReady();
emitState(state);
return Mono.empty();
}
case AuthorizationStateClosing.CONSTRUCTOR:
logger.debug("Received AuthorizationStateClosing from td");
return Mono.empty();
case AuthorizationStateClosed.CONSTRUCTOR:
logger.debug("Received AuthorizationStateClosed from td");
return Mono.fromCallable(() -> {
var closeRequested = this.requestedDefinitiveExit.get();
if (closeRequested) {
logger.debug("td closed successfully");
} else {
logger.warn("td closed unexpectedly: {}", logName);
}
emitState(obj);
return closeRequested;
}).flatMap(closeRequested -> {
if (closeRequested) {
return Mono
.fromCallable(settings::get)
.single()
.map(settings -> settings.databaseDirectory)
.map(Path::of)
.flatMapIterable(sessionPath -> Set.of(sessionPath.resolve("media"),
sessionPath.resolve("passport"),
sessionPath.resolve("profile_photos"),
sessionPath.resolve("stickers"),
sessionPath.resolve("temp"),
sessionPath.resolve("thumbnails"),
sessionPath.resolve("wallpapers")
))
.filterWhen(file -> Mono
.fromCallable(() -> Files.exists(file))
.subscribeOn(Schedulers.boundedElastic()))
.doOnNext(directory -> {
try {
if (!Files.walk(directory)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.allMatch(File::delete)) {
throw new IOException("Can't delete a file!");
}
} catch (IOException e) {
logger.error("Can't delete temporary session subdirectory", e);
}
})
.then(Mono.just(true));
} else {
return Mono.just(false);
}
}).then();
default:
return Mono.empty();
}
})
.then(Mono.justOrEmpty(updateObj.getConstructor() == Error.CONSTRUCTOR ? null : (Update) updateObj));
}
private void emitState(AuthorizationState state) {
if (state.getConstructor() == AuthorizationStateClosed.CONSTRUCTOR) {
this.closed.tryEmitEmpty();
}
this.authState.set(state);
EmitResult emitResult;
while ((emitResult = this.authStateSink.tryEmitNext(state)) == EmitResult.FAIL_NON_SERIALIZED) {
// Wait 10ms
LockSupport.parkNanos(10L * 1000000L);
}
emitResult.orThrow();
}
private <T extends TdApi.Object> Mono<Void> thenOrFatalError(Mono<TdResult<T>> mono) {
return mono.doOnNext(result -> {
if (result.failed()) {
analyzeFatalErrors(result.cause());
}
}).transform(MonoUtils::thenOrError);
}
private <T extends TdApi.Object> Mono<Void> thenOrLogRepeatError(Mono<TdResult<T>> mono) {
return mono.handle((TdResult<T> optional, SynchronousSink<Void> sink) -> {
if (optional.succeeded()) {
sink.complete();
} else {
logger.error("Received TDLib error: {}", optional.cause());
sink.error(new TdError(optional.cause().code, optional.cause().message));
}
}).retry();
}
}

View File

@ -1,49 +0,0 @@
package it.tdlight.tdlibsession.td.easy;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AuthorizationState;
import java.util.Objects;
import java.util.StringJoiner;
public class AsyncTdUpdateObj {
private final AuthorizationState state;
private final TdApi.Object update;
public AsyncTdUpdateObj(AuthorizationState state, TdApi.Object update) {
this.state = state;
this.update = update;
}
public AuthorizationState getState() {
return state;
}
public TdApi.Object getUpdate() {
return update;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AsyncTdUpdateObj that = (AsyncTdUpdateObj) o;
return Objects.equals(state, that.state) && Objects.equals(update, that.update);
}
@Override
public int hashCode() {
return Objects.hash(state, update);
}
@Override
public String toString() {
return new StringJoiner(", ", AsyncTdUpdateObj.class.getSimpleName() + "[", "]")
.add("state=" + state)
.add("update=" + update)
.toString();
}
}

View File

@ -1,9 +0,0 @@
package it.tdlight.tdlibsession.td.easy;
import it.tdlight.tdlibsession.FatalErrorType;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Mono;
public interface FatalErrorHandler {
@NotNull Mono<Void> onFatalError(FatalErrorType error);
}

View File

@ -1,9 +0,0 @@
package it.tdlight.tdlibsession.td.easy;
public enum Parameter {
ASK_FIRST_NAME,
ASK_LAST_NAME,
ASK_CODE,
ASK_PASSWORD,
NOTIFY_LINK
}

View File

@ -1,5 +0,0 @@
package it.tdlight.tdlibsession.td.easy;
public interface ParameterInfo {
}

View File

@ -1,47 +0,0 @@
package it.tdlight.tdlibsession.td.easy;
import it.tdlight.jni.TdApi.AuthenticationCodeType;
import java.util.StringJoiner;
public class ParameterInfoCode implements ParameterInfo {
private final String phoneNumber;
private final AuthenticationCodeType nextType;
private final int timeout;
private final AuthenticationCodeType type;
public ParameterInfoCode(String phoneNumber,
AuthenticationCodeType nextType,
int timeout,
AuthenticationCodeType type) {
this.phoneNumber = phoneNumber;
this.nextType = nextType;
this.timeout = timeout;
this.type = type;
}
public String getPhoneNumber() {
return phoneNumber;
}
public AuthenticationCodeType getNextType() {
return nextType;
}
public int getTimeout() {
return timeout;
}
public AuthenticationCodeType getType() {
return type;
}
@Override
public String toString() {
return new StringJoiner(", ", ParameterInfoCode.class.getSimpleName() + "[", "]")
.add("phoneNumber='" + phoneNumber + "'")
.add("nextType=" + nextType)
.add("timeout=" + timeout)
.add("type=" + type)
.toString();
}
}

View File

@ -1,5 +0,0 @@
package it.tdlight.tdlibsession.td.easy;
public class ParameterInfoEmpty implements ParameterInfo {
}

View File

@ -1,13 +0,0 @@
package it.tdlight.tdlibsession.td.easy;
public class ParameterInfoNotifyLink implements ParameterInfo {
private final String link;
public ParameterInfoNotifyLink(String link) {
this.link = link;
}
public String getLink() {
return link;
}
}

View File

@ -1,13 +0,0 @@
package it.tdlight.tdlibsession.td.easy;
public class ParameterInfoPasswordHint implements ParameterInfo {
private final String hint;
public ParameterInfoPasswordHint(String hint) {
this.hint = hint;
}
public String getHint() {
return hint;
}
}

View File

@ -1,7 +0,0 @@
package it.tdlight.tdlibsession.td.easy;
import reactor.core.publisher.Mono;
public interface ParameterRequestHandler {
Mono<String> onParameterRequest(Parameter parameter, ParameterInfo parameterInfo);
}

View File

@ -1,47 +0,0 @@
package it.tdlight.tdlibsession.td.easy;
import it.tdlight.common.utils.ScannerUtils;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
public class ScannerParameterRequestHandler implements ParameterRequestHandler {
private final String botName;
public ScannerParameterRequestHandler(String botName) {
this.botName = botName;
}
@Override
public Mono<String> onParameterRequest(Parameter parameter, ParameterInfo parameterInfo) {
return Mono.fromCallable(() -> {
String question;
boolean trim = false;
switch (parameter) {
case ASK_FIRST_NAME: question = "Enter first name"; trim = true; break;
case ASK_LAST_NAME: question = "Enter last name"; trim = true; break;
case ASK_CODE: question = "Enter authentication code"; trim = true; break;
case ASK_PASSWORD:
question = "Enter your password";
String passwordMessage = "Password authorization of '" + this.botName + "':";
String hint = ((ParameterInfoPasswordHint) parameterInfo).getHint();
if (hint != null && !hint.isBlank()) {
passwordMessage += "\n\tHint: " + hint;
}
System.out.println(passwordMessage);
break;
case NOTIFY_LINK:
System.out.println("Please confirm this login link on another device: "
+ ((ParameterInfoNotifyLink) parameterInfo).getLink());
return "";
default: question = parameter.toString(); break;
}
var result = ScannerUtils.askParameter(this.botName, question);
if (trim) {
return result.trim();
} else {
return result;
}
}).subscribeOn(Schedulers.boundedElastic());
}
}

View File

@ -1,310 +0,0 @@
package it.tdlight.tdlibsession.td.easy;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import reactor.core.publisher.Mono;
public class TdEasySettings {
public final boolean useTestDc;
public final String databaseDirectory;
public final String filesDirectory;
public final boolean useFileDatabase;
public final boolean useChatInfoDatabase;
public final boolean useMessageDatabase;
public final int apiId;
public final String apiHash;
public final String systemLanguageCode;
public final String deviceModel;
public final String systemVersion;
public final String applicationVersion;
public final boolean enableStorageOptimizer;
public final boolean ignoreFileNames;
private final Long phoneNumber;
private final String botToken;
private final ParameterRequestHandler parameterRequestHandler;
private final FatalErrorHandler fatalErrorHandler;
public TdEasySettings(boolean useTestDc,
String databaseDirectory,
String filesDirectory,
boolean useFileDatabase,
boolean useChatInfoDatabase,
boolean useMessageDatabase,
int apiId,
String apiHash,
String systemLanguageCode,
String deviceModel,
String systemVersion,
String applicationVersion,
boolean enableStorageOptimizer,
boolean ignoreFileNames,
@Nullable Long phoneNumber,
@Nullable String botToken,
@Nullable ParameterRequestHandler parameterRequestHandler,
@Nullable FatalErrorHandler fatalErrorHandler) {
this.useTestDc = useTestDc;
this.databaseDirectory = databaseDirectory;
this.filesDirectory = filesDirectory;
this.useFileDatabase = useFileDatabase;
this.useChatInfoDatabase = useChatInfoDatabase;
this.useMessageDatabase = useMessageDatabase;
this.apiId = apiId;
this.apiHash = apiHash;
this.systemLanguageCode = systemLanguageCode;
this.deviceModel = deviceModel;
this.systemVersion = systemVersion;
this.applicationVersion = applicationVersion;
this.enableStorageOptimizer = enableStorageOptimizer;
this.ignoreFileNames = ignoreFileNames;
this.phoneNumber = phoneNumber;
this.botToken = botToken;
if ((phoneNumber == null) == (botToken == null)) {
throw new IllegalArgumentException("You must set a phone number or a bot token");
}
if (parameterRequestHandler == null) {
if (botToken != null) {
parameterRequestHandler = new ScannerParameterRequestHandler("bot_" + botToken.split(":")[0]);
} else {
parameterRequestHandler = new ScannerParameterRequestHandler("+" + phoneNumber);
}
}
this.parameterRequestHandler = parameterRequestHandler;
if (fatalErrorHandler == null) {
fatalErrorHandler = error -> Mono.empty();
}
this.fatalErrorHandler = fatalErrorHandler;
}
public boolean isPhoneNumberSet() {
return phoneNumber != null;
}
public long getPhoneNumber() {
return Objects.requireNonNull(phoneNumber, "You must set a phone number");
}
public boolean isBotTokenSet() {
return botToken != null;
}
public String getBotToken() {
return Objects.requireNonNull(botToken, "You must set a bot token");
}
public ParameterRequestHandler getParameterRequestHandler() {
return Objects.requireNonNull(parameterRequestHandler, "You must set a parameter request handler");
}
public FatalErrorHandler getFatalErrorHandler() {
return Objects.requireNonNull(fatalErrorHandler, "You must set a fatal error handler");
}
public static Builder newBuilder() {
return new Builder();
}
public static class Builder {
private boolean useTestDc = false;
private String databaseDirectory = "jtdlib-database";
private String filesDirectory = "jtdlib-files";
private boolean useFileDatabase = true;
private boolean useChatInfoDatabase = true;
private boolean useMessageDatabase = true;
private int apiId = 376588;
private String apiHash = "2143fdfc2bbba3ec723228d2f81336c9";
private String systemLanguageCode = "en";
private String deviceModel = "JTDLib";
private String systemVersion = "JTDLib";
private String applicationVersion = "1.0";
private boolean enableStorageOptimizer = false;
private boolean ignoreFileNames = false;
@Nullable
private Long phoneNumber = null;
@Nullable
private String botToken = null;
private ParameterRequestHandler parameterRequestHandler;
@Nullable
private FatalErrorHandler fatalErrorHandler;
private Builder() {
}
public boolean isUseTestDc() {
return useTestDc;
}
public Builder setUseTestDc(boolean useTestDc) {
this.useTestDc = useTestDc;
return this;
}
public String getDatabaseDirectory() {
return databaseDirectory;
}
public Builder setDatabaseDirectory(String databaseDirectory) {
this.databaseDirectory = databaseDirectory;
return this;
}
public String getFilesDirectory() {
return filesDirectory;
}
public Builder setFilesDirectory(String filesDirectory) {
this.filesDirectory = filesDirectory;
return this;
}
public boolean isUseFileDatabase() {
return useFileDatabase;
}
public Builder setUseFileDatabase(boolean useFileDatabase) {
this.useFileDatabase = useFileDatabase;
return this;
}
public boolean isUseChatInfoDatabase() {
return useChatInfoDatabase;
}
public Builder setUseChatInfoDatabase(boolean useChatInfoDatabase) {
this.useChatInfoDatabase = useChatInfoDatabase;
return this;
}
public boolean isUseMessageDatabase() {
return useMessageDatabase;
}
public Builder setUseMessageDatabase(boolean useMessageDatabase) {
this.useMessageDatabase = useMessageDatabase;
return this;
}
public int getApiId() {
return apiId;
}
public Builder setApiId(int apiId) {
this.apiId = apiId;
return this;
}
public String getApiHash() {
return apiHash;
}
public Builder setApiHash(String apiHash) {
this.apiHash = apiHash;
return this;
}
public String getSystemLanguageCode() {
return systemLanguageCode;
}
public Builder setSystemLanguageCode(String systemLanguageCode) {
this.systemLanguageCode = systemLanguageCode;
return this;
}
public String getDeviceModel() {
return deviceModel;
}
public Builder setDeviceModel(String deviceModel) {
this.deviceModel = deviceModel;
return this;
}
public String getSystemVersion() {
return systemVersion;
}
public Builder setSystemVersion(String systemVersion) {
this.systemVersion = systemVersion;
return this;
}
public String getApplicationVersion() {
return applicationVersion;
}
public Builder setApplicationVersion(String applicationVersion) {
this.applicationVersion = applicationVersion;
return this;
}
public boolean isEnableStorageOptimizer() {
return enableStorageOptimizer;
}
public Builder setEnableStorageOptimizer(boolean enableStorageOptimizer) {
this.enableStorageOptimizer = enableStorageOptimizer;
return this;
}
public boolean isIgnoreFileNames() {
return ignoreFileNames;
}
public Builder setIgnoreFileNames(boolean ignoreFileNames) {
this.ignoreFileNames = ignoreFileNames;
return this;
}
public Builder setPhoneNumber(long phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
public Builder setBotToken(String botToken) {
this.botToken = botToken;
return this;
}
public Builder setParameterRequestHandler(ParameterRequestHandler parameterRequestHandler) {
this.parameterRequestHandler = parameterRequestHandler;
return this;
}
public ParameterRequestHandler getParameterRequestHandler() {
return parameterRequestHandler;
}
public Builder setFatalErrorHandler(FatalErrorHandler fatalErrorHandler) {
this.fatalErrorHandler = fatalErrorHandler;
return this;
}
public @Nullable FatalErrorHandler getFatalErrorHandler() {
return fatalErrorHandler;
}
public TdEasySettings build() {
return new TdEasySettings(useTestDc,
databaseDirectory,
filesDirectory,
useFileDatabase,
useChatInfoDatabase,
useMessageDatabase,
apiId,
apiHash,
systemLanguageCode,
deviceModel,
systemVersion,
applicationVersion,
enableStorageOptimizer,
ignoreFileNames,
phoneNumber,
botToken,
parameterRequestHandler,
fatalErrorHandler
);
}
}
}

View File

@ -1,28 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import it.tdlight.jni.TdApi;
import it.tdlight.tdlibsession.td.TdResult;
import java.time.Duration;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface AsyncTdMiddle {
Mono<Void> initialize();
/**
* Receives incoming updates from TDLib.
*
* @return Updates (or Error if received a fatal error. A fatal error means that the client is no longer working)
*/
Flux<TdApi.Object> receive();
/**
* Sends request to TDLib. May be called from any thread.
*
* @param request Request to TDLib.
* @param timeout Timeout.
* @param executeSync Execute the function synchronously.
*/
<T extends TdApi.Object> Mono<TdResult<T>> execute(TdApi.Function<T> request, Duration timeout, boolean executeSync);
}

View File

@ -1,49 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.reactivex.core.buffer.Buffer;
import java.util.Objects;
import java.util.StringJoiner;
public final class EndSessionMessage {
private final int id;
private final Buffer binlog;
public EndSessionMessage(int id, Buffer binlog) {
this.id = id;
this.binlog = binlog;
}
public int id() {
return id;
}
public Buffer binlog() {
return binlog;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
EndSessionMessage that = (EndSessionMessage) o;
return id == that.id && Objects.equals(binlog, that.binlog);
}
@Override
public int hashCode() {
return Objects.hash(id, binlog);
}
@Override
public String toString() {
return new StringJoiner(", ", EndSessionMessage.class.getSimpleName() + "[", "]")
.add("id=" + id)
.add("binlog=" + binlog)
.toString();
}
}

View File

@ -1,46 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.MessageCodec;
import it.tdlight.utils.BufferUtils;
public class EndSessionMessageCodec implements MessageCodec<EndSessionMessage, EndSessionMessage> {
private final String codecName;
public EndSessionMessageCodec() {
super();
this.codecName = "EndSessionMessageCodec";
}
@Override
public void encodeToWire(Buffer buffer, EndSessionMessage t) {
BufferUtils.encode(buffer, os -> {
os.writeInt(t.id());
BufferUtils.writeBuf(os, t.binlog());
});
}
@Override
public EndSessionMessage decodeFromWire(int pos, Buffer buffer) {
return BufferUtils.decode(pos, buffer, is -> new EndSessionMessage(is.readInt(), BufferUtils.rxReadBuf(is)));
}
@Override
public EndSessionMessage transform(EndSessionMessage t) {
// If a message is sent *locally* across the event bus.
// This sends message just as is
return t;
}
@Override
public String name() {
return codecName;
}
@Override
public byte systemCodecID() {
// Always -1
return -1;
}
}

View File

@ -1,93 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.buffer.Buffer;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Function;
import java.time.Duration;
import java.util.Objects;
import java.util.StringJoiner;
public class ExecuteObject<T extends TdApi.Object> {
private static final TdExecuteObjectMessageCodec<?> realCodec = new TdExecuteObjectMessageCodec<>();
private boolean executeDirectly;
private TdApi.Function<T> request;
private Duration timeout;
private int pos;
private Buffer buffer;
public ExecuteObject(boolean executeDirectly, Function<T> request, Duration timeout) {
if (request == null) throw new NullPointerException();
this.executeDirectly = executeDirectly;
this.request = request;
this.timeout = timeout;
}
public ExecuteObject(int pos, Buffer buffer) {
this.pos = pos;
this.buffer = buffer;
}
private void tryDecode() {
if (request == null) {
@SuppressWarnings("unchecked")
ExecuteObject<T> data = (ExecuteObject<T>) realCodec.decodeFromWire(pos, buffer);
this.executeDirectly = data.executeDirectly;
this.request = data.request;
this.buffer = null;
this.timeout = data.timeout;
}
}
public boolean isExecuteDirectly() {
tryDecode();
return executeDirectly;
}
public TdApi.Function<T> getRequest() {
tryDecode();
return request;
}
public Duration getTimeout() {
tryDecode();
return timeout;
}
@Override
public boolean equals(Object o) {
tryDecode();
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ExecuteObject<?> that = (ExecuteObject<?>) o;
if (executeDirectly != that.executeDirectly) {
return false;
}
return Objects.equals(request, that.request);
}
@Override
public int hashCode() {
tryDecode();
int result = (executeDirectly ? 1 : 0);
result = 31 * result + (request != null ? request.hashCode() : 0);
return result;
}
@Override
public String toString() {
return new StringJoiner(", ", ExecuteObject.class.getSimpleName() + "[", "]")
.add("executeDirectly=" + executeDirectly)
.add("request=" + request)
.add("timeout=" + timeout)
.toString();
}
}

View File

@ -1,43 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.MessageCodec;
import it.tdlight.jni.TdApi;
public class LazyTdExecuteObjectMessageCodec<T extends TdApi.Object>
implements MessageCodec<ExecuteObject<T>, ExecuteObject<T>> {
private static final TdExecuteObjectMessageCodec<?> realCodec = new TdExecuteObjectMessageCodec<>();
public LazyTdExecuteObjectMessageCodec() {
super();
}
@Override
public void encodeToWire(Buffer buffer, ExecuteObject t) {
realCodec.encodeToWire(buffer, t);
}
@Override
public ExecuteObject<T> decodeFromWire(int pos, Buffer buffer) {
return new ExecuteObject<>(pos, buffer);
}
@Override
public ExecuteObject<T> transform(ExecuteObject t) {
// If a message is sent *locally* across the event bus.
// This sends message just as is
return t;
}
@Override
public String name() {
return "ExecuteObjectCodec";
}
@Override
public byte systemCodecID() {
// Always -1
return -1;
}
}

View File

@ -1,44 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.MessageCodec;
@SuppressWarnings("rawtypes")
public class LazyTdResultListMessageCodec implements MessageCodec<TdResultList, TdResultList> {
private final String codecName;
private static final TdResultListMessageCodec realCodec = new TdResultListMessageCodec();
public LazyTdResultListMessageCodec() {
super();
this.codecName = "TdOptListCodec";
}
@Override
public void encodeToWire(Buffer buffer, TdResultList t) {
realCodec.encodeToWire(buffer, t);
}
@Override
public TdResultList decodeFromWire(int pos, Buffer buffer) {
return new TdResultList(pos, buffer);
}
@Override
public TdResultList transform(TdResultList t) {
// If a message is sent *locally* across the event bus.
// This sends message just as is
return t;
}
@Override
public String name() {
return codecName;
}
@Override
public byte systemCodecID() {
// Always -1
return -1;
}
}

View File

@ -1,44 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.MessageCodec;
@SuppressWarnings("rawtypes")
public class LazyTdResultMessageCodec implements MessageCodec<TdResultMessage, TdResultMessage> {
private final String codecName;
private static final TdResultMessageCodec realCodec = new TdResultMessageCodec();
public LazyTdResultMessageCodec() {
super();
this.codecName = "TdResultCodec";
}
@Override
public void encodeToWire(Buffer buffer, TdResultMessage t) {
realCodec.encodeToWire(buffer, t);
}
@Override
public TdResultMessage decodeFromWire(int pos, Buffer buffer) {
return new TdResultMessage(pos, buffer);
}
@Override
public TdResultMessage transform(TdResultMessage t) {
// If a message is sent *locally* across the event bus.
// This sends message just as is
return t;
}
@Override
public String name() {
return codecName;
}
@Override
public byte systemCodecID() {
// Always -1
return -1;
}
}

View File

@ -1,73 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.json.JsonObject;
import io.vertx.reactivex.core.buffer.Buffer;
import java.util.Objects;
import java.util.StringJoiner;
public final class StartSessionMessage {
private final long id;
private final String alias;
private final Buffer binlog;
private final long binlogDate;
private final JsonObject implementationDetails;
public StartSessionMessage(long id, String alias, Buffer binlog, long binlogDate, JsonObject implementationDetails) {
this.id = id;
this.alias = alias;
this.binlog = binlog;
this.binlogDate = binlogDate;
this.implementationDetails = implementationDetails;
}
public long id() {
return id;
}
public String alias() {
return alias;
}
public Buffer binlog() {
return binlog;
}
public long binlogDate() {
return binlogDate;
}
public JsonObject implementationDetails() {
return implementationDetails;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
StartSessionMessage that = (StartSessionMessage) o;
return id == that.id && binlogDate == that.binlogDate && Objects.equals(alias, that.alias) && Objects.equals(binlog,
that.binlog
) && Objects.equals(implementationDetails, that.implementationDetails);
}
@Override
public int hashCode() {
return Objects.hash(id, alias, binlog, binlogDate, implementationDetails);
}
@Override
public String toString() {
return new StringJoiner(", ", StartSessionMessage.class.getSimpleName() + "[", "]")
.add("id=" + id)
.add("alias='" + alias + "'")
.add("binlog=" + binlog)
.add("binlogDate=" + binlogDate)
.add("implementationDetails=" + implementationDetails)
.toString();
}
}

View File

@ -1,56 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.MessageCodec;
import io.vertx.core.json.JsonObject;
import it.tdlight.utils.BufferUtils;
import org.warp.commonutils.serialization.UTFUtils;
public class StartSessionMessageCodec implements MessageCodec<StartSessionMessage, StartSessionMessage> {
private final String codecName;
public StartSessionMessageCodec() {
super();
this.codecName = "StartSessionMessageCodec";
}
@Override
public void encodeToWire(Buffer buffer, StartSessionMessage t) {
BufferUtils.encode(buffer, os -> {
os.writeLong(t.id());
UTFUtils.writeUTF(os, t.alias());
BufferUtils.writeBuf(os, t.binlog());
os.writeLong(t.binlogDate());
UTFUtils.writeUTF(os, t.implementationDetails().toString());
});
}
@Override
public StartSessionMessage decodeFromWire(int pos, Buffer buffer) {
return BufferUtils.decode(pos, buffer, is -> new StartSessionMessage(is.readLong(),
UTFUtils.readUTF(is),
BufferUtils.rxReadBuf(is),
is.readLong(),
new JsonObject(UTFUtils.readUTF(is))
));
}
@Override
public StartSessionMessage transform(StartSessionMessage t) {
// If a message is sent *locally* across the event bus.
// This sends message just as is
return t;
}
@Override
public String name() {
return codecName;
}
@Override
public byte systemCodecID() {
// Always -1
return -1;
}
}

View File

@ -1,303 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import com.hazelcast.config.Config;
import com.hazelcast.config.MapConfig;
import com.hazelcast.config.MultiMapConfig;
import com.hazelcast.config.cp.SemaphoreConfig;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Handler;
import io.vertx.core.VertxOptions;
import io.vertx.core.eventbus.DeliveryOptions;
import io.vertx.core.eventbus.MessageCodec;
import io.vertx.core.http.ClientAuth;
import io.vertx.core.metrics.MetricsOptions;
import io.vertx.core.net.JksOptions;
import io.vertx.core.spi.cluster.ClusterManager;
import io.vertx.reactivex.core.Vertx;
import io.vertx.reactivex.core.eventbus.EventBus;
import io.vertx.reactivex.core.eventbus.Message;
import io.vertx.reactivex.core.eventbus.MessageConsumer;
import io.vertx.reactivex.core.shareddata.SharedData;
import io.vertx.spi.cluster.hazelcast.HazelcastClusterManager;
import it.tdlight.common.ConstructorDetector;
import it.tdlight.jni.TdApi;
import it.tdlight.utils.MonoUtils;
import java.nio.channels.AlreadyBoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.jetbrains.annotations.Nullable;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
public class TdClusterManager {
private static final AtomicBoolean definedMasterCluster = new AtomicBoolean(false);
private static final AtomicBoolean definedNodesCluster = new AtomicBoolean(false);
private final ClusterManager mgr;
private final VertxOptions vertxOptions;
private final Vertx vertx;
private final boolean isMaster;
@SuppressWarnings({"unchecked", "rawtypes"})
public TdClusterManager(ClusterManager mgr, VertxOptions vertxOptions, Vertx vertx, boolean isMaster) {
this.isMaster = isMaster;
this.mgr = mgr;
this.vertxOptions = vertxOptions;
this.vertx = vertx;
if (vertx != null && vertx.eventBus() != null) {
vertx
.eventBus()
.getDelegate()
.registerDefaultCodec(TdResultList.class, new TdResultListMessageCodec())
.registerDefaultCodec(ExecuteObject.class, new TdExecuteObjectMessageCodec())
.registerDefaultCodec(TdResultMessage.class, new TdResultMessageCodec())
.registerDefaultCodec(StartSessionMessage.class, new StartSessionMessageCodec())
.registerDefaultCodec(EndSessionMessage.class, new EndSessionMessageCodec());
for (Class<?> declaredClass : TdApi.class.getDeclaredClasses()) {
if (declaredClass.isAssignableFrom(declaredClass)) {
vertx.eventBus().getDelegate().registerDefaultCodec(declaredClass, new TdMessageCodec(declaredClass));
}
}
}
}
public static Mono<TdClusterManager> ofMaster(@Nullable JksOptions keyStoreOptions,
@Nullable JksOptions trustStoreOptions,
boolean onlyLocal,
String masterHostname,
String netInterface,
int port,
Set<String> nodesAddresses) {
if (definedMasterCluster.compareAndSet(false, true)) {
var vertxOptions = new VertxOptions();
netInterface = onlyLocal ? "127.0.0.1" : netInterface;
Config cfg;
if (!onlyLocal) {
cfg = new Config();
cfg.setInstanceName("Master");
} else {
cfg = null;
}
return of(cfg,
vertxOptions,
keyStoreOptions, trustStoreOptions, masterHostname, netInterface, port, nodesAddresses, true);
} else {
return Mono.error(new AlreadyBoundException());
}
}
public static Mono<TdClusterManager> ofNodes(@Nullable JksOptions keyStoreOptions,
@Nullable JksOptions trustStoreOptions,
boolean onlyLocal,
String masterHostname,
String netInterface,
int port,
Set<String> nodesAddresses) {
return Mono.defer(() -> {
if (definedNodesCluster.compareAndSet(false, true)) {
var vertxOptions = new VertxOptions();
var netInterfaceF = onlyLocal ? "127.0.0.1" : netInterface;
Config cfg;
if (!onlyLocal) {
cfg = new Config();
cfg.setInstanceName("Node-" + new Random().nextLong());
} else {
cfg = null;
}
return of(cfg, vertxOptions, keyStoreOptions, trustStoreOptions, masterHostname, netInterfaceF, port,
nodesAddresses, false);
} else {
return Mono.error(new AlreadyBoundException());
}
});
}
public static Mono<TdClusterManager> of(@Nullable Config cfg,
VertxOptions vertxOptions,
@Nullable JksOptions keyStoreOptions,
@Nullable JksOptions trustStoreOptions,
String masterHostname,
String netInterface,
int port,
Set<String> nodesAddresses,
boolean isMaster) {
ClusterManager mgr;
if (cfg != null) {
cfg.getNetworkConfig().setPortCount(1);
cfg.getNetworkConfig().setPort(port);
cfg.getNetworkConfig().setPortAutoIncrement(false);
cfg.getPartitionGroupConfig().setEnabled(false);
cfg.addMapConfig(new MapConfig().setName("__vertx.haInfo").setBackupCount(1));
cfg.addMapConfig(new MapConfig().setName("__vertx.nodeInfo").setBackupCount(1));
cfg
.getCPSubsystemConfig()
.setCPMemberCount(0)
.setSemaphoreConfigs(Map.of("__vertx.*", new SemaphoreConfig().setInitialPermits(1).setJDKCompatible(false)));
cfg.addMultiMapConfig(new MultiMapConfig().setName("__vertx.subs").setBackupCount(1).setValueCollectionType("SET"));
cfg.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false);
cfg.getNetworkConfig().getJoin().getAwsConfig().setEnabled(false);
cfg.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true);
var addresses = new ArrayList<>(nodesAddresses);
cfg.getNetworkConfig().getJoin().getTcpIpConfig().setMembers(addresses);
cfg.getNetworkConfig().getInterfaces().clear();
cfg.getNetworkConfig().getInterfaces().setInterfaces(Collections.singleton(netInterface)).setEnabled(true);
cfg.getNetworkConfig().setOutboundPorts(Collections.singleton(0));
cfg.setProperty("hazelcast.logging.type", "slf4j");
cfg.setProperty("hazelcast.wait.seconds.before.join", "0");
cfg.setProperty("hazelcast.tcp.join.port.try.count", "5");
cfg.setProperty("hazelcast.socket.bind.any", "false");
cfg.setProperty("hazelcast.health.monitoring.level", "OFF");
cfg.setClusterName("tdlib-session-container");
mgr = new HazelcastClusterManager(cfg);
vertxOptions.setClusterManager(mgr);
vertxOptions.getEventBusOptions().setConnectTimeout(120000);
//vertxOptions.getEventBusOptions().setIdleTimeout(60);
//vertxOptions.getEventBusOptions().setSsl(false);
vertxOptions.getEventBusOptions().setSslHandshakeTimeout(120000).setSslHandshakeTimeoutUnit(TimeUnit.MILLISECONDS);
if (keyStoreOptions != null && trustStoreOptions != null) {
vertxOptions.getEventBusOptions().setKeyStoreOptions(keyStoreOptions);
vertxOptions.getEventBusOptions().setTrustStoreOptions(trustStoreOptions);
vertxOptions
.getEventBusOptions()
.setUseAlpn(true)
.setSsl(true)
.setEnabledSecureTransportProtocols(Set.of("TLSv1.3", "TLSv1.2"));
} else {
vertxOptions
.getEventBusOptions()
.setSsl(false);
}
vertxOptions.getEventBusOptions().setHost(masterHostname);
vertxOptions.getEventBusOptions().setPort(port + 1);
vertxOptions.getEventBusOptions().setClientAuth(ClientAuth.REQUIRED);
} else {
mgr = null;
vertxOptions.setClusterManager(null);
}
vertxOptions.setPreferNativeTransport(true);
vertxOptions.setMetricsOptions(new MetricsOptions().setEnabled(false));
// check for blocked threads every 5s
vertxOptions.setBlockedThreadCheckInterval(5);
vertxOptions.setBlockedThreadCheckIntervalUnit(TimeUnit.SECONDS);
// warn if an event loop thread handler took more than 10s to execute
vertxOptions.setMaxEventLoopExecuteTime(10);
vertxOptions.setMaxEventLoopExecuteTimeUnit(TimeUnit.SECONDS);
// warn if an worker thread handler took more than 10s to execute
vertxOptions.setMaxWorkerExecuteTime(10);
vertxOptions.setMaxWorkerExecuteTimeUnit(TimeUnit.SECONDS);
// log the stack trace if an event loop or worker handler took more than 20s to execute
vertxOptions.setWarningExceptionTime(100);
vertxOptions.setWarningExceptionTimeUnit(TimeUnit.MILLISECONDS);
return Mono
.defer(() -> {
if (mgr != null) {
return Vertx.rxClusteredVertx(vertxOptions).as(MonoUtils::toMono).subscribeOn(Schedulers.boundedElastic());
} else {
return Mono.just(Vertx.vertx(vertxOptions));
}
})
.flatMap(vertx -> Mono
.fromCallable(() -> new TdClusterManager(mgr, vertxOptions, vertx, isMaster))
.subscribeOn(Schedulers.boundedElastic())
);
}
public Vertx getVertx() {
return vertx;
}
public EventBus getEventBus() {
return vertx.eventBus();
}
public VertxOptions getVertxOptions() {
return vertxOptions;
}
public DeliveryOptions newDeliveryOpts() {
return new DeliveryOptions().setSendTimeout(120000);
}
/**
*
* @param messageCodec
* @param <T>
* @return true if registered, false if already registered
*/
public <T> boolean registerCodec(MessageCodec<T, T> messageCodec) {
try {
vertx.eventBus().registerCodec(messageCodec);
return true;
} catch (IllegalStateException ex) {
if (ex.getMessage().startsWith("Already a default codec registered for class")) {
return false;
}
if (ex.getMessage().startsWith("Already a codec registered with name")) {
return false;
}
throw ex;
}
}
/**
* Create a message consumer against the specified address.
* <p>
* The returned consumer is not yet registered
* at the address, registration will be effective when {@link MessageConsumer#handler(io.vertx.core.Handler)}
* is called.
*
* @param address the address that it will register it at
* @param localOnly if you want to receive only local messages
* @return the event bus message consumer
*/
public <T> MessageConsumer<T> consumer(String address, boolean localOnly) {
if (localOnly) {
return vertx.eventBus().localConsumer(address);
} else {
return vertx.eventBus().consumer(address);
}
}
/**
* Create a consumer and register it against the specified address.
*
* @param address the address that will register it at
* @param localOnly if you want to receive only local messages
* @param handler the handler that will process the received messages
*
* @return the event bus message consumer
*/
public <T> MessageConsumer<T> consumer(String address, boolean localOnly, Handler<Message<T>> handler) {
if (localOnly) {
return vertx.eventBus().localConsumer(address, handler);
} else {
return vertx.eventBus().consumer(address, handler);
}
}
public DeploymentOptions newDeploymentOpts() {
return new DeploymentOptions().setWorkerPoolName("td-main-pool");
}
public SharedData getSharedData() {
return vertx.sharedData();
}
public Mono<Void> close() {
return Mono.from(vertx.rxClose().toFlowable()).then(Mono.fromRunnable(() -> {
if (isMaster) {
definedMasterCluster.set(false);
}
}));
}
}

View File

@ -1,53 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.MessageCodec;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.utils.BufferUtils;
import java.time.Duration;
public class TdExecuteObjectMessageCodec<T extends TdApi.Object>
implements MessageCodec<ExecuteObject<T>, ExecuteObject<T>> {
public TdExecuteObjectMessageCodec() {
super();
}
@Override
public void encodeToWire(Buffer buffer, ExecuteObject t) {
BufferUtils.encode(buffer, os -> {
os.writeBoolean(t.isExecuteDirectly());
t.getRequest().serialize(os);
os.writeLong(t.getTimeout().toMillis());
});
}
@SuppressWarnings("unchecked")
@Override
public ExecuteObject<T> decodeFromWire(int pos, Buffer buffer) {
return BufferUtils.decode(pos, buffer, is -> new ExecuteObject<T>(
is.readBoolean(),
(Function<T>) TdApi.Deserializer.deserialize(is),
Duration.ofMillis(is.readLong())
));
}
@Override
public ExecuteObject<T> transform(ExecuteObject<T> t) {
// If a message is sent *locally* across the event bus.
// This sends message just as is
return t;
}
@Override
public String name() {
return "ExecuteObjectCodec";
}
@Override
public byte systemCodecID() {
// Always -1
return -1;
}
}

View File

@ -1,57 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.ByteBufOutputStream;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.MessageCodec;
import it.tdlight.jni.TdApi;
import java.io.IOException;
public class TdMessageCodec<T extends TdApi.Object> implements MessageCodec<T, T> {
private final Class<T> clazz;
private final String codecName;
public TdMessageCodec(Class<T> clazz) {
super();
this.clazz = clazz;
this.codecName = clazz.getSimpleName() + "TdCodec";
}
@Override
public void encodeToWire(Buffer buffer, T t) {
try (var os = new ByteBufOutputStream(buffer.getByteBuf())) {
t.serialize(os);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
@Override
public T decodeFromWire(int pos, Buffer buffer) {
try (var is = new ByteBufInputStream(buffer.getByteBuf(), pos)) {
//noinspection unchecked
return (T) TdApi.Deserializer.deserialize(is);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
@Override
public T transform(T t) {
// If a message is sent *locally* across the event bus.
// This sends message just as is
return t;
}
@Override
public String name() {
return codecName;
}
@Override
public byte systemCodecID() {
// Always -1
return -1;
}
}

View File

@ -1,93 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.buffer.Buffer;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Error;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
public class TdResultList {
private static final TdResultListMessageCodec realCodec = new TdResultListMessageCodec();
private List<TdApi.Object> values;
private Error error;
private int pos;
private Buffer buffer;
public TdResultList(List<TdApi.Object> values) {
this.values = values;
this.error = null;
if (values == null) throw new NullPointerException("Null message");
}
public TdResultList(TdApi.Error error) {
this.values = null;
this.error = error;
if (error == null) throw new NullPointerException("Null message");
}
public TdResultList(int pos, Buffer buffer) {
this.pos = pos;
this.buffer = buffer;
}
private void tryDecode() {
if (error == null && values == null) {
var value = realCodec.decodeFromWire(pos, buffer);
this.values = value.values;
this.error = value.error;
this.buffer = null;
}
}
public List<TdApi.Object> value() {
tryDecode();
return values;
}
public TdApi.Error error() {
tryDecode();
return error;
}
public boolean succeeded() {
tryDecode();
return error == null && values != null;
}
@Override
public boolean equals(Object o) {
tryDecode();
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TdResultList that = (TdResultList) o;
if (!Objects.equals(values, that.values)) {
return false;
}
return Objects.equals(error, that.error);
}
@Override
public int hashCode() {
tryDecode();
int result = values != null ? values.hashCode() : 0;
result = 31 * result + (error != null ? error.hashCode() : 0);
return result;
}
@Override
public String toString() {
return new StringJoiner(", ", TdResultList.class.getSimpleName() + "[", "]")
.add("values=" + values)
.add("error=" + error)
.toString();
}
}

View File

@ -1,64 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.MessageCodec;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.utils.BufferUtils;
import java.util.ArrayList;
public class TdResultListMessageCodec implements MessageCodec<TdResultList, TdResultList> {
public TdResultListMessageCodec() {
super();
}
@Override
public void encodeToWire(Buffer buffer, TdResultList ts) {
BufferUtils.encode(buffer, os -> {
if (ts.succeeded()) {
os.writeBoolean(true);
var t = ts.value();
os.writeInt(t.size());
for (TdApi.Object t1 : t) {
t1.serialize(os);
}
} else {
os.writeBoolean(false);
ts.error().serialize(os);
}
});
}
@Override
public TdResultList decodeFromWire(int pos, Buffer buffer) {
return BufferUtils.decode(pos, buffer, is -> {
if (is.readBoolean()) {
var size = is.readInt();
ArrayList<TdApi.Object> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
list.add((TdApi.Object) TdApi.Deserializer.deserialize(is));
}
return new TdResultList(list);
} else {
return new TdResultList((Error) TdApi.Deserializer.deserialize(is));
}
});
}
@Override
public TdResultList transform(TdResultList ts) {
return ts;
}
@Override
public String name() {
return "TdOptListCodec";
}
@Override
public byte systemCodecID() {
// Always -1
return -1;
}
}

View File

@ -1,57 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.buffer.Buffer;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.Object;
import it.tdlight.tdlibsession.td.TdResult;
import java.util.StringJoiner;
public class TdResultMessage {
private static final TdResultMessageCodec realCodec = new TdResultMessageCodec();
public TdApi.Object value;
public TdApi.Error cause;
private int pos;
private Buffer buffer;
public TdResultMessage(Object value, Error cause) {
this.value = value;
this.cause = cause;
if (value == null && cause == null) throw new NullPointerException("Null message");
}
public TdResultMessage(int pos, Buffer buffer) {
this.pos = pos;
this.buffer = buffer;
}
public <T extends Object> TdResult<T> toTdResult() {
if (value != null) {
//noinspection unchecked
return TdResult.succeeded((T) value);
} else if (cause != null) {
return TdResult.failed(cause);
} else {
var data = realCodec.decodeFromWire(pos, buffer);
this.value = data.value;
this.cause = data.cause;
this.buffer = null;
if (value != null) {
//noinspection unchecked
return TdResult.succeeded((T) value);
} else {
return TdResult.failed(cause);
}
}
}
@Override
public String toString() {
return new StringJoiner(", ", TdResultMessage.class.getSimpleName() + "[", "]")
.add("value=" + value)
.add("cause=" + cause)
.toString();
}
}

View File

@ -1,59 +0,0 @@
package it.tdlight.tdlibsession.td.middle;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.MessageCodec;
import it.tdlight.jni.TdApi;
import it.tdlight.utils.BufferUtils;
@SuppressWarnings("rawtypes")
public class TdResultMessageCodec implements MessageCodec<TdResultMessage, TdResultMessage> {
private final String codecName;
public TdResultMessageCodec() {
super();
this.codecName = "TdResultCodec";
}
@Override
public void encodeToWire(Buffer buffer, TdResultMessage t) {
BufferUtils.encode(buffer, os -> {
if (t.value != null) {
os.writeBoolean(true);
t.value.serialize(os);
} else {
os.writeBoolean(false);
t.cause.serialize(os);
}
});
}
@Override
public TdResultMessage decodeFromWire(int pos, Buffer buffer) {
return BufferUtils.decode(pos, buffer, is -> {
if (is.readBoolean()) {
return new TdResultMessage(TdApi.Deserializer.deserialize(is), null);
} else {
return new TdResultMessage(null, (TdApi.Error) TdApi.Deserializer.deserialize(is));
}
});
}
@Override
public TdResultMessage transform(TdResultMessage t) {
// If a message is sent *locally* across the event bus.
// This sends message just as is
return t;
}
@Override
public String name() {
return codecName;
}
@Override
public byte systemCodecID() {
// Always -1
return -1;
}
}

View File

@ -1,366 +0,0 @@
package it.tdlight.tdlibsession.td.middle.client;
import io.vertx.core.eventbus.DeliveryOptions;
import io.vertx.core.eventbus.ReplyException;
import io.vertx.core.eventbus.ReplyFailure;
import io.vertx.core.json.JsonObject;
import io.vertx.reactivex.core.Vertx;
import io.vertx.reactivex.core.buffer.Buffer;
import io.vertx.reactivex.core.eventbus.Message;
import io.vertx.reactivex.core.eventbus.MessageConsumer;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.Object;
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
import it.tdlight.tdlibsession.td.ResponseError;
import it.tdlight.tdlibsession.td.TdError;
import it.tdlight.tdlibsession.td.TdResult;
import it.tdlight.tdlibsession.td.middle.TdResultMessage;
import it.tdlight.tdlibsession.td.middle.AsyncTdMiddle;
import it.tdlight.tdlibsession.td.middle.EndSessionMessage;
import it.tdlight.tdlibsession.td.middle.ExecuteObject;
import it.tdlight.tdlibsession.td.middle.StartSessionMessage;
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
import it.tdlight.tdlibsession.td.middle.TdResultList;
import it.tdlight.utils.BinlogAsyncFile;
import it.tdlight.utils.BinlogUtils;
import it.tdlight.utils.MonoUtils;
import java.net.ConnectException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import org.warp.commonutils.locks.LockUtils;
import org.warp.commonutils.log.Logger;
import org.warp.commonutils.log.LoggerFactory;
import reactor.adapter.rxjava.RxJava2Adapter;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Sinks.EmitResult;
import reactor.core.publisher.Sinks.Empty;
import reactor.core.publisher.Sinks.One;
import reactor.core.scheduler.Schedulers;
public class AsyncTdMiddleEventBusClient implements AsyncTdMiddle {
private Logger logger;
public static final byte[] EMPTY = new byte[0];
private final TdClusterManager cluster;
private final DeliveryOptions deliveryOptions;
private final DeliveryOptions deliveryOptionsWithTimeout;
private final DeliveryOptions pingDeliveryOptions;
private final AtomicReference<BinlogAsyncFile> binlog = new AtomicReference<>();
private final AtomicReference<Disposable> pinger = new AtomicReference<>();
private final AtomicReference<MessageConsumer<TdResultList>> updates = new AtomicReference<>();
// This will only result in a successful completion, never completes in other ways
private final Empty<Void> updatesStreamEnd = Sinks.empty();
// This will only result in a crash, never completes in other ways
private final AtomicReference<Throwable> crash = new AtomicReference<>();
// This will only result in a successful completion, never completes in other ways
private final Empty<Void> pingFail = Sinks.empty();
private long botId;
private String botAddress;
private String botAlias;
private boolean local;
public AsyncTdMiddleEventBusClient(TdClusterManager clusterManager) {
this.logger = LoggerFactory.getLogger(AsyncTdMiddleEventBusClient.class);
this.cluster = clusterManager;
this.deliveryOptions = cluster.newDeliveryOpts().setLocalOnly(local);
this.deliveryOptionsWithTimeout = cluster.newDeliveryOpts().setLocalOnly(local).setSendTimeout(30000);
this.pingDeliveryOptions = cluster.newDeliveryOpts().setLocalOnly(local).setSendTimeout(60000);
}
private Mono<AsyncTdMiddleEventBusClient> initializeEb() {
return Mono.just(this);
}
@Override
public Mono<Void> initialize() {
// Do nothing here.
return Mono.empty();
}
public static Mono<AsyncTdMiddle> getAndDeployInstance(TdClusterManager clusterManager,
long botId,
String botAlias,
boolean local,
JsonObject implementationDetails,
Path binlogsArchiveDirectory) {
return new AsyncTdMiddleEventBusClient(clusterManager)
.initializeEb()
.flatMap(instance -> retrieveBinlog(clusterManager.getVertx(), binlogsArchiveDirectory, botId)
.flatMap(binlog -> binlog
.getLastModifiedTime()
.filter(modTime -> modTime == 0)
.doOnNext(v -> LoggerFactory
.getLogger(AsyncTdMiddleEventBusClient.class)
.error("Can't retrieve binlog of bot " + botId + " " + botAlias + ". Creating a new one..."))
.thenReturn(binlog)).<AsyncTdMiddle>flatMap(binlog -> instance
.start(botId, botAlias, local, implementationDetails, binlog)
.thenReturn(instance)
)
.single()
)
.single();
}
/**
*
* @return optional result
*/
public static Mono<BinlogAsyncFile> retrieveBinlog(Vertx vertx, Path binlogsArchiveDirectory, long botId) {
return BinlogUtils.retrieveBinlog(vertx.fileSystem(), binlogsArchiveDirectory.resolve(botId + ".binlog"));
}
private Mono<Void> saveBinlog(Buffer data) {
return Mono.fromSupplier(this.binlog::get).flatMap(binlog -> BinlogUtils.saveBinlog(binlog, data));
}
public Mono<Void> start(long botId,
String botAlias,
boolean local,
JsonObject implementationDetails,
BinlogAsyncFile binlog) {
this.botId = botId;
this.botAlias = botAlias;
this.botAddress = "bots.bot." + this.botId;
this.local = local;
this.logger = LoggerFactory.getLogger(this.botId + " " + botAlias);
return Mono
.fromRunnable(() -> this.binlog.set(binlog))
.then(binlog.getLastModifiedTime())
.zipWith(binlog.readFully().map(Buffer::getDelegate))
.single()
.flatMap(tuple -> {
var binlogLastModifiedTime = tuple.getT1();
var binlogData = tuple.getT2();
var msg = new StartSessionMessage(this.botId,
this.botAlias,
Buffer.newInstance(binlogData),
binlogLastModifiedTime,
implementationDetails
);
Mono<Void> startBotRequest;
if (local) {
startBotRequest = Mono.empty();
} else {
startBotRequest = cluster
.getEventBus()
.<byte[]>rxRequest("bots.start-bot", msg)
.to(RxJava2Adapter::singleToMono)
.doOnSuccess(s -> logger.trace("bots.start-bot returned successfully"))
.doFirst(() -> logger.trace("Requesting bots.start-bot"))
.onErrorMap(ex -> {
if (ex instanceof ReplyException) {
if (((ReplyException) ex).failureType() == ReplyFailure.NO_HANDLERS) {
return new NoClustersAvailableException("Can't start bot "
+ botId + " " + botAlias);
}
}
return ex;
})
.then()
.subscribeOn(Schedulers.boundedElastic());
}
return setupUpdatesListener().then(startBotRequest).then(setupPing());
});
}
private Mono<Void> setupPing() {
// Disable ping on local servers
if (local) {
return Mono.empty();
}
var pingRequest = cluster.getEventBus()
.<byte[]>rxRequest(botAddress + ".ping", EMPTY, pingDeliveryOptions)
.to(RxJava2Adapter::singleToMono)
.doFirst(() -> logger.trace("Requesting ping..."));
return Mono
.fromRunnable(() -> pinger.set(pingRequest
.flatMap(msg -> Mono.fromCallable(msg::body).subscribeOn(Schedulers.boundedElastic()))
.repeatWhen(l -> l.delayElements(Duration.ofSeconds(10)).takeWhile(x -> true))
.doOnNext(s -> logger.trace("PING"))
.then()
.onErrorResume(ex -> {
logger.warn("Ping failed: {}", ex.getMessage());
return Mono.empty();
})
.doOnNext(s -> logger.debug("END PING"))
.then(Mono.fromRunnable(() -> {
while (this.pingFail.tryEmitEmpty() == EmitResult.FAIL_NON_SERIALIZED) {
// 10ms
LockSupport.parkNanos(10000000);
}
}).subscribeOn(Schedulers.boundedElastic()))
.subscribeOn(Schedulers.parallel())
.subscribe())
)
.then()
.doFirst(() -> logger.trace("Setting up ping"))
.doOnSuccess(s -> logger.trace("Ping setup success"))
.subscribeOn(Schedulers.boundedElastic());
}
private Mono<Void> setupUpdatesListener() {
return Mono
.fromRunnable(() -> logger.trace("Setting up updates listener..."))
.then(Mono.<MessageConsumer<TdResultList>>fromSupplier(() -> MessageConsumer
.newInstance(cluster.getEventBus().<TdResultList>consumer(botAddress + ".updates")
.setMaxBufferedMessages(5000)
.getDelegate()
))
)
.flatMap(updateConsumer -> {
// Return when the registration of all the consumers has been done across the cluster
return Mono
.fromRunnable(() -> logger.trace("Emitting updates flux to sink"))
.then(Mono.fromRunnable(() -> {
var previous = this.updates.getAndSet(updateConsumer);
if (previous != null) {
logger.error("Already subscribed a consumer to the updates");
}
}))
.doOnSuccess(s -> logger.trace("Emitted updates flux to sink"))
.doOnSuccess(s -> logger.trace("Waiting to register update consumer across the cluster"))
.doOnSuccess(s -> logger.trace("Registered update consumer across the cluster"));
})
.doOnSuccess(s ->logger.trace("Set up updates listener"))
.then();
}
@SuppressWarnings("Convert2MethodRef")
@Override
public Flux<TdApi.Object> receive() {
// Here the updates will be received
return Mono
.fromRunnable(() -> logger.trace("Called receive() from parent"))
.then(Mono.fromCallable(() -> updates.get()))
.doOnSuccess(s -> logger.trace("Registering updates flux"))
.flatMapMany(updatesMessageConsumer -> MonoUtils.fromMessageConsumer(Mono
.empty()
.doOnSuccess(s -> logger.trace("Sending ready-to-receive"))
.then(cluster.getEventBus().<byte[]>rxRequest(botAddress + ".ready-to-receive",
EMPTY,
deliveryOptionsWithTimeout
).to(RxJava2Adapter::singleToMono))
.doOnSuccess(s -> logger.trace("Sent ready-to-receive, received reply"))
.doOnSuccess(s -> logger.trace("About to read updates flux"))
.then(), updatesMessageConsumer)
)
.takeUntilOther(pingFail.asMono())
.takeUntil(a -> a.succeeded() && a.value().stream().anyMatch(item -> {
if (item.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
return ((UpdateAuthorizationState) item).authorizationState.getConstructor()
== AuthorizationStateClosed.CONSTRUCTOR;
}
return false;
}))
.flatMapSequential(updates -> {
if (updates.succeeded()) {
return Flux.fromIterable(updates.value());
} else {
return Mono.fromCallable(() -> TdResult.failed(updates.error()).orElseThrow());
}
})
.concatMap(update -> interceptUpdate(update))
// Redirect errors to crash sink
.doOnError(error -> crash.compareAndSet(null, error))
.onErrorResume(ex -> {
logger.trace("Absorbing the error, the error has been published using the crash sink", ex);
return Mono.empty();
})
.doOnCancel(() -> {
})
.doFinally(s -> {
var pinger = this.pinger.get();
if (pinger != null) {
pinger.dispose();
}
updatesStreamEnd.tryEmitEmpty();
});
}
private Mono<TdApi.Object> interceptUpdate(Object update) {
logger.trace("Received update {}", update.getClass().getSimpleName());
if (update.getConstructor() == TdApi.UpdateAuthorizationState.CONSTRUCTOR) {
var updateAuthorizationState = (TdApi.UpdateAuthorizationState) update;
if (updateAuthorizationState.authorizationState.getConstructor() == AuthorizationStateClosed.CONSTRUCTOR) {
logger.info("Received AuthorizationStateClosed from tdlib");
var pinger = this.pinger.get();
if (pinger != null) {
pinger.dispose();
}
return cluster.getEventBus()
.<EndSessionMessage>rxRequest(this.botAddress + ".read-binlog", EMPTY)
.to(RxJava2Adapter::singleToMono)
.mapNotNull(Message::body)
.doOnNext(latestBinlog -> logger.info("Received binlog from server. Size: {}",
BinlogUtils.humanReadableByteCountBin(latestBinlog.binlog().length())
))
.flatMap(latestBinlog -> this.saveBinlog(latestBinlog.binlog()))
.doOnSuccess(s -> logger.info("Overwritten binlog from server"))
.doFirst(() -> logger.info("Asking binlog to server"))
.thenReturn(update);
}
}
return Mono.just(update);
}
@Override
public <T extends TdApi.Object> Mono<TdResult<T>> execute(Function<T> request, Duration timeout,
boolean executeSync) {
var req = new ExecuteObject<>(executeSync, request, timeout);
var deliveryOptions = new DeliveryOptions(this.deliveryOptions)
// Timeout + 5s (5 seconds extra are used to wait the graceful server-side timeout response)
.setSendTimeout(timeout.toMillis() + 5000);
var executionMono = cluster.getEventBus()
.<TdResultMessage>rxRequest(botAddress + ".execute", req, deliveryOptions)
.to(RxJava2Adapter::singleToMono)
.onErrorMap(ex -> ResponseError.newResponseError(request, botAlias, ex))
.<TdResult<T>>handle((resp, sink) -> {
if (resp.body() == null) {
var tdError = new TdError(500, "Response is empty");
sink.error(ResponseError.newResponseError(request, botAlias, tdError));
} else {
sink.next(resp.body().toTdResult());
}
})
.doFirst(() -> logger.trace("Executing request {}", request))
.doOnSuccess(s -> logger.trace("Executed request {}", request))
.doOnError(ex -> logger.debug("Failed request {}: {}", req, ex));
return executionMono
.transformDeferred(mono -> {
var crash = this.crash.get();
if (crash != null) {
logger.debug("Failed request {} because the TDLib session was already crashed", request);
return Mono.empty();
} else {
return mono;
}
})
.switchIfEmpty(Mono.error(() -> ResponseError.newResponseError(request, botAlias,
new TdError(500, "The client is closed or the response is empty"))));
}
}

View File

@ -1,13 +0,0 @@
package it.tdlight.tdlibsession.td.middle.client;
public class NoClustersAvailableException extends Throwable {
public NoClustersAvailableException(String error) {
super(error);
}
@Override
public String toString() {
return "No clusters are available. " + this.getMessage();
}
}

View File

@ -1,112 +0,0 @@
package it.tdlight.tdlibsession.td.middle.direct;
import static it.tdlight.tdlibsession.td.middle.server.AsyncTdMiddleEventBusServer.WAIT_DURATION;
import io.reactivex.Completable;
import io.vertx.core.json.JsonObject;
import io.vertx.reactivex.core.AbstractVerticle;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.Object;
import it.tdlight.tdlibsession.td.ResponseError;
import it.tdlight.tdlibsession.td.TdResult;
import it.tdlight.tdlibsession.td.direct.AsyncTdDirectImpl;
import it.tdlight.tdlibsession.td.direct.AsyncTdDirectOptions;
import it.tdlight.tdlibsession.td.direct.TelegramClientFactory;
import it.tdlight.tdlibsession.td.middle.AsyncTdMiddle;
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
import it.tdlight.utils.MonoUtils;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicReference;
import org.warp.commonutils.log.Logger;
import org.warp.commonutils.log.LoggerFactory;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Sinks.Empty;
import reactor.core.scheduler.Schedulers;
public class AsyncTdMiddleDirect extends AbstractVerticle implements AsyncTdMiddle {
private static final Logger logger = LoggerFactory.getLogger(AsyncTdMiddleDirect.class);
private final TelegramClientFactory clientFactory;
protected AsyncTdDirectImpl td;
private String botAddress;
private String botAlias;
private final Empty<Object> closeRequest = Sinks.empty();
public AsyncTdMiddleDirect() {
this.clientFactory = new TelegramClientFactory();
}
public static Mono<AsyncTdMiddle> getAndDeployInstance(TdClusterManager clusterManager,
String botAlias,
String botAddress,
JsonObject implementationDetails) {
var instance = new AsyncTdMiddleDirect();
var options = clusterManager.newDeploymentOpts().setConfig(new JsonObject()
.put("botAlias", botAlias)
.put("botAddress", botAddress)
.put("implementationDetails", implementationDetails));
return clusterManager.getVertx()
.rxDeployVerticle(instance, options)
.as(MonoUtils::toMono)
.doOnNext(_v -> logger.trace("Deployed verticle for bot " + botAlias + ", address: " + botAddress))
.thenReturn(instance);
}
@Override
public Completable rxStart() {
var botAddress = config().getString("botAddress");
if (botAddress == null || botAddress.isEmpty()) {
throw new IllegalArgumentException("botAddress is not set!");
}
this.botAddress = botAddress;
var botAlias = config().getString("botAlias");
if (botAlias == null || botAlias.isEmpty()) {
throw new IllegalArgumentException("botAlias is not set!");
}
this.botAlias = botAlias;
var implementationDetails = config().getJsonObject("implementationDetails");
if (implementationDetails == null) {
throw new IllegalArgumentException("implementationDetails is not set!");
}
this.td = new AsyncTdDirectImpl(clientFactory, implementationDetails, botAlias);
return Completable.complete();
}
@Override
public Completable rxStop() {
return Completable.fromRunnable(closeRequest::tryEmitEmpty);
}
@Override
public Mono<Void> initialize() {
return td.initialize();
}
@Override
public Flux<TdApi.Object> receive() {
return td
.receive(new AsyncTdDirectOptions(WAIT_DURATION, 100))
.takeUntilOther(closeRequest.asMono())
.doOnNext(s -> logger.trace("Received update from tdlib: {}", s.getClass().getSimpleName()))
.doOnError(ex -> logger.info("TdMiddle verticle error", ex))
.doOnTerminate(() -> logger.debug("TdMiddle verticle stopped"))
.subscribeOn(Schedulers.boundedElastic());
}
@Override
public <T extends Object> Mono<TdResult<T>> execute(Function<T> requestFunction,
Duration timeout,
boolean executeDirectly) {
return td
.<T>execute(requestFunction, timeout, executeDirectly)
.onErrorMap(error -> ResponseError.newResponseError(requestFunction, botAlias, error));
}
}

View File

@ -1,100 +0,0 @@
package it.tdlight.tdlibsession.td.middle.direct;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.reactivex.core.Vertx;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.Object;
import it.tdlight.tdlibsession.td.TdResult;
import it.tdlight.tdlibsession.td.middle.AsyncTdMiddle;
import it.tdlight.tdlibsession.td.middle.TdClusterManager;
import it.tdlight.tdlibsession.td.middle.client.AsyncTdMiddleEventBusClient;
import it.tdlight.tdlibsession.td.middle.server.AsyncTdMiddleEventBusServer;
import it.tdlight.utils.MonoUtils;
import java.nio.file.Path;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicReference;
import org.warp.commonutils.error.InitializationException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Sinks.One;
public class AsyncTdMiddleLocal implements AsyncTdMiddle {
private final TdClusterManager masterClusterManager;
private final String botAlias;
private final int botId;
private final DeploymentOptions deploymentOptions;
private final Vertx vertx;
private final AsyncTdMiddleEventBusServer srv;
private final AtomicReference<AsyncTdMiddle> cli = new AtomicReference<>(null);
private final AtomicReference<Throwable> startError = new AtomicReference<>(null);
private final JsonObject implementationDetails;
public AsyncTdMiddleLocal(TdClusterManager masterClusterManager,
String botAlias,
int botId,
JsonObject implementationDetails) {
this.masterClusterManager = masterClusterManager;
this.botAlias = botAlias;
this.botId = botId;
this.implementationDetails = implementationDetails;
this.vertx = masterClusterManager.getVertx();
this.deploymentOptions = masterClusterManager
.newDeploymentOpts()
.setConfig(new JsonObject()
.put("botId", botId)
.put("botAlias", botAlias)
.put("local", true)
.put("implementationDetails", implementationDetails)
);
this.srv = new AsyncTdMiddleEventBusServer();
}
public Mono<AsyncTdMiddleLocal> start() {
return vertx
.rxDeployVerticle(srv, deploymentOptions).as(MonoUtils::toMono)
.single()
.then(Mono.fromSupplier(() -> new AsyncTdMiddleEventBusClient(masterClusterManager)))
.zipWith(AsyncTdMiddleEventBusClient.retrieveBinlog(vertx, Path.of("binlogs"), botId))
.flatMap(tuple -> tuple
.getT1()
.start(botId, botAlias, true, implementationDetails, tuple.getT2())
.thenReturn(tuple.getT1()))
.onErrorMap(InitializationException::new)
.doOnNext(this.cli::set)
.doOnError(this.startError::set)
.thenReturn(this);
}
@Override
public Mono<Void> initialize() {
var startError = this.startError.get();
if (startError != null) {
return Mono.error(startError);
}
return Mono.fromCallable(cli::get).single().flatMap(AsyncTdMiddle::initialize);
}
@Override
public Flux<TdApi.Object> receive() {
var startError = this.startError.get();
if (startError != null) {
return Flux.error(startError);
}
return Mono.fromCallable(cli::get).single().flatMapMany(AsyncTdMiddle::receive);
}
@Override
public <T extends Object> Mono<TdResult<T>> execute(Function<T> request, Duration timeout, boolean executeDirectly) {
var startError = this.startError.get();
if (startError != null) {
return Mono.error(startError);
}
return Mono.fromCallable(cli::get).single().flatMap(c -> c.execute(request, timeout, executeDirectly));
}
}

View File

@ -1,402 +0,0 @@
package it.tdlight.tdlibsession.td.middle.server;
import io.reactivex.Completable;
import io.reactivex.processors.BehaviorProcessor;
import io.vertx.core.eventbus.DeliveryOptions;
import io.vertx.core.eventbus.ReplyException;
import io.vertx.core.eventbus.ReplyFailure;
import io.vertx.core.streams.Pipe;
import io.vertx.core.streams.Pump;
import io.vertx.ext.reactivestreams.impl.ReactiveWriteStreamImpl;
import io.vertx.reactivex.RxHelper;
import io.vertx.reactivex.WriteStreamObserver;
import io.vertx.reactivex.core.AbstractVerticle;
import io.vertx.reactivex.core.eventbus.Message;
import io.vertx.reactivex.core.eventbus.MessageConsumer;
import io.vertx.reactivex.core.eventbus.MessageProducer;
import io.vertx.reactivex.core.streams.WriteStream;
import io.vertx.reactivex.impl.FlowableReadStream;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AuthorizationStateClosed;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.SetTdlibParameters;
import it.tdlight.jni.TdApi.Update;
import it.tdlight.jni.TdApi.UpdateAuthorizationState;
import it.tdlight.tdlibsession.remoteclient.TDLibRemoteClient;
import it.tdlight.tdlibsession.td.TdError;
import it.tdlight.tdlibsession.td.direct.AsyncTdDirectImpl;
import it.tdlight.tdlibsession.td.direct.AsyncTdDirectOptions;
import it.tdlight.tdlibsession.td.direct.TelegramClientFactory;
import it.tdlight.tdlibsession.td.middle.ExecuteObject;
import it.tdlight.tdlibsession.td.middle.TdResultList;
import it.tdlight.tdlibsession.td.middle.TdResultListMessageCodec;
import it.tdlight.tdlibsession.td.middle.TdResultMessage;
import it.tdlight.utils.BinlogUtils;
import java.net.ConnectException;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.warp.commonutils.log.Logger;
import org.warp.commonutils.log.LoggerFactory;
import reactor.adapter.rxjava.RxJava2Adapter;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.function.Tuples;
public class AsyncTdMiddleEventBusServer extends AbstractVerticle {
// Static values
protected static final Logger logger = LoggerFactory.getLogger("TdMiddleServer");
public static final byte[] EMPTY = new byte[0];
public static final Duration WAIT_DURATION = Duration.ofSeconds(1);
// Values configured from constructor
private final AsyncTdDirectOptions tdOptions;
private final TelegramClientFactory clientFactory;
// Variables configured by the user at startup
private final AtomicReference<Integer> botId = new AtomicReference<>();
private final AtomicReference<String> botAddress = new AtomicReference<>();
private final AtomicReference<String> botAlias = new AtomicReference<>();
// Variables configured at startup
private final AtomicReference<AsyncTdDirectImpl> td = new AtomicReference<>();
private final AtomicReference<Disposable> executeConsumer = new AtomicReference<>();
private final AtomicReference<Disposable> readBinlogConsumer = new AtomicReference<>();
private final AtomicReference<Disposable> readyToReceiveConsumer = new AtomicReference<>();
private final AtomicReference<Disposable> pingConsumer = new AtomicReference<>();
private final AtomicReference<Disposable> clusterPropagationWaiter = new AtomicReference<>();
private final AtomicReference<Flux<Void>> pipeFlux = new AtomicReference<>();
public AsyncTdMiddleEventBusServer() {
this.tdOptions = new AsyncTdDirectOptions(WAIT_DURATION, 100);
this.clientFactory = new TelegramClientFactory();
}
@Override
public Completable rxStart() {
return Mono
.fromCallable(() -> {
logger.trace("Stating verticle");
System.setProperty("it.tdlight.enableShutdownHooks", "false");
var botId = config().getInteger("botId");
if (botId == null || botId <= 0) {
throw new IllegalArgumentException("botId is not set!");
}
this.botId.set(botId);
var botAddress = "bots.bot." + botId;
this.botAddress.set(botAddress);
var botAlias = config().getString("botAlias");
if (botAlias == null || botAlias.isEmpty()) {
throw new IllegalArgumentException("botAlias is not set!");
}
this.botAlias.set(botAlias);
var local = config().getBoolean("local");
if (local == null) {
throw new IllegalArgumentException("local is not set!");
}
var implementationDetails = config().getJsonObject("implementationDetails");
if (implementationDetails == null) {
throw new IllegalArgumentException("implementationDetails is not set!");
}
var td = new AsyncTdDirectImpl(clientFactory, implementationDetails, botAlias);
this.td.set(td);
return new OnSuccessfulStartRequestInfo(td, botAddress, botAlias, botId, local);
})
.flatMap(r -> onSuccessfulStartRequest(r.td, r.botAddress, r.botAlias, r.botId, r.local))
.doOnSuccess(s -> logger.trace("Started verticle"))
.as(RxJava2Adapter::monoToCompletable);
}
private static class OnSuccessfulStartRequestInfo {
public final AsyncTdDirectImpl td;
public final String botAddress;
public final String botAlias;
public final int botId;
public final boolean local;
public OnSuccessfulStartRequestInfo(AsyncTdDirectImpl td, String botAddress, String botAlias, int botId,
boolean local) {
this.td = td;
this.botAddress = botAddress;
this.botAlias = botAlias;
this.botId = botId;
this.local = local;
}
}
private Mono<Void> onSuccessfulStartRequest(AsyncTdDirectImpl td, String botAddress, String botAlias, int botId,
boolean local) {
return td
.initialize()
.then(this.pipe(td, botAddress, local))
.then(this.listen(td, botAddress, botId, local))
.doOnSuccess(s -> logger.info("Deploy and start of bot \"" + botAlias + "\": ✅ Succeeded"))
.doOnError(ex -> logger.error("Deploy and start of bot \"" + botAlias + "\": ❌ Failed", ex));
}
private Mono<Void> listen(AsyncTdDirectImpl td, String botAddress, int botId, boolean local) {
return Mono.create(registrationSink -> {
logger.trace("Preparing listeners");
MessageConsumer<ExecuteObject<?>> executeConsumer = vertx.eventBus().consumer(botAddress + ".execute");
this.executeConsumer.set(executeConsumer
.toFlowable()
.to(RxJava2Adapter::flowableToFlux)
.flatMap(msg -> {
var body = msg.body();
var request = overrideRequest(body.getRequest(), botId);
var timeout = body.getTimeout();
if (logger.isTraceEnabled()) {
logger.trace("Received execute request {}", request);
}
return td
.execute(request, timeout, body.isExecuteDirectly())
.single()
.doOnSuccess(s -> logger.trace("Executed successfully. Request was {}", request))
.onErrorResume(ex -> Mono.fromRunnable(() -> msg.fail(500, ex.getLocalizedMessage())))
.map(response -> {
var replyOpts = new DeliveryOptions().setLocalOnly(local);
var replyValue = new TdResultMessage(response.result(), response.cause());
try {
logger.trace("Replying with success response. Request was {}", request);
msg.reply(replyValue, replyOpts);
return response;
} catch (Exception ex) {
logger.debug("Replying with error response: {}. Request was {}", ex.getLocalizedMessage(), request);
msg.fail(500, ex.getLocalizedMessage());
throw ex;
}
});
})
.then()
.subscribeOn(Schedulers.boundedElastic())
.subscribe(v -> {},
ex -> logger.error("Fatal error when processing an execute request."
+ " Can't process further requests since the subscription has been broken", ex),
() -> logger.trace("Finished handling execute requests")
));
MessageConsumer<byte[]> readBinlogConsumer = vertx.eventBus().consumer(botAddress + ".read-binlog");
this.readBinlogConsumer.set(BinlogUtils
.readBinlogConsumer(vertx, readBinlogConsumer, botId, local)
.subscribeOn(Schedulers.boundedElastic())
.subscribe(v -> {}, ex -> logger.error("Error when processing a read-binlog request", ex)));
MessageConsumer<byte[]> readyToReceiveConsumer = vertx.eventBus().consumer(botAddress + ".ready-to-receive");
// Pipe the data
this.readyToReceiveConsumer.set(readyToReceiveConsumer
.toFlowable()
.to(RxJava2Adapter::flowableToFlux)
.take(1, true)
.single()
.doOnNext(s -> logger.trace("Received ready-to-receive request from client"))
.map(msg -> Tuples.of(msg, Objects.requireNonNull(pipeFlux.get(), "PipeFlux is empty")))
.doOnError(ex -> logger.error("Error when processing a ready-to-receive request", ex))
.doOnNext(s -> logger.trace("Replying to ready-to-receive request"))
.flatMapMany(tuple -> {
var opts = new DeliveryOptions().setLocalOnly(local).setSendTimeout(Duration.ofSeconds(10).toMillis());
tuple.getT1().reply(EMPTY, opts);
logger.trace("Replied to ready-to-receive");
logger.trace("Start piping data");
// Start piping the data
return tuple.getT2().doOnSubscribe(s -> logger.trace("Subscribed to updates pipe"));
})
.then()
.doOnSuccess(s -> logger.trace("Finished handling ready-to-receive requests (updates pipe ended)"))
.subscribeOn(Schedulers.boundedElastic())
// Don't handle errors here. Handle them in pipeFlux
.subscribe(v -> {}));
MessageConsumer<byte[]> pingConsumer = vertx.eventBus().consumer(botAddress + ".ping");
this.pingConsumer.set(pingConsumer
.toFlowable()
.to(RxJava2Adapter::flowableToFlux)
.doOnNext(msg -> {
var opts = new DeliveryOptions().setLocalOnly(local).setSendTimeout(Duration.ofSeconds(10).toMillis());
msg.reply(EMPTY, opts);
})
.subscribeOn(Schedulers.boundedElastic())
.subscribe(unused -> logger.trace("Finished handling ping requests"),
ex -> logger.error("Error when processing a ping request", ex)
));
var executorPropagated = executeConsumer.rxCompletionHandler().to(RxJava2Adapter::completableToMono);
var readyToReceivePropagated = executeConsumer.rxCompletionHandler().to(RxJava2Adapter::completableToMono);
var readBinLogPropagated = executeConsumer.rxCompletionHandler().to(RxJava2Adapter::completableToMono);
var pingPropagated = executeConsumer.rxCompletionHandler().to(RxJava2Adapter::completableToMono);
var allPropagated = Mono.when(executorPropagated, readyToReceivePropagated, readBinLogPropagated, pingPropagated);
this.clusterPropagationWaiter.set(allPropagated
.doOnSuccess(s -> logger.trace("Finished preparing listeners"))
.subscribeOn(Schedulers.boundedElastic())
.subscribe(v -> {}, registrationSink::error, registrationSink::success));
});
}
/**
* Override some requests
*/
private <T extends TdApi.Object> Function<T> overrideRequest(Function<T> request, int botId) {
if (request.getConstructor() == SetTdlibParameters.CONSTRUCTOR) {
// Fix session directory locations
var setTdlibParamsObj = (SetTdlibParameters) request;
setTdlibParamsObj.parameters.databaseDirectory = TDLibRemoteClient.getSessionDirectory(botId).toString();
setTdlibParamsObj.parameters.filesDirectory = TDLibRemoteClient.getMediaDirectory(botId).toString();
}
return request;
}
@Override
public Completable rxStop() {
return Mono
.fromRunnable(() -> {
logger.info("Undeploy of bot \"" + botAlias.get() + "\": stopping");
var executeConsumer = this.executeConsumer.get();
if (executeConsumer != null) {
executeConsumer.dispose();
logger.trace("Unregistered execute consumer");
}
var pingConsumer = this.pingConsumer.get();
if (pingConsumer != null) {
pingConsumer.dispose();
}
var readBinlogConsumer = this.readBinlogConsumer.getAndSet(null);
Schedulers.boundedElastic().schedule(() -> {
if (readBinlogConsumer != null) {
readBinlogConsumer.dispose();
}
}, 10, TimeUnit.MINUTES);
var readyToReceiveConsumer = this.readyToReceiveConsumer.get();
if (readyToReceiveConsumer != null) {
readyToReceiveConsumer.dispose();
}
var clusterPropagationWaiter = this.clusterPropagationWaiter.get();
if (clusterPropagationWaiter != null) {
clusterPropagationWaiter.dispose();
}
})
.doOnError(ex -> logger.error("Undeploy of bot \"" + botAlias.get() + "\": stop failed", ex))
.doOnTerminate(() -> logger.info("Undeploy of bot \"" + botAlias.get() + "\": stopped"))
.as(RxJava2Adapter::monoToCompletable);
}
private Mono<Void> pipe(AsyncTdDirectImpl td, String botAddress, boolean local) {
logger.trace("Preparing to pipe requests");
Flux<TdResultList> updatesFlux = td
.receive(tdOptions)
.takeUntil(item -> {
if (item instanceof Update) {
var tdUpdate = (Update) item;
if (tdUpdate.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
var updateAuthorizationState = (UpdateAuthorizationState) tdUpdate;
return updateAuthorizationState.authorizationState.getConstructor()
== AuthorizationStateClosed.CONSTRUCTOR;
}
} else
return item instanceof Error;
return false;
})
.flatMap(update -> Mono.fromCallable(() -> {
if (update.getConstructor() == TdApi.Error.CONSTRUCTOR) {
var error = (Error) update;
throw new TdError(error.code, error.message);
} else {
return update;
}
}))
.limitRate(Math.max(1, tdOptions.getEventsSize()))
//.transform(normal -> new BufferTimeOutPublisher<>(normal,Math.max(1, tdOptions.getEventsSize()),
// local ? Duration.ofMillis(1) : Duration.ofMillis(100), false))
//.bufferTimeout(Math.max(1, tdOptions.getEventsSize()), local ? Duration.ofMillis(1) : Duration.ofMillis(100))
.map(List::of)
.limitRate(Math.max(1, tdOptions.getEventsSize()))
.map(TdResultList::new);
var fluxCodec = new TdResultListMessageCodec();
var opts = new DeliveryOptions()
.setLocalOnly(local)
.setSendTimeout(Duration.ofSeconds(30).toMillis())
.setCodecName(fluxCodec.name());
MessageProducer<TdResultList> updatesSender = vertx
.eventBus()
.sender(botAddress + ".updates", opts);
var pipeFlux = updatesFlux
.concatMap(updatesList -> updatesSender
.rxWrite(updatesList)
.to(RxJava2Adapter::completableToMono)
.thenReturn(updatesList)
)
.concatMap(updatesList -> Flux
.fromIterable(updatesList.value())
.concatMap(item -> {
if (item instanceof Update) {
var tdUpdate = (Update) item;
if (tdUpdate.getConstructor() == UpdateAuthorizationState.CONSTRUCTOR) {
var tdUpdateAuthorizationState = (UpdateAuthorizationState) tdUpdate;
if (tdUpdateAuthorizationState.authorizationState.getConstructor()
== AuthorizationStateClosed.CONSTRUCTOR) {
logger.info("Undeploying after receiving AuthorizationStateClosed");
return rxStop().to(RxJava2Adapter::completableToMono).thenReturn(item);
}
}
} else if (item instanceof Error) {
// An error in updates means that a fatal error occurred
logger.info("Undeploying after receiving a fatal error");
return rxStop().to(RxJava2Adapter::completableToMono).thenReturn(item);
}
return Mono.just(item);
})
.then()
)
.doOnTerminate(() -> updatesSender.close(h -> {
if (h.failed()) {
logger.error("Failed to close \"updates\" message sender");
}
}))
.onErrorResume(ex -> {
boolean printDefaultException = true;
if (ex instanceof ReplyException) {
ReplyException replyException = (ReplyException) ex;
if (replyException.failureCode() == -1 && replyException.failureType() == ReplyFailure.NO_HANDLERS) {
logger.warn("Undeploying, the flux has been terminated because no more"
+ " handlers are available on the event bus. {}", replyException.getMessage());
printDefaultException = false;
}
} else if (ex instanceof ConnectException || ex instanceof java.nio.channels.ClosedChannelException) {
logger.warn("Undeploying, the flux has been terminated because the consumer"
+ " disconnected from the event bus. {}", ex.getMessage());
printDefaultException = false;
}
if (printDefaultException) {
logger.warn("Undeploying after a fatal error in a served flux", ex);
}
return td
.execute(new TdApi.Close(), Duration.ofDays(1), false)
.doOnError(ex2 -> logger.error("Unexpected error", ex2))
.doOnSuccess(s -> logger.debug("Emergency Close() signal has been sent successfully"))
.then(rxStop().to(RxJava2Adapter::completableToMono));
});
return Mono.fromRunnable(() -> {
this.pipeFlux.set(pipeFlux);
logger.trace("Prepared piping requests successfully");
});
}
}

View File

@ -1,19 +0,0 @@
package it.tdlight.tdlibsession.td.middle.server;
import java.util.concurrent.atomic.AtomicLong;
import reactor.core.publisher.Mono;
public class RequestId {
public static Mono<Long> create() {
AtomicLong _requestId = new AtomicLong(1);
return Mono.fromCallable(() -> _requestId.updateAndGet(n -> {
if (n > Long.MAX_VALUE - 100) {
return 1;
} else {
return n + 1;
}
}));
}
}

View File

@ -1,30 +0,0 @@
package it.tdlight.tdlibsession.td.middle.server;
import io.vertx.core.Promise;
import it.tdlight.jni.TdApi.Object;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.Async.Execute;
import org.jetbrains.annotations.Async.Schedule;
public class RequestIdToReplyAddress {
private final ConcurrentHashMap<Long, Promise<Object>> reqIdToReplyAddress = new ConcurrentHashMap<>();;
public RequestIdToReplyAddress() {
}
public void schedule(@Schedule Long requestId, Promise<Object> replyPromise) {
reqIdToReplyAddress.put(requestId, replyPromise);
}
public void failed(@Execute Long requestId, Promise<Object> replyPromise) {
reqIdToReplyAddress.remove(requestId, replyPromise);
}
public void complete(@Execute Long id, Object item) {
var replyPromise = reqIdToReplyAddress.remove(id);
Objects.requireNonNull(replyPromise, () -> "Reply promise must be not empty");
replyPromise.complete(item);
}
}

View File

@ -1,88 +0,0 @@
package it.tdlight.utils;
import io.vertx.core.file.OpenOptions;
import io.vertx.reactivex.core.buffer.Buffer;
import io.vertx.reactivex.core.file.AsyncFile;
import io.vertx.reactivex.core.file.FileProps;
import io.vertx.reactivex.core.file.FileSystem;
import org.warp.commonutils.log.Logger;
import org.warp.commonutils.log.LoggerFactory;
import reactor.core.publisher.Mono;
public class BinlogAsyncFile {
private static final Logger logger = LoggerFactory.getLogger(BinlogAsyncFile.class);
private final FileSystem filesystem;
private final String path;
private final OpenOptions openOptions;
public BinlogAsyncFile(FileSystem fileSystem, String path) {
this.filesystem = fileSystem;
this.path = path;
this.openOptions = new OpenOptions().setWrite(true).setRead(true).setCreate(false).setDsync(true);
}
private Mono<AsyncFile> openRW() {
return filesystem.rxOpen(path, openOptions).as(MonoUtils::toMono);
}
public Mono<Buffer> readFully() {
return openRW()
.flatMap(asyncFile -> filesystem
.rxProps(path)
.map(props -> (int) props.size())
.as(MonoUtils::toMono)
.flatMap(size -> {
var buf = Buffer.buffer(size);
logger.debug("Reading binlog from disk. Size: " + BinlogUtils.humanReadableByteCountBin(size));
return asyncFile.rxRead(buf, 0, 0, size).as(MonoUtils::toMono).thenReturn(buf);
})
);
}
public Mono<byte[]> readFullyBytes() {
return this.readFully().map(Buffer::getBytes);
}
public Mono<Void> overwrite(Buffer newData) {
return openRW().flatMap(asyncFile -> this
.getSize()
.doOnNext(size -> logger.debug("Preparing to overwrite binlog. Initial size: " + BinlogUtils.humanReadableByteCountBin(size)))
.then(asyncFile.rxWrite(newData, 0)
.andThen(asyncFile.rxFlush())
.andThen(filesystem.rxTruncate(path, newData.length()))
.as(MonoUtils::toMono)
)
.then(getSize())
.doOnNext(size -> logger.debug("Overwritten binlog. Final size: " + BinlogUtils.humanReadableByteCountBin(size)))
.then()
);
}
public Mono<Void> overwrite(byte[] newData) {
return this.overwrite(Buffer.buffer(newData));
}
public FileSystem getFilesystem() {
return filesystem;
}
public String getPath() {
return path;
}
public Mono<Long> getLastModifiedTime() {
return filesystem
.rxProps(path)
.map(fileProps -> fileProps.size() == 0 ? 0 : fileProps.lastModifiedTime())
.as(MonoUtils::toMono);
}
public Mono<Long> getSize() {
return filesystem
.rxProps(path)
.map(FileProps::size)
.as(MonoUtils::toMono);
}
}

View File

@ -1,128 +0,0 @@
package it.tdlight.utils;
import io.vertx.core.eventbus.DeliveryOptions;
import io.vertx.core.file.OpenOptions;
import io.vertx.reactivex.core.Vertx;
import io.vertx.reactivex.core.buffer.Buffer;
import io.vertx.reactivex.core.eventbus.Message;
import io.vertx.reactivex.core.eventbus.MessageConsumer;
import io.vertx.reactivex.core.file.FileSystem;
import it.tdlight.tdlibsession.remoteclient.TDLibRemoteClient;
import it.tdlight.tdlibsession.td.middle.EndSessionMessage;
import java.nio.file.Path;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import org.warp.commonutils.log.Logger;
import org.warp.commonutils.log.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
public class BinlogUtils {
private static final Logger logger = LoggerFactory.getLogger(BinlogUtils.class);
public static Mono<BinlogAsyncFile> retrieveBinlog(FileSystem vertxFilesystem, Path binlogPath) {
var path = binlogPath.toString();
return vertxFilesystem
// Create file if not exist to avoid errors
.rxExists(path).filter(exists -> exists).as(MonoUtils::toMono)
.switchIfEmpty(Mono.defer(() -> vertxFilesystem.rxMkdirs(binlogPath.getParent().toString()).as(MonoUtils::toMono))
.then(vertxFilesystem.rxCreateFile(path).as(MonoUtils::toMono))
.thenReturn(true)
)
// Open file
.map(x -> new BinlogAsyncFile(vertxFilesystem, path))
.single();
}
public static Mono<Void> saveBinlog(BinlogAsyncFile binlog, Buffer data) {
return binlog.overwrite(data);
}
public static Mono<Void> chooseBinlog(FileSystem vertxFilesystem,
Path binlogPath,
Buffer remoteBinlog,
long remoteBinlogDate) {
var path = binlogPath.toString();
return retrieveBinlog(vertxFilesystem, binlogPath)
.flatMap(binlog -> Mono
.just(binlog)
.zipWith(binlog.getLastModifiedTime())
)
.doOnSuccess(s -> logger.info("Local binlog: " + binlogPath + ". Local date: " + Instant.ofEpochMilli(s == null ? 0 : s.getT2()).atZone(ZoneOffset.UTC).toString() + " Remote date: " + Instant.ofEpochMilli(remoteBinlogDate).atZone(ZoneOffset.UTC).toString()))
// Files older than the remote file will be overwritten
.filter(tuple -> tuple.getT2() >= remoteBinlogDate)
.doOnNext(v -> logger.info("Using local binlog: " + binlogPath))
.map(Tuple2::getT1)
.switchIfEmpty(Mono.defer(() -> Mono.fromRunnable(() -> logger.info("Using remote binlog. Overwriting " + binlogPath)))
.then(vertxFilesystem.rxWriteFile(path, remoteBinlog.copy()).as(MonoUtils::toMono))
.then(retrieveBinlog(vertxFilesystem, binlogPath))
)
.single()
.then();
}
public static Mono<Void> cleanSessionPath(FileSystem vertxFilesystem,
Path binlogPath,
Path sessionPath,
Path mediaPath) {
return vertxFilesystem
.rxReadFile(binlogPath.toString()).as(MonoUtils::toMono)
.flatMap(buffer -> vertxFilesystem
.rxReadDir(sessionPath.toString(), "^(?!td.binlog$).*").as(MonoUtils::toMono)
.flatMapIterable(list -> list)
.doOnNext(file -> logger.debug("Deleting session file {}", file))
.flatMap(file -> vertxFilesystem.rxDeleteRecursive(file, true).as(MonoUtils::toMono))
.then(vertxFilesystem.rxReadDir(mediaPath.toString(), "^(?!td.binlog$).*").as(MonoUtils::toMono))
.flatMapIterable(list -> list)
.doOnNext(file -> logger.debug("Deleting media file {}", file))
.flatMap(file -> vertxFilesystem.rxDeleteRecursive(file, true).as(MonoUtils::toMono))
.onErrorResume(ex -> Mono.empty())
.then()
);
}
public static String humanReadableByteCountBin(long bytes) {
long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes);
if (absB < 1024) {
return bytes + " B";
}
long value = absB;
CharacterIterator ci = new StringCharacterIterator("KMGTPE");
for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) {
value >>= 10;
ci.next();
}
value *= Long.signum(bytes);
return String.format("%.1f %ciB", value / 1024.0, ci.current());
}
public static Mono<Void> readBinlogConsumer(Vertx vertx,
MessageConsumer<byte[]> readBinlogConsumer,
int botId,
boolean local) {
return Flux
.<Message<byte[]>>create(sink -> {
readBinlogConsumer.handler(sink::next);
readBinlogConsumer.endHandler(h -> sink.complete());
})
.flatMapSequential(req -> BinlogUtils
.retrieveBinlog(vertx.fileSystem(), TDLibRemoteClient.getSessionBinlogDirectory(botId))
.flatMap(BinlogAsyncFile::readFully)
.map(Buffer::copy)
.single()
.map(binlog -> Tuples.of(req, binlog))
)
.doOnNext(tuple -> {
var opts = new DeliveryOptions().setLocalOnly(local).setSendTimeout(Duration.ofSeconds(10).toMillis());
tuple.getT1().reply(new EndSessionMessage(botId, tuple.getT2()), opts);
})
.then();
}
}

View File

@ -1,178 +0,0 @@
package it.tdlight.utils;
/** Based on:
* https://gist.github.com/glandais-sparklane/e38834aa9df0c56f23e2d8d2e6899c78
*/
import java.time.Duration;
import org.jetbrains.annotations.NotNull;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import reactor.core.CoreSubscriber;
@SuppressWarnings("ReactiveStreamsPublisherImplementation")
public class BufferTimeOutPublisher<T> implements Publisher<List<T>> {
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private final Publisher<T> source;
private final int size;
private final long duration;
private final boolean discardOnError;
public BufferTimeOutPublisher(Publisher<T> source, int size, Duration duration, boolean discardOnError) {
this.source = source;
this.size = size;
this.duration = duration.toMillis();
this.discardOnError = discardOnError;
}
@Override
public void subscribe(Subscriber<? super List<T>> subscriber) {
subscriber.onSubscribe(new BufferTimeOutSubscription<T>(source, subscriber, size, duration, discardOnError));
}
protected static class BufferTimeOutSubscription<T> implements Subscription, CoreSubscriber<T> {
private final Subscriber<? super List<T>> subscriber;
private final int size;
private final long duration;
private final boolean discardOnError;
private Subscription subscription;
private final ReentrantLock lock = new ReentrantLock();
private List<T> buffer;
private ScheduledFuture<?> scheduledFuture;
private long downstreamRequests = 0;
private long downstreamTransmit = 0;
private long upstreamRequests = 0;
private long upstreamTransmit = 0;
private boolean upstreamCompleted = false;
public BufferTimeOutSubscription(Publisher<T> source,
Subscriber<? super List<T>> subscriber,
int size,
long duration,
boolean discardOnError) {
this.subscriber = subscriber;
this.size = size;
this.duration = duration;
this.discardOnError = discardOnError;
this.buffer = new ArrayList<>(size);
source.subscribe(this);
}
// downstream
@Override
public void request(long n) {
lock.lock();
downstreamRequests = downstreamRequests + n;
checkSend();
long downstreamMax = (downstreamRequests - downstreamTransmit) * size;
long upstreamRequested = upstreamRequests - upstreamTransmit;
long toRequest = downstreamMax - upstreamRequested;
if (toRequest > 0) {
subscription.request(toRequest);
upstreamRequests = upstreamRequests + toRequest;
}
lock.unlock();
}
@Override
public void cancel() {
subscription.cancel();
}
// upstream
@Override
public void onSubscribe(@NotNull Subscription s) {
this.subscription = s;
scheduledFuture = EXECUTOR.scheduleAtFixedRate(this::timeout, 0, this.duration, TimeUnit.MILLISECONDS);
}
private void timeout() {
checkSend();
}
private void checkSend() {
lock.lock();
if (!this.buffer.isEmpty() && downstreamRequests > downstreamTransmit) {
List<T> output = prepareOutput();
subscriber.onNext(output);
downstreamTransmit++;
if (!this.buffer.isEmpty()) {
checkSend();
}
}
if (upstreamCompleted && downstreamRequests > downstreamTransmit) {
scheduledFuture.cancel(false);
subscriber.onComplete();
}
lock.unlock();
}
private List<T> prepareOutput() {
if (this.buffer.size() > size) {
List<T> output = new ArrayList<>(this.buffer.subList(0, size));
this.buffer = new ArrayList<>(this.buffer.subList(size, this.buffer.size()));
return output;
} else {
List<T> output = this.buffer;
this.buffer = new ArrayList<>(size);
return output;
}
}
@Override
public void onNext(T t) {
lock.lock();
this.buffer.add(t);
upstreamTransmit++;
if (this.buffer.size() == size) {
checkSend();
}
lock.unlock();
}
@Override
public void onError(Throwable t) {
if (discardOnError) {
scheduledFuture.cancel(false);
subscriber.onError(t);
} else {
lock.lock();
try {
checkSend();
scheduledFuture.cancel(false);
subscriber.onError(t);
} finally {
lock.unlock();
}
}
}
@Override
public void onComplete() {
lock.lock();
upstreamCompleted = true;
checkSend();
lock.unlock();
}
}
}

View File

@ -1,94 +0,0 @@
package it.tdlight.utils;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.ByteBufOutputStream;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.buffer.impl.BufferImpl;
import java.io.IOException;
import org.apache.commons.lang3.SerializationException;
public class BufferUtils {
private static final int CHUNK_SIZE = 8192;
public static void writeBuf(ByteBufOutputStream os, io.vertx.reactivex.core.buffer.Buffer dataToWrite)
throws IOException {
var len = dataToWrite.length();
os.writeInt(len);
byte[] part = new byte[CHUNK_SIZE];
for (int i = 0; i < len; i += CHUNK_SIZE) {
var end = Math.min(i + CHUNK_SIZE, len);
dataToWrite.getBytes(i, end, part, 0);
os.write(part, 0, end - i);
}
}
public static void writeBuf(ByteBufOutputStream os, io.vertx.core.buffer.Buffer dataToWrite) throws IOException {
var len = dataToWrite.length();
os.writeInt(len);
byte[] part = new byte[CHUNK_SIZE];
for (int i = 0; i < len; i += CHUNK_SIZE) {
var end = Math.min(i + CHUNK_SIZE, len);
dataToWrite.getBytes(i, end, part, 0);
os.write(part, 0, end - i);
}
}
public static io.vertx.core.buffer.Buffer readBuf(ByteBufInputStream is) throws IOException {
int len = is.readInt();
Buffer buf = Buffer.buffer(len);
byte[] part = new byte[1024];
int readPart = 0;
for (int i = 0; i < len; i += 1024) {
var lenx = (Math.min(i + 1024, len)) - i;
if (lenx > 0) {
readPart = is.readNBytes(part, 0, lenx);
buf.appendBytes(part, 0, readPart);
}
}
return buf;
}
public static io.vertx.reactivex.core.buffer.Buffer rxReadBuf(ByteBufInputStream is) throws IOException {
int len = is.readInt();
io.vertx.reactivex.core.buffer.Buffer buf = io.vertx.reactivex.core.buffer.Buffer.buffer(len);
byte[] part = new byte[1024];
int readPart = 0;
for (int i = 0; i < len; i += 1024) {
var lenx = (Math.min(i + 1024, len)) - i;
if (lenx > 0) {
readPart = is.readNBytes(part, 0, lenx);
buf.appendBytes(part, 0, readPart);
}
}
return buf;
}
public interface Writer {
void write(ByteBufOutputStream os) throws IOException;
}
public interface Reader<T> {
T read(ByteBufInputStream is) throws IOException;
}
public static void encode(Buffer buffer, Writer writer) {
try (var os = new ByteBufOutputStream(((BufferImpl) buffer).byteBuf())) {
writer.write(os);
} catch (IOException ex) {
throw new SerializationException(ex);
}
}
public static <T> T decode(int pos, Buffer buffer, Reader<T> reader) {
try (var is = new ByteBufInputStream(buffer.slice(pos, buffer.length()).getByteBuf())) {
return reader.read(is);
} catch (IOException ex) {
throw new SerializationException(ex);
}
}
}

View File

@ -1,5 +0,0 @@
package it.tdlight.utils;
public interface EmptyCallable {
void call() throws Exception;
}

View File

@ -1,163 +0,0 @@
package it.tdlight.utils;
import io.reactivex.Completable;
import io.reactivex.Flowable;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Promise;
import io.vertx.reactivex.RxHelper;
import io.vertx.reactivex.core.eventbus.Message;
import io.vertx.reactivex.core.eventbus.MessageConsumer;
import io.vertx.reactivex.core.streams.Pipe;
import io.vertx.reactivex.core.streams.ReadStream;
import io.vertx.reactivex.core.streams.WriteStream;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Chat;
import it.tdlight.tdlibsession.td.TdError;
import it.tdlight.tdlibsession.td.TdResult;
import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.apache.commons.lang3.NotImplementedException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;
import org.warp.commonutils.concurrency.future.CompletableFutureUtils;
import org.warp.commonutils.functional.IOConsumer;
import org.warp.commonutils.log.Logger;
import org.warp.commonutils.log.LoggerFactory;
import reactor.adapter.rxjava.RxJava2Adapter;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink.OverflowStrategy;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
import reactor.core.publisher.Sinks;
import reactor.core.publisher.Sinks.EmissionException;
import reactor.core.publisher.Sinks.EmitResult;
import reactor.core.publisher.Sinks.Empty;
import reactor.core.publisher.Sinks.Many;
import reactor.core.publisher.Sinks.One;
import reactor.core.publisher.SynchronousSink;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import reactor.util.concurrent.Queues;
import reactor.util.context.Context;
public class MonoUtils {
private static final Logger logger = LoggerFactory.getLogger(MonoUtils.class);
public static <T> Mono<T> notImplemented() {
return Mono.fromCallable(() -> {
throw new NotImplementedException();
});
}
public static <T> Mono<T> fromBlockingMaybe(Callable<T> callable) {
return Mono.fromCallable(callable).subscribeOn(Schedulers.boundedElastic());
}
public static Mono<Void> fromBlockingEmpty(EmptyCallable callable) {
return Mono.<Void>fromCallable(() -> {
callable.call();
return null;
}).subscribeOn(Schedulers.boundedElastic());
}
public static <T> Mono<T> fromBlockingSingle(Callable<T> callable) {
return fromBlockingMaybe(callable).single();
}
public static <T extends TdApi.Object> void orElseThrow(TdResult<T> value, SynchronousSink<T> sink) {
if (value.succeeded()) {
sink.next(value.result());
} else {
sink.error(new TdError(value.cause().code, value.cause().message));
}
}
public static <T extends TdApi.Object> Mono<Void> thenOrError(Mono<TdResult<T>> optionalMono) {
return optionalMono.handle((optional, sink) -> {
if (optional.succeeded()) {
sink.complete();
} else {
sink.error(new TdError(optional.cause().code, optional.cause().message));
}
});
}
@Deprecated
public static <T> Mono<T> toMono(Future<T> future) {
return Mono.fromCompletionStage(future.toCompletionStage());
}
@Deprecated
@NotNull
public static <T> Mono<T> toMono(Single<T> single) {
return RxJava2Adapter.singleToMono(single);
}
@Deprecated
@NotNull
public static <T> Mono<T> toMono(Maybe<T> maybe) {
return RxJava2Adapter.maybeToMono(maybe);
}
@Deprecated
@NotNull
public static <T> Mono<T> toMono(Completable completable) {
//noinspection unchecked
return (Mono<T>) RxJava2Adapter.completableToMono(completable);
}
public static <T> Flux<T> fromMessageConsumer(Mono<Void> onRegistered, MessageConsumer<T> messageConsumer) {
return fromReplyableMessageConsumer(onRegistered, messageConsumer).map(Message::body);
}
public static <T> Flux<Message<T>> fromReplyableMessageConsumer(Mono<Void> onRegistered,
MessageConsumer<T> messageConsumer) {
var registration = messageConsumer
.rxCompletionHandler().to(RxJava2Adapter::completableToMono)
.doFirst(() -> logger.trace("Waiting for consumer registration completion..."))
.doOnSuccess(s -> logger.trace("Consumer registered"))
.then(onRegistered);
var messages = messageConsumer.toFlowable().to(RxJava2Adapter::flowableToFlux);
return messages.mergeWith(registration.then(Mono.empty()));
}
public static Scheduler newBoundedSingle(String name) {
return newBoundedSingle(name, false);
}
public static Scheduler newBoundedSingle(String name, boolean daemon) {
return Schedulers.newBoundedElastic(1,
Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE,
name,
Integer.MAX_VALUE,
daemon
);
}
public static <R> Mono<Optional<R>> toOptional(Mono<R> mono) {
return mono.map(Optional::of).defaultIfEmpty(Optional.empty());
}
public static <T> Mono<Boolean> isSet(Mono<T> mono) {
return mono
.map(res -> true)
.defaultIfEmpty(false);
}
}

View File

@ -1,49 +0,0 @@
package it.tdlight.utils;
import io.vertx.core.Promise;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.SynchronousSink;
import reactor.util.context.Context;
public abstract class PromiseSink<T> implements SynchronousSink<T> {
private final Promise<T> promise;
private PromiseSink(Promise<T> promise) {
this.promise = promise;
}
public static <K> PromiseSink<K> of(Context context, Promise<K> promise) {
return new PromiseSinkImpl<>(promise, context);
}
@Override
public void complete() {
promise.complete();
}
@Override
public void error(@NotNull Throwable error) {
promise.fail(error);
}
@Override
public void next(@NotNull T value) {
promise.complete(value);
}
private static class PromiseSinkImpl<K> extends PromiseSink<K> {
private final Context context;
public PromiseSinkImpl(Promise<K> promise, Context context) {
super(promise);
this.context = context;
}
@Override
public @NotNull Context currentContext() {
return context;
}
}
}

View File

@ -1,40 +0,0 @@
package it.tdlight.utils;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.tdlibsession.td.ResponseError;
import it.tdlight.tdlibsession.td.TdError;
import org.jetbrains.annotations.Nullable;
public class TdLightUtils {
@SuppressWarnings("RedundantIfStatement")
public static boolean errorEquals(Throwable ex, @Nullable Integer errorCode, @Nullable String errorText) {
while (ex != null) {
TdApi.Error error = null;
if (ex instanceof TdError) {
error = ((TdError) ex).getTdError();
}
if (ex instanceof ResponseError) {
error = new Error(((ResponseError) ex).getErrorCode(), ((ResponseError) ex).getErrorMessage());
}
if (error != null) {
if (errorCode != null) {
if (error.code != errorCode) {
return false;
}
}
if (errorText != null) {
if (error.message == null || !error.message.contains(errorText)) {
return false;
}
}
return true;
}
ex = ex.getCause();
}
return false;
}
}

View File

@ -1,49 +0,0 @@
package it.tdlight.utils;
import com.squareup.moshi.JsonAdapter;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Object;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
import org.warp.commonutils.moshi.MoshiPolymorphic;
public class TdMoshiPolymorphic extends MoshiPolymorphic<Object> {
private final Set<Class<TdApi.Object>> abstractClasses = new HashSet<>();
private final Set<Class<TdApi.Object>> concreteClasses = new HashSet<>();
public TdMoshiPolymorphic() {
super();
var declaredClasses = TdApi.class.getDeclaredClasses();
for (Class<?> declaredClass : declaredClasses) {
var modifiers = declaredClass.getModifiers();
if (Modifier.isPublic(modifiers) && Modifier
.isStatic(modifiers)) {
if (Modifier.isAbstract(modifiers)) {
//noinspection unchecked
this.abstractClasses.add((Class<TdApi.Object>) declaredClass);
} else {
//noinspection unchecked
this.concreteClasses.add((Class<TdApi.Object>) declaredClass);
}
}
}
}
@Override
public Set<Class<TdApi.Object>> getAbstractClasses() {
return abstractClasses;
}
@Override
public Set<Class<TdApi.Object>> getConcreteClasses() {
return concreteClasses;
}
@Override
protected boolean shouldIgnoreField(String fieldName) {
return fieldName.equals("CONSTRUCTOR");
}
}

View File

@ -1,132 +0,0 @@
package it.tdlight.utils;
import io.vertx.core.buffer.Buffer;
import org.warp.commonutils.stream.SafeMeasurableInputStream;
import org.warp.commonutils.stream.SafeRepositionableStream;
public class VertxBufferInputStream extends SafeMeasurableInputStream implements SafeRepositionableStream {
private final Buffer buffer;
/** The first valid entry. */
public int offset;
/** The number of valid bytes in {@link #buffer} starting from {@link #offset}. */
public int length;
/** The current position as a distance from {@link #offset}. */
private int position;
/** The current mark as a position, or -1 if no mark exists. */
private int mark;
/** Creates a new buffer input stream using a given buffer fragment.
*
* @param buffer the backing buffer.
* @param offset the first valid entry of the buffer.
* @param length the number of valid bytes.
*/
public VertxBufferInputStream(final Buffer buffer, final int offset, final int length) {
this.buffer = buffer;
this.offset = offset;
this.length = length;
}
/** Creates a new buffer input stream using a given buffer fragment.
*
* @param buffer the backing buffer.
* @param offset the first valid entry of the buffer.
*/
public VertxBufferInputStream(final Buffer buffer, final int offset) {
this.buffer = buffer;
this.offset = offset;
this.length = buffer.length();
}
/** Creates a new buffer input stream using a given buffer.
*
* @param buffer the backing buffer.
*/
public VertxBufferInputStream(final Buffer buffer) {
this(buffer, 0, buffer.length());
}
/** Creates a new buffer input stream using a given buffer.
*
* @param in the backing buffer.
*/
public VertxBufferInputStream(final VertxBufferInputStream in) {
this(in.buffer, in.offset + in.position, in.buffer.length());
}
@Override
public boolean markSupported() {
return true;
}
@Override
public void reset() {
position = mark;
}
/** Closing a fast byte buffer input stream has no effect. */
@Override
public void close() {}
@Override
public void mark(final int dummy) {
mark = position;
}
@Override
public int available() {
return length - position;
}
@Override
public long skip(long n) {
if (n <= length - position) {
position += (int)n;
return n;
}
n = length - position;
position = length;
return n;
}
@Override
public int read() {
if (length == position) return -1;
return buffer.getByte(offset + position++) & 0xFF;
}
/** Reads bytes from this byte-buffer input stream as
* specified in {@link java.io.InputStream#read(byte[], int, int)}.
* Note that the implementation given in {@link java.io.ByteArrayInputStream#read(byte[], int, int)}
* will return -1 on a zero-length read at EOF, contrarily to the specification. We won't.
*/
@Override
public int read(final byte b[], final int offset, final int length) {
if (this.length == this.position) return length == 0 ? 0 : -1;
final int n = Math.min(length, this.length - this.position);
buffer.getBytes(this.offset + this.position, this.offset + this.position + n, b, offset);
this.position += n;
return n;
}
@Override
public long position() {
return position;
}
@Override
public void position(final long newPosition) {
position = (int)Math.min(newPosition, length);
}
@Override
public long length() {
return length;
}
}

View File

@ -1,59 +0,0 @@
package it.tdlight.utils;
import io.vertx.core.buffer.Buffer;
import it.unimi.dsi.fastutil.bytes.ByteArrays;
import org.warp.commonutils.stream.SafeMeasurableOutputStream;
import org.warp.commonutils.stream.SafeRepositionableStream;
public class VertxBufferOutputStream extends SafeMeasurableOutputStream implements SafeRepositionableStream {
/** The buffer backing the output stream. */
public Buffer buffer;
/** Creates a new buffer output stream with an initial capacity of 0 bytes. */
public VertxBufferOutputStream() {
this(0);
}
/** Creates a new buffer output stream with a given initial capacity.
*
* @param initialCapacity the initial length of the backing buffer.
*/
public VertxBufferOutputStream(final int initialCapacity) {
buffer = Buffer.buffer(initialCapacity);
}
/** Creates a new buffer output stream wrapping a given byte buffer.
*
* @param a the byte buffer to wrap.
*/
public VertxBufferOutputStream(final Buffer a) {
buffer = a;
}
@Override
public void write(final int b) {
buffer.appendByte((byte) b);
}
@Override
public void write(final byte[] b, final int off, final int len) {
ByteArrays.ensureOffsetLength(b, off, len);
buffer.appendBytes(b, off, len);
}
@Override
public void position(long newPosition) {
throw new UnsupportedOperationException("Can't change position of a vertx buffer output stream");
}
@Override
public long position() {
return this.length();
}
@Override
public long length() {
return buffer.length();
}
}

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<TerminalConsole name="ConsoleAppender">
<PatternLayout disableAnsi="false"
pattern="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} %highlight{${LOG_LEVEL_PATTERN:-%5p}}{FATAL=red blink, ERROR=red, WARN=yellow bold, INFO=green, DEBUG=green bold, TRACE=blue} %style{%processId}{magenta} [%15.15t] %style{%-20.20c{1}}{cyan} : %m%n%ex"/>
</TerminalConsole>
<Async name="Async">
<AppenderRef ref="ConsoleAppender"/>
<LinkedTransferQueue/>
</Async>
</Appenders>
<Loggers>
<Root level="INFO">
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY"
onMismatch="NEUTRAL"/>
</filters>
<AppenderRef ref="Async"/>
</Root>
</Loggers>
</Configuration>

View File

@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- log4j2.xml - Example log4j configuration file
Place this file in the same directory as your server.jar, edit
to taste, and add -Dlog4j.configurationFile=log4j2.xml to your
server startup flags.
More log4j example configs can be found at
<http://logging.apache.org/log4j/2.x/manual/appenders.html>.
-->
<Configuration>
<Appenders>
<!-- DEFAULT APPENDERS -->
<!-- console logging - logs to stdout -->
<Console name="ConsoleAppender" target="SYSTEM_OUT" follow="true">
<ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="ACCEPT"/>
<PatternLayout disableAnsi="false"
pattern="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} %highlight{${LOG_LEVEL_PATTERN:-%5p}}{FATAL=red blink, ERROR=red, WARN=yellow bold, INFO=green, DEBUG=green bold, TRACE=blue} %style{%processId}{magenta} [%15.15t] %style{%-20.20c{1}}{cyan} : %m%n%ex"/>
</Console>
<!-- console logging - logs to stderr -->
<Console name="ConsoleAppenderErr" target="SYSTEM_ERR" follow="true">
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout disableAnsi="false"
pattern="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} %highlight{${LOG_LEVEL_PATTERN:-%5p}}{FATAL=red blink, ERROR=red, WARN=yellow bold, INFO=green, DEBUG=green bold, TRACE=blue} %style{%processId}{magenta} [%15.15t] %style{%-20.20c{1}}{cyan} : %m%n%ex"/>
</Console>
</Appenders>
<Loggers>
<Logger name="com.hazelcast.internal.diagnostics.HealthMonitor" level="WARN"/>
<Logger name="it.cavallium.dbengine.database.disk.LLLocalDictionary" level="TRACE"/>
<Logger name="it.cavallium.dbengine.database.disk.LLLocalKeyValueDatabase" level="TRACE"/>
<Logger name="it.cavallium.dbengine.database.disk.LLLocalLuceneIndex" level="TRACE"/>
<Logger name="com.hazelcast" level="INFO"/>
<Root level="INFO">
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY"
onMismatch="NEUTRAL"/>
</filters>
<AppenderRef ref="ConsoleAppender"/>
<AppenderRef ref="ConsoleAppenderErr"/>
</Root>
</Loggers>
</Configuration>