Add chat and chats command

This commit is contained in:
Andrea Cavalli 2023-01-02 03:25:45 +01:00
parent 917f498034
commit 6d002239c2
11 changed files with 746 additions and 114 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
jelegram.iml
/target/
/.cache/
/run/*

102
pom.xml
View File

@ -36,7 +36,7 @@
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-java-bom</artifactId>
<version>2.8.9.0</version>
<version>2.8.9.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@ -65,6 +65,18 @@
<groupId>it.tdlight</groupId>
<artifactId>tdlight-natives-linux-amd64</artifactId>
</dependency>
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-natives-windows-amd64</artifactId>
</dependency>
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-natives-linux-aarch64</artifactId>
</dependency>
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-natives-osx-amd64</artifactId>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli-shell-jline3</artifactId>
@ -75,6 +87,21 @@
<artifactId>jansi</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.19.0</version>
</dependency>
</dependencies>
<build>
@ -97,7 +124,7 @@
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<!--<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.4.2</version>
@ -122,6 +149,77 @@
</goals>
</execution>
</executions>
</plugin>-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<!-- note that the main class is set *here* -->
<mainClass>picocli.shell.jline3.example.Example</mainClass>
</transformer>
</transformers>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
<!-- now make the jar chmod +x style executable -->
<plugin>
<groupId>org.skife.maven</groupId>
<artifactId>really-executable-jar-maven-plugin</artifactId>
<version>2.0.0</version>
<configuration>
<!-- value of flags will be interpolated into the java invocation -->
<!-- as "java $flags -jar ..." -->
<flags>-Xmx256M</flags>
<!-- (optional) name for binary executable, if not set will just -->
<!-- make the regular jar artifact executable -->
<programFile>jelegram</programFile>
<!-- (optional) support other packaging formats than jar -->
<!-- <allowOtherTypes>true</allowOtherTypes> -->
<!-- (optional) name for a file that will define what script gets -->
<!-- embedded into the executable jar. This can be used to -->
<!-- override the default startup script which is -->
<!-- `#!/bin/sh -->
<!-- -->
<!-- exec java " + flags + " -jar "$0" "$@" -->
<!-- <scriptFile>src/packaging/someScript.extension</scriptFile> -->
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>really-executable-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@ -5,6 +5,10 @@ module jelegram {
requires tdlight.api;
requires tdlight.java;
requires com.google.zxing;
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.annotation;
requires com.fasterxml.jackson.databind;
requires it.unimi.dsi.fastutil.core;
exports picocli.shell.jline3;
opens picocli.shell.jline3.example;

View File

@ -0,0 +1,3 @@
package picocli.shell.jline3.example;
public class ChatData {}

View File

@ -0,0 +1,5 @@
package picocli.shell.jline3.example;
public enum CliInputParameterValue {
ASK_AUTH_DATA_TYPE
}

View File

@ -4,5 +4,10 @@ import it.tdlight.client.InputParameter;
import it.tdlight.client.ParameterInfo;
import java.util.function.Consumer;
public record ClientInteractionRequest(TDLibInstance instance, InputParameter parameter, ParameterInfo parameterInfo,
Consumer<String> result) {}
public record ClientInteractionRequest(TDLibInstance instance, CustomInputParameter parameter, ParameterInfo parameterInfo,
Consumer<String> result) {
sealed interface CustomInputParameter {
record StandardInputParameter(InputParameter value) implements CustomInputParameter {}
record CliInputParameter(CliInputParameterValue value) implements CustomInputParameter {}
}
}

View File

@ -1,28 +1,43 @@
package picocli.shell.jline3.example;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import it.tdlight.client.APIToken;
import it.tdlight.client.AuthenticationData;
import it.tdlight.client.ConsoleInteractiveAuthenticationData;
import it.tdlight.client.ParameterInfoCode;
import it.tdlight.client.ParameterInfoNotifyLink;
import it.tdlight.client.ParameterInfoPasswordHint;
import it.tdlight.client.ParameterInfoTermsOfService;
import it.tdlight.common.internal.InternalClientManager;
import it.tdlight.common.utils.CantLoadLibrary;
import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.ChatList;
import it.tdlight.jni.TdApi.ChatListArchive;
import it.tdlight.jni.TdApi.ChatListMain;
import it.tdlight.jni.TdApi.Chats;
import it.tdlight.jni.TdApi.GetChat;
import it.tdlight.jni.TdApi.LoadChats;
import it.tdlight.jni.TdApi.Ok;
import it.tdlight.jni.TdApi.TermsOfService;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
@ -47,9 +62,12 @@ import picocli.CommandLine;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.ParentCommand;
import picocli.shell.jline3.PicocliCommands;
import picocli.shell.jline3.PicocliCommands.PicocliCommandsFactory;
import picocli.shell.jline3.example.ClientInteractionRequest.CustomInputParameter.CliInputParameter;
import picocli.shell.jline3.example.ClientInteractionRequest.CustomInputParameter.StandardInputParameter;
/**
* Example that demonstrates how to build an interactive shell with JLine3 and picocli.
@ -60,6 +78,8 @@ import picocli.shell.jline3.PicocliCommands.PicocliCommandsFactory;
*/
public class Example {
static ObjectMapper mapper = new ObjectMapper();
static String currentInstance = null;
static boolean waitingLogin = false;
static Map<String, TDLibInstance> instanceMap = new HashMap<>();
@ -80,7 +100,9 @@ public class Example {
"Hit @|magenta ALT-S|@ to toggle tailtips.",
""},
footer = {"", "Press Ctrl-D to exit."},
subcommands = {InstanceCommand.class, PicocliCommands.ClearScreen.class, CommandLine.HelpCommand.class})
subcommands = {InstanceCommand.class, ChatsCommand.class,
ChatCommand.class, PicocliCommands.ClearScreen.class,
CommandLine.HelpCommand.class})
static class CliCommands implements Runnable {
PrintWriter out;
@ -99,6 +121,8 @@ public class Example {
description = {"Instance-related commands."},
subcommands = {ConfigureCommand.class, StartCommand.class, StopCommand.class, CommandLine.HelpCommand.class})
static class InstanceCommand implements Runnable {
private static final APIToken API_TOKEN = APIToken.example();
@Option(names = {"-v", "--verbose"},
description = { "Specify multiple -v options to increase verbosity.",
"For example, `-v -v -v` or `-vvv`"})
@ -120,20 +144,74 @@ public class Example {
}
}
@Command(mixinStandardHelpOptions = true, subcommands = {CommandLine.HelpCommand.class},
description = "Add an instance.")
public void add(@Option(names = {"-n", "--name"}, required = true) String name) throws CantLoadLibrary {
@Command(mixinStandardHelpOptions = true, description = "Add an instance.")
public void add(@Parameters(paramLabel = "<name>", description = "Instance name.", arity = "1") String name)
throws CantLoadLibrary {
if (instanceMap.containsKey(name)) {
System.err.printf("Instance %s already exists%n", name);
return;
}
currentInstance = name;
instanceMap.put(name, new TDLibInstance(name, interactionRequests));
instanceMap.put(name, new TDLibInstance(API_TOKEN, name, interactionRequests));
}
@Command(mixinStandardHelpOptions = true, subcommands = {CommandLine.HelpCommand.class},
description = "Select an instance.")
public void select(@Option(names = {"-n", "--name"}, required = true) String name) {
@Command(mixinStandardHelpOptions = true, description = "Load an instance from disk.")
public void load(@Parameters(paramLabel = "<path>", description = "Instance path.", arity = "1") Path path)
throws CantLoadLibrary, IOException {
var settingsFilePath = path.resolve("settings.json");
if (Files.notExists(settingsFilePath)) {
System.err.printf("The following path does not exist: %s%n", settingsFilePath);
return;
}
if (!Files.isRegularFile(settingsFilePath)) {
System.err.printf("The following path is not valid: %s%n", settingsFilePath);
return;
}
if (!Files.isReadable(settingsFilePath)) {
System.err.printf("The following path is not readable: %s%n", settingsFilePath);
return;
}
var settings = mapper.readValue(Files.readString(settingsFilePath, StandardCharsets.UTF_8), InstanceSettings.class);
var name = settings.name;
if (instanceMap.containsKey(name)) {
System.err.printf("Instance %s already exists%n", name);
return;
}
TDLibInstance instance = new TDLibInstance(API_TOKEN, settings, interactionRequests);
currentInstance = name;
instanceMap.put(name, instance);
System.out.println("Client is loading...");
waitingLogin = true;
instance.start(settings.toAuthData());
}
private void askAuthDataType(TDLibInstance instance) {
interactionRequests.offer(new ClientInteractionRequest(instance,
new CliInputParameter(CliInputParameterValue.ASK_AUTH_DATA_TYPE),
null,
result -> {
AuthenticationData authenticationData;
if (result.startsWith("phone ")) {
authenticationData = AuthenticationData.user(Long.parseLong(result
.substring("phone ".length())
.replaceAll("[^\\d.]", "")));
} else if (result.startsWith("token ")) {
authenticationData = AuthenticationData.bot(result.substring("token ".length()));
} else if (result.equals("qr")) {
authenticationData = AuthenticationData.qrCode();
} else {
System.err.println("Invalid auth data type, please, enter a correctly formatted response.");
askAuthDataType(instance);
return;
}
instance.start(authenticationData);
System.out.println("Client loaded.");
}
));
}
@Command(mixinStandardHelpOptions = true, description = "Select an instance.")
public void select(@Parameters(paramLabel = "<name>", description = "Instance name.", arity = "1") String name) {
if (!instanceMap.containsKey(name)) {
System.err.printf("Instance %s does not exist%n", name);
return;
@ -142,9 +220,171 @@ public class Example {
}
}
@Command(name = "configure", mixinStandardHelpOptions = true, version = "1.0",
description = {"Instance-related configuration command."},
@Command(name = "chats", mixinStandardHelpOptions = true, version = "1.0",
description = {"Chats-related commands."},
subcommands = {CommandLine.HelpCommand.class})
static class ChatsCommand implements Runnable {
@Option(names = {"-v", "--verbose"},
description = { "Specify multiple -v options to increase verbosity.",
"For example, `-v -v -v` or `-vvv`"})
private boolean[] verbosity = {};
@Option(names = {"-l", "--list"},
description = { "Chats list. Can be \"main\" or \"archive\""})
private String chatsList = "main";
@Option(names = {"-s", "--skip"},
description = { "Skip the following chats."})
private int skipCount = 0;
@Option(names = {"-c", "--count"},
description = { "Show n chats."})
private int count = 100;
@ParentCommand CliCommands parent;
public void run() {
if (currentInstance == null) {
System.out.println("No instance is active.");
return;
}
var i = getInstance();
ChatList chatList;
if (chatsList.equals("main")) {
chatList = new ChatListMain();
} else if (chatsList.equals("archive")) {
chatList = new ChatListArchive();
} else {
System.err.println("Invalid chat list: " + chatsList);
return;
}
var chats = getChatsTo(i, chatList, skipCount + count);
System.out.printf("Chat list %s has %d chats. (shown from %d to %d)%n",
chatsList,
chats.totalCount,
Math.min(skipCount, chats.totalCount),
Math.min(skipCount + count, chats.totalCount)
);
long[] chatIds = chats.chatIds;
for (int j = skipCount; j < chatIds.length; j++) {
long chatId = chatIds[j];
var chat = i.send(new GetChat(chatId)).join();
if (verbosity.length == 0) {
System.out.printf("\t- %s%n", chat.title);
} else {
System.out.printf("\t- [%d] %s%n", chat.id, chat.title);
}
}
var more = Math.max(0, chats.totalCount - (skipCount + count));
if (more > 0) {
System.out.printf(" ... and %d more, use the argument --skip=%d to see them.%n", more, skipCount + count);
}
}
private boolean getIsBot(TDLibInstance i) {
return i.getAuthenticationData().isBot();
}
private Chats getChatsTo(TDLibInstance instance, ChatList chatList, int max) {
var bot = getIsBot(instance);
if (bot && chatList.getConstructor() != ChatListMain.CONSTRUCTOR) {
return new Chats(0, new long[0]);
}
if (bot) {
var chats = instance.getChats();
var array = new long[chats.size()];
var result = new Chats(chats.size(), array);
int i = 0;
for (Long chat : chats) {
array[i++] = chat;
}
return result;
}
int next = 0;
while (next < max) {
next += 100;
instance.send(new LoadChats(chatList, next)).exceptionallyCompose(ex -> {
if (ex instanceof TDException ex2 && ex2.code == 404) {
return CompletableFuture.completedFuture(new Ok());
} else {
return CompletableFuture.failedFuture(ex);
}
}).join();
}
return instance.send(new TdApi.GetChats(chatList, max)).join();
}
}
@Command(name = "chat", mixinStandardHelpOptions = true, version = "1.0",
description = {"Chat-related commands."},
subcommands = {CommandLine.HelpCommand.class})
static class ChatCommand implements Runnable {
@Option(names = {"-v", "--verbose"},
description = { "Specify multiple -v options to increase verbosity.",
"For example, `-v -v -v` or `-vvv`"})
private boolean[] verbosity = {};
@Parameters(paramLabel = "id", description = { "Chat id"}, arity = "1")
private long id;
@ParentCommand CliCommands parent;
public void run() {
if (currentInstance == null) {
System.out.println("No instance is active.");
return;
}
var i = getInstance();
var chat = i.send(new TdApi.GetChat(id)).join();
System.out.printf("Chat %s [%d]%n", chat.title, chat.id);
var lastMessage = chat.lastMessage;
if (lastMessage != null) {
System.out.printf("Last message:%n");
var sender = i.getSender(lastMessage.senderId).join();
System.out.printf("\tSender: %s [%d]%n", sender.title, sender.id);
var text = i.contentToString(lastMessage.content).join();
System.out.printf("\t%s%n", text);
}
}
private boolean getIsBot(TDLibInstance i) {
return i.getAuthenticationData().isBot();
}
private Chats getChatsTo(TDLibInstance instance, ChatList chatList, int max) {
var bot = getIsBot(instance);
if (bot && chatList.getConstructor() != ChatListMain.CONSTRUCTOR) {
return new Chats(0, new long[0]);
}
if (bot) {
var chats = instance.getChats();
var array = new long[chats.size()];
var result = new Chats(chats.size(), array);
int i = 0;
for (Long chat : chats) {
array[i++] = chat;
}
return result;
}
int next = 0;
while (next < max) {
next += 100;
instance.send(new LoadChats(chatList, next)).exceptionallyCompose(ex -> {
if (ex instanceof TDException ex2 && ex2.code == 404) {
return CompletableFuture.completedFuture(new Ok());
} else {
return CompletableFuture.failedFuture(ex);
}
}).join();
}
return instance.send(new TdApi.GetChats(chatList, max)).join();
}
}
@Command(name = "configure", mixinStandardHelpOptions = true, version = "1.0",
description = {"Instance-related configuration command."})
static class ConfigureCommand implements Runnable {
@Option(names = {"-p", "--path"},
description = { "Specify the path."})
@ -162,7 +402,8 @@ public class Example {
description = { "Use the chats database."}, negatable = true)
private Boolean useTestDc;
@ParentCommand InstanceCommand parent;
@ParentCommand
InstanceCommand parent;
public void run() {
if (currentInstance == null) {
@ -172,7 +413,7 @@ public class Example {
var instance = getInstance();
var settings = instance.getSettings();
if (path != null) {
instance.setPath(currentInstance);
instance.setPath(path);
System.out.printf("Path set to \"%s\".%n", path);
}
if (useMessagesDb != null) {
@ -194,9 +435,7 @@ public class Example {
}
}
@Command(name = "start", mixinStandardHelpOptions = true, version = "1.0",
description = {"Start the instance."},
subcommands = {CommandLine.HelpCommand.class})
@Command(name = "start", mixinStandardHelpOptions = true, version = "1.0", description = {"Start the instance."})
static class StartCommand implements Runnable {
@ParentCommand InstanceCommand parent;
@ -222,6 +461,11 @@ public class Example {
return;
}
var instance = getInstance();
var path = instance.getPath();
if (Files.exists(path)) {
System.err.printf("Instance path already exists: %s%n", path);
return;
}
AuthenticationData authenticationData;
if (authMode.qrLogin) {
authenticationData = AuthenticationData.qrCode();
@ -239,9 +483,7 @@ public class Example {
}
}
@Command(name = "stop", mixinStandardHelpOptions = true, version = "1.0",
description = {"Stop the instance."},
subcommands = {CommandLine.HelpCommand.class})
@Command(name = "stop", mixinStandardHelpOptions = true, version = "1.0", description = {"Stop the instance."})
static class StopCommand implements Runnable {
@ParentCommand InstanceCommand parent;
@ -299,7 +541,11 @@ public class Example {
builtins.setLineReader(reader);
commands.setReader(reader);
factory.setTerminal(terminal);
TailTipWidgets widgets = new TailTipWidgets(reader, systemRegistry::commandDescription, 5, TailTipWidgets.TipType.COMPLETER);
TailTipWidgets widgets = new TailTipWidgets(reader,
systemRegistry::commandDescription,
5,
TailTipWidgets.TipType.COMPLETER
);
widgets.enable();
KeyMap<Binding> keyMap = reader.getKeyMaps().get("main");
keyMap.bind(new Reference("tailtip-toggle"), KeyMap.alt("s"));
@ -309,6 +555,7 @@ public class Example {
// start the shell and process input until the user quits with Ctrl-D
String line;
shell:
while (true) {
try {
systemRegistry.cleanUp();
@ -326,13 +573,14 @@ public class Example {
if (lastIr != null) {
var requestAndResponse = getInteractionRequest(lastIr);
prompt = requestAndResponse.getKey().orElse(null);
if (prompt != null) {
prompt += "\n> ";
}
line = reader.readLine(prompt, null, (MaskingCallback) null, null);
if (requestAndResponse.getValue().isPresent()) {
System.out.println(prompt);
lastIr.result().accept(requestAndResponse.getValue().get());
} else {
if (prompt != null) {
prompt += "\n> ";
}
line = reader.readLine(prompt, null, (MaskingCallback) null, null);
lastIr.result().accept(line);
}
} else if (!waitingLogin) {
@ -342,34 +590,38 @@ public class Example {
} catch (UserInterruptException e) {
// Ignore
} catch (EndOfFileException e) {
return;
break shell;
} catch (Exception e) {
systemRegistry.trace(e);
}
}
for (var entry : new HashSet<>(instanceMap.entrySet())) {
try {
entry.getValue().stop();
} catch (Exception ex) {
System.out.printf("Failed to close instance %s%n", entry.getKey());
ex.printStackTrace();
}
}
}
} catch (Throwable t) {
t.printStackTrace();
} finally {
try {
InternalClientManager.get("tdlight").close();
} catch (Exception ex) {
ex.printStackTrace();
}
AnsiConsole.systemUninstall();
}
}
private static Entry<Optional<String>, Optional<String>> getInteractionRequest(ClientInteractionRequest interactionRequest) {
var parameter = interactionRequest.parameter();
var customParameter = interactionRequest.parameter();
var parameterInfo = interactionRequest.parameterInfo();
var authenticationData = interactionRequest.instance().getAuthenticationData();
var resultCons = interactionRequest.result();
String who;
boolean useRealWho;
if (authenticationData instanceof ConsoleInteractiveAuthenticationData) {
useRealWho = ((ConsoleInteractiveAuthenticationData) authenticationData).isInitialized();
} else {
useRealWho = true;
}
if (!useRealWho) {
who = "login";
} else if (authenticationData.isQrCode()) {
if (authenticationData.isQrCode()) {
who = "QR login";
} else if (authenticationData.isBot()) {
who = authenticationData.getBotToken().split(":", 2)[0];
@ -378,74 +630,84 @@ public class Example {
}
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";
ParameterInfoCode codeInfo = ((ParameterInfoCode) parameterInfo);
question += "\n\tPhone number: " + codeInfo.getPhoneNumber();
question += "\n\tTimeout: " + codeInfo.getTimeout() + " seconds";
question += "\n\tCode type: " + codeInfo.getType().getClass().getSimpleName()
.replace("AuthenticationCodeType", "");
if (codeInfo.getNextType() != null) {
question += "\n\tNext code type: " + codeInfo
.getNextType()
.getClass()
.getSimpleName()
.replace("AuthenticationCodeType", "");
}
trim = true;
break;
case ASK_PASSWORD:
question = "Enter your password";
String passwordMessage = "Password authorization:";
String hint = ((ParameterInfoPasswordHint) parameterInfo).getHint();
if (hint != null && !hint.isEmpty()) {
passwordMessage += "\n\tHint: " + hint;
}
boolean hasRecoveryEmailAddress = ((ParameterInfoPasswordHint) parameterInfo)
.hasRecoveryEmailAddress();
passwordMessage += "\n\tHas recovery email: " + hasRecoveryEmailAddress;
String recoveryEmailAddressPattern = ((ParameterInfoPasswordHint) parameterInfo)
.getRecoveryEmailAddressPattern();
if (recoveryEmailAddressPattern != null && !recoveryEmailAddressPattern.isEmpty()) {
passwordMessage += "\n\tRecovery email address pattern: " + recoveryEmailAddressPattern;
}
System.out.println(passwordMessage);
break;
case NOTIFY_LINK:
String link = ((ParameterInfoNotifyLink) parameterInfo).getLink();
var sb = new StringBuilder();
sb.append("Please confirm this login link on another device: " + link + "\n");
sb.append("\n");
sb.append(getQr(link) + "\n");
sb.append("\n");
return Map.entry(Optional.of(sb.toString()), Optional.of(""));
case TERMS_OF_SERVICE:
TermsOfService tos = ((ParameterInfoTermsOfService) parameterInfo).getTermsOfService();
question = "Terms of service:\n\t" + tos.text.text;
if (tos.minUserAge > 0) {
question += "\n\tMinimum user age: " + tos.minUserAge;
}
if (tos.showPopup) {
question += "\nPlease press enter.";
if (customParameter instanceof StandardInputParameter inputParameter) {
var parameter = inputParameter.value();
switch (parameter) {
case ASK_FIRST_NAME:
question = "Enter first name";
trim = true;
} else {
return Map.entry(Optional.of(question), Optional.of(""));
}
break;
default:
question = parameter.toString();
break;
break;
case ASK_LAST_NAME:
question = "Enter last name";
trim = true;
break;
case ASK_CODE:
question = "Enter authentication code";
ParameterInfoCode codeInfo = ((ParameterInfoCode) parameterInfo);
question += "\n\tPhone number: " + codeInfo.getPhoneNumber();
question += "\n\tTimeout: " + codeInfo.getTimeout() + " seconds";
question += "\n\tCode type: " + codeInfo.getType().getClass().getSimpleName()
.replace("AuthenticationCodeType", "");
if (codeInfo.getNextType() != null) {
question += "\n\tNext code type: " + codeInfo
.getNextType()
.getClass()
.getSimpleName()
.replace("AuthenticationCodeType", "");
}
trim = true;
break;
case ASK_PASSWORD:
question = "Enter your password";
String passwordMessage = "Password authorization:";
String hint = ((ParameterInfoPasswordHint) parameterInfo).getHint();
if (hint != null && !hint.isEmpty()) {
passwordMessage += "\n\tHint: " + hint;
}
boolean hasRecoveryEmailAddress = ((ParameterInfoPasswordHint) parameterInfo)
.hasRecoveryEmailAddress();
passwordMessage += "\n\tHas recovery email: " + hasRecoveryEmailAddress;
String recoveryEmailAddressPattern = ((ParameterInfoPasswordHint) parameterInfo)
.getRecoveryEmailAddressPattern();
if (recoveryEmailAddressPattern != null && !recoveryEmailAddressPattern.isEmpty()) {
passwordMessage += "\n\tRecovery email address pattern: " + recoveryEmailAddressPattern;
}
System.out.println(passwordMessage);
break;
case NOTIFY_LINK:
String link = ((ParameterInfoNotifyLink) parameterInfo).getLink();
var sb = new StringBuilder();
sb.append("Please confirm this login link on another device: " + link + "\n");
sb.append("\n");
sb.append(getQr(link) + "\n");
sb.append("\n");
return Map.entry(Optional.of("[" + who + "] " + sb), Optional.of(""));
case TERMS_OF_SERVICE:
TermsOfService tos = ((ParameterInfoTermsOfService) parameterInfo).getTermsOfService();
question = "Terms of service:\n\t" + tos.text.text;
if (tos.minUserAge > 0) {
question += "\n\tMinimum user age: " + tos.minUserAge;
}
if (tos.showPopup) {
question += "\nPlease press enter.";
trim = true;
} else {
return Map.entry(Optional.of("[" + who + "] " + question), Optional.of(""));
}
break;
default:
question = parameter.toString();
break;
}
} else if (customParameter instanceof CliInputParameter inputParameter) {
question = switch (inputParameter.value()) {
case ASK_AUTH_DATA_TYPE -> "Please type the preferred auth mode [qr | token <token> | phone <phone number>]";
default -> inputParameter.value().toString();
};
} else {
throw new IllegalStateException("Invalid custom parameter type: " + customParameter.toString());
}
return Map.entry(Optional.of(question), Optional.empty());
return Map.entry(Optional.of("[" + who + "] " + question), Optional.empty());
}
private static String getQr(String url) {

View File

@ -0,0 +1,127 @@
package picocli.shell.jline3.example;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import it.tdlight.client.APIToken;
import it.tdlight.client.AuthenticationData;
import it.tdlight.client.TDLibSettings;
import java.nio.file.Path;
public class InstanceSettings {
public boolean useTestDatacenter;
public String token;
public Long phoneNumber;
public Path path;
public boolean fileDatabaseEnabled;
public boolean chatInfoDatabaseEnabled;
public boolean messageDatabaseEnabled;
public String systemLanguageCode;
public String deviceModel;
public String systemVersion;
public String applicationVersion;
public boolean enableStorageOptimizer;
public boolean ignoreFileNames;
public String name;
public InstanceSettings(String name, Path path, TDLibSettings settings, AuthenticationData authenticationData) {
this.name = name;
this.useTestDatacenter = settings.isUsingTestDatacenter();
if (authenticationData.isBot()) {
this.token = authenticationData.getBotToken();
} else if (!authenticationData.isQrCode()) {
this.phoneNumber = authenticationData.getUserPhoneNumber();
}
this.path = path;
this.fileDatabaseEnabled = settings.isFileDatabaseEnabled();
this.chatInfoDatabaseEnabled = settings.isChatInfoDatabaseEnabled();
this.messageDatabaseEnabled = settings.isMessageDatabaseEnabled();
this.systemLanguageCode = settings.getSystemLanguageCode();
this.deviceModel = settings.getDeviceModel();
this.systemVersion = settings.getSystemVersion();
this.applicationVersion = settings.getApplicationVersion();
this.enableStorageOptimizer = settings.isStorageOptimizerEnabled();
this.ignoreFileNames = settings.isIgnoreFileNames();
}
@JsonCreator
public InstanceSettings(@JsonProperty("useTestDatacenter") boolean useTestDatacenter,
@JsonProperty("token") String token,
@JsonProperty("phoneNumber") Long phoneNumber,
@JsonProperty("path") Path path,
@JsonProperty("fileDatabaseEnabled") boolean fileDatabaseEnabled,
@JsonProperty("chatInfoDatabaseEnabled") boolean chatInfoDatabaseEnabled,
@JsonProperty("messageDatabaseEnabled") boolean messageDatabaseEnabled,
@JsonProperty("systemLanguageCode") String systemLanguageCode,
@JsonProperty("deviceModel") String deviceModel,
@JsonProperty("systemVersion") String systemVersion,
@JsonProperty("applicationVersion") String applicationVersion,
@JsonProperty("enableStorageOptimizer") boolean enableStorageOptimizer,
@JsonProperty("ignoreFileNames") boolean ignoreFileNames,
@JsonProperty("name") String name) {
this.name = name;
this.useTestDatacenter = useTestDatacenter;
this.token = token;
this.phoneNumber = phoneNumber;
this.path = path;
this.fileDatabaseEnabled = fileDatabaseEnabled;
this.chatInfoDatabaseEnabled = chatInfoDatabaseEnabled;
this.messageDatabaseEnabled = messageDatabaseEnabled;
this.systemLanguageCode = systemLanguageCode;
this.deviceModel = deviceModel;
this.systemVersion = systemVersion;
this.applicationVersion = applicationVersion;
this.enableStorageOptimizer = enableStorageOptimizer;
this.ignoreFileNames = ignoreFileNames;
}
public static Path getDatabaseDirectoryPath(Path basePath) {
return basePath.resolve("data");
}
public static Path getDownloadsDirectoryPath(Path basePath) {
return basePath.resolve("downloads");
}
@JsonIgnore
public Path getDatabaseDirectoryPath() {
return getDatabaseDirectoryPath(path);
}
@JsonIgnore
public Path getDownloadsDirectoryPath() {
return getDownloadsDirectoryPath(path);
}
public static void setTdPaths(TDLibSettings settings, Path path) {
settings.setDatabaseDirectoryPath(getDatabaseDirectoryPath(path));
settings.setDownloadedFilesDirectoryPath(getDownloadsDirectoryPath(path));
}
public TDLibSettings toTDLibSettings(APIToken apiToken) {
var settings = TDLibSettings.create(apiToken);
settings.setUseTestDatacenter(useTestDatacenter);
setTdPaths(settings, path);
settings.setFileDatabaseEnabled(fileDatabaseEnabled);
settings.setChatInfoDatabaseEnabled(chatInfoDatabaseEnabled);
settings.setMessageDatabaseEnabled(messageDatabaseEnabled);
settings.setSystemLanguageCode(systemLanguageCode);
settings.setDeviceModel(deviceModel);
settings.setSystemVersion(systemVersion);
settings.setApplicationVersion(applicationVersion);
settings.setEnableStorageOptimizer(enableStorageOptimizer);
settings.setIgnoreFileNames(ignoreFileNames);
return settings;
}
public AuthenticationData toAuthData() {
if (token != null) {
return AuthenticationData.bot(token);
} else if (phoneNumber != null) {
return AuthenticationData.user(phoneNumber);
} else {
return AuthenticationData.qrCode();
}
}
}

View File

@ -0,0 +1,13 @@
package picocli.shell.jline3.example;
import it.tdlight.jni.TdApi.Error;
public class TDException extends Exception {
public final int code;
public TDException(Error error) {
super(error.code + ": " + error.message);
this.code = error.code;
}
}

View File

@ -2,6 +2,8 @@ package picocli.shell.jline3.example;
import it.tdlight.client.APIToken;
import it.tdlight.client.AuthenticationData;
import it.tdlight.client.InputParameter;
import it.tdlight.client.ParameterInfo;
import it.tdlight.client.SimpleTelegramClient;
import it.tdlight.client.TDLibSettings;
import it.tdlight.common.Init;
@ -11,30 +13,58 @@ import it.tdlight.jni.TdApi.AuthorizationStateClosed;
import it.tdlight.jni.TdApi.AuthorizationStateClosing;
import it.tdlight.jni.TdApi.AuthorizationStateLoggingOut;
import it.tdlight.jni.TdApi.AuthorizationStateReady;
import it.tdlight.jni.TdApi.Chat;
import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.MessageContent;
import it.tdlight.jni.TdApi.MessageSender;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collection;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import picocli.shell.jline3.example.ClientInteractionRequest.CustomInputParameter.StandardInputParameter;
public class TDLibInstance {
private final String name;
private Path path;
private final TDLibSettings settings;
private final Queue<ClientInteractionRequest> interactionRequests;
private SimpleTelegramClient client;
private AuthenticationData authenticationData;
private final Long2ObjectLinkedOpenHashMap<ChatData> chats = new Long2ObjectLinkedOpenHashMap<>();
public TDLibInstance(String name, Queue<ClientInteractionRequest> interactionRequests) throws CantLoadLibrary {
public TDLibInstance(APIToken apiToken, String name, Queue<ClientInteractionRequest> interactionRequests)
throws CantLoadLibrary {
Init.start();
var apiToken = APIToken.example();
this.name = name;
settings = TDLibSettings.create(apiToken);
setPath(name);
this.interactionRequests = interactionRequests;
}
public TDLibInstance(APIToken apiToken, InstanceSettings settings,
Queue<ClientInteractionRequest> interactionRequests) throws CantLoadLibrary {
Init.start();
this.name = settings.name;
this.settings = settings.toTDLibSettings(apiToken);
this.interactionRequests = interactionRequests;
this.path = settings.path;
}
public void setPath(String name) {
var path = Path.of(".").resolve(name);
settings.setDatabaseDirectoryPath(path.resolve("data"));
settings.setDownloadedFilesDirectoryPath(path.resolve("downloads"));
this.setPath(Path.of(".").resolve(name));
}
public void setPath(Path path) {
this.path = path;
InstanceSettings.setTdPaths(settings, path);
}
public TDLibSettings getSettings() {
@ -50,11 +80,11 @@ public class TDLibInstance {
throw new UnsupportedOperationException("Client is already started");
}
client = new SimpleTelegramClient(settings);
client.setClientInteraction((parameter, parameterInfo, result) ->
interactionRequests.offer(new ClientInteractionRequest(TDLibInstance.this, parameter, parameterInfo, result)));
client.setClientInteraction(this::onClientInteraction);
client.addUpdateHandler(TdApi.UpdateAuthorizationState.class, updateAuthorizationState -> {
var state = updateAuthorizationState.authorizationState.getConstructor();
if (state == AuthorizationStateReady.CONSTRUCTOR) {
saveSettings();
System.out.println("Logged in");
}
switch (state) {
@ -64,9 +94,33 @@ public class TDLibInstance {
AuthorizationStateClosing.CONSTRUCTOR -> Example.waitingLogin = false;
}
});
client.addUpdateHandler(TdApi.UpdateNewChat.class, updateNewChat -> {
var chat = chats.getAndMoveToFirst(updateNewChat.chat.id);
if (chat == null) {
chat = new ChatData();
chats.putAndMoveToFirst(updateNewChat.chat.id, chat);
}
});
client.start(authenticationData);
}
private void onClientInteraction(InputParameter inputParameter,
ParameterInfo parameterInfo,
Consumer<String> result) {
var customParameter = new StandardInputParameter(inputParameter);
interactionRequests.offer(new ClientInteractionRequest(this, customParameter, parameterInfo, result));
}
private void saveSettings() {
var is = new InstanceSettings(name, path, settings, authenticationData);
try (var os = Files.newOutputStream(path.resolve("settings.json"),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
Example.mapper.writeValue(os, is);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public AuthenticationData getAuthenticationData() {
return authenticationData;
}
@ -74,4 +128,43 @@ public class TDLibInstance {
public void stop() throws InterruptedException {
client.closeAndWait();
}
public Path getPath() {
return path;
}
public <T extends Function<R>, R extends TdApi.Object> CompletableFuture<R> send(T f) {
var cf = new CompletableFuture<R>();
client.send(f, result -> {
if (result.isError()) {
//noinspection OptionalGetWithoutIsPresent
cf.completeExceptionally(new TDException(result.error().get()));
} else {
cf.complete(result.get());
}
});
return cf;
}
public Collection<Long> getChats() {
return chats.keySet();
}
public CompletableFuture<Chat> getSender(MessageSender senderId) {
if (senderId instanceof TdApi.MessageSenderChat senderChat) {
return send(new TdApi.GetChat(senderChat.chatId));
} else if (senderId instanceof TdApi.MessageSenderUser senderUser) {
return send(new TdApi.GetChat(senderUser.userId));
} else {
return CompletableFuture.failedFuture(new UnsupportedOperationException("Sender not supported: " + senderId));
}
}
public CompletableFuture<String> contentToString(MessageContent content) {
if (content instanceof TdApi.MessageText messageText) {
return CompletableFuture.completedFuture(messageText.text.text);
} else {
return CompletableFuture.completedFuture("(" + content.getClass().getSimpleName().substring("Message".length()) + ")");
}
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration strict="true"
xmlns="http://logging.apache.org/log4j/2.0/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://logging.apache.org/log4j/2.0/config
https://raw.githubusercontent.com/apache/logging-log4j2/log4j-2.16.0/log4j-core/src/main/resources/Log4j-config.xsd"
status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout
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} %style{%-20.20c{1}}{cyan} : %m%n%ex"/>
</Console>
</Appenders>
<Loggers>
<Logger name="it.tdlight.TDLight" level="ERROR" additivity="false" />
<Root level="WARN">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>