Compare commits

...

No commits in common. "master" and "cli" have entirely different histories.
master ... cli

14 changed files with 1709 additions and 1 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/.idea/
jelegram.iml
/target/
/.cache/
/run/*

View File

@ -1 +0,0 @@
pac merda

226
pom.xml Normal file
View File

@ -0,0 +1,226 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>it.cavallium</groupId>
<artifactId>jelegram</artifactId>
<version>2.0.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<teavm.version>0.7.0-dev-1209</teavm.version>
</properties>
<repositories>
<repository>
<id>teavm-dev</id>
<url>https://teavm.org/maven/repository</url>
</repository>
<repository>
<id>mchv</id>
<name>MCHV Apache Maven Packages</name>
<url>https://mvn.mchv.eu/repository/mchv/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>teavm-dev</id>
<url>https://teavm.org/maven/repository</url>
</pluginRepository>
</pluginRepositories>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-java-bom</artifactId>
<version>2.8.9.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Servlet 3.1 specification -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>23.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>it.tdlight</groupId>
<artifactId>tdlight-java</artifactId>
</dependency>
<dependency>
<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>
<version>4.7.0</version>
</dependency>
<dependency>
<groupId>org.fusesource.jansi</groupId>
<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>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>4.0.0-M4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<!--<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>picocli.shell.jline3.example.Example</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</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>
</project>

View File

@ -0,0 +1,15 @@
module jelegram {
requires info.picocli;
requires org.jline;
requires org.fusesource.jansi;
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,310 @@
package picocli.shell.jline3;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import org.jline.builtins.Options.HelpException;
import org.jline.console.ArgDesc;
import org.jline.console.CmdDesc;
import org.jline.console.CommandRegistry;
import org.jline.reader.Candidate;
import org.jline.reader.Completer;
import org.jline.reader.LineReader;
import org.jline.reader.ParsedLine;
import org.jline.reader.impl.completer.ArgumentCompleter;
import org.jline.reader.impl.completer.NullCompleter;
import org.jline.reader.impl.completer.SystemCompleter;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedString;
import org.jline.utils.InfoCmp.Capability;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Help;
import picocli.CommandLine.IFactory;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.OptionSpec;
/**
* Compiles SystemCompleter for command completion and implements a method commandDescription() that provides command descriptions
* for JLine TailTipWidgets to be displayed in terminal status bar.
* SystemCompleter implements the JLine 3 {@link Completer} interface. SystemCompleter generates completion
* candidates for the specified command line based on the {@link CommandSpec} that this {@code PicocliCommands} was constructed with.
*
* @since 4.1.2
*/
public class PicocliCommands implements CommandRegistry {
/**
* Command that clears the screen.
* <p>
* <b>WARNING:</b> This subcommand needs a JLine {@code Terminal} to clear the screen.
* To accomplish this, construct the {@code CommandLine} with a {@code PicocliCommandsFactory},
* and set the {@code Terminal} on that factory. For example:
* <pre>
* &#064;Command(subcommands = PicocliCommands.ClearScreen.class)
* class MyApp //...
*
* PicocliCommandsFactory factory = new PicocliCommandsFactory();
* CommandLine cmd = new CommandLine(new MyApp(), factory);
* // create terminal
* factory.setTerminal(terminal);
* </pre>
*
* @since 4.6
*/
@Command(name = "cls", aliases = "clear", mixinStandardHelpOptions = true,
description = "Clears the screen", version = "1.0")
public static class ClearScreen implements Callable<Void> {
private final Terminal terminal;
ClearScreen(Terminal terminal) { this.terminal = terminal; }
public Void call() throws IOException {
if (terminal != null) { terminal.puts(Capability.clear_screen); }
return null;
}
}
/**
* Command factory that is necessary for applications that want the use the {@code ClearScreen} subcommand.
* It can be chained with other factories.
* <p>
* <b>WARNING:</b> If the application uses the {@code ClearScreen} subcommand, construct the {@code CommandLine}
* with a {@code PicocliCommandsFactory}, and set the {@code Terminal} on that factory. Applications need
* to call the {@code setTerminal} method with a {@code Terminal}; this will be passed to the {@code ClearScreen}
* subcommand.
*
* For example:
* <pre>
* PicocliCommandsFactory factory = new PicocliCommandsFactory();
* CommandLine cmd = new CommandLine(new MyApp(), factory);
* // create terminal
* factory.setTerminal(terminal);
* </pre>
*
* Other factories can be chained by passing them in to the constructor like this:
* <pre>
* MyCustomFactory customFactory = createCustomFactory(); // your application custom factory
* PicocliCommandsFactory factory = new PicocliCommandsFactory(customFactory); // chain the factories
* </pre>
*
* @since 4.6
*/
public static class PicocliCommandsFactory implements CommandLine.IFactory {
private CommandLine.IFactory nextFactory;
private Terminal terminal;
public PicocliCommandsFactory() {
// nextFactory and terminal are null
}
public PicocliCommandsFactory(IFactory nextFactory) {
this.nextFactory = nextFactory;
// nextFactory is set (but may be null) and terminal is null
}
@SuppressWarnings("unchecked")
public <K> K create(Class<K> clazz) throws Exception {
if (ClearScreen.class == clazz) { return (K) new ClearScreen(terminal); }
if (nextFactory != null) { return nextFactory.create(clazz); }
return CommandLine.defaultFactory().create(clazz);
}
public void setTerminal(Terminal terminal) {
this.terminal = terminal;
// terminal may be null, so check before using it in ClearScreen command
}
}
private final CommandLine cmd;
private final Set<String> commands;
private final Map<String,String> aliasCommand = new HashMap<>();
public PicocliCommands(CommandLine cmd) {
this.cmd = cmd;
commands = cmd.getCommandSpec().subcommands().keySet();
for (String c: commands) {
for (String a: cmd.getSubcommands().get(c).getCommandSpec().aliases()) {
aliasCommand.put(a, c);
}
}
}
/**
*
* @param command
* @return true if PicocliCommands contains command
*/
public boolean hasCommand(String command) {
return commands.contains(command) || aliasCommand.containsKey(command);
}
public SystemCompleter compileCompleters() {
SystemCompleter out = new SystemCompleter();
List<String> all = new ArrayList<>();
all.addAll(commands);
all.addAll(aliasCommand.keySet());
out.add(all, new PicocliCompleter());
return out;
}
private class PicocliCompleter extends ArgumentCompleter implements Completer {
public PicocliCompleter() { super(NullCompleter.INSTANCE); }
@Override
public void complete(LineReader reader, ParsedLine commandLine, List<Candidate> candidates) {
assert commandLine != null;
assert candidates != null;
String word = commandLine.word();
List<String> words = commandLine.words();
CommandLine sub = findSubcommandLine(words, commandLine.wordIndex());
if (sub == null) {
return;
}
if (word.startsWith("-")) {
String buffer = word.substring(0, commandLine.wordCursor());
int eq = buffer.indexOf('=');
for (OptionSpec option : sub.getCommandSpec().options()) {
if (option.arity().max() == 0 && eq < 0) {
addCandidates(candidates, Arrays.asList(option.names()));
} else {
if (eq > 0) {
String opt = buffer.substring(0, eq);
if (Arrays.asList(option.names()).contains(opt) && option.completionCandidates() != null) {
addCandidates(candidates, option.completionCandidates(), buffer.substring(0, eq + 1), "", true);
}
} else {
addCandidates(candidates, Arrays.asList(option.names()), "", "=", false);
}
}
}
} else {
addCandidates(candidates, sub.getSubcommands().keySet());
for (CommandLine s : sub.getSubcommands().values()) {
addCandidates(candidates, Arrays.asList(s.getCommandSpec().aliases()));
}
}
}
private void addCandidates(List<Candidate> candidates, Iterable<String> cands) {
addCandidates(candidates, cands, "", "", true);
}
private void addCandidates(List<Candidate> candidates, Iterable<String> cands, String preFix, String postFix, boolean complete) {
for (String s : cands) {
candidates.add(new Candidate(AttributedString.stripAnsi(preFix + s + postFix), s, null, null, null, null, complete));
}
}
}
private CommandLine findSubcommandLine(List<String> args, int lastIdx) {
CommandLine out = cmd;
for (int i = 0; i < lastIdx; i++) {
if (!args.get(i).startsWith("-")) {
out = findSubcommandLine(out, args.get(i));
if (out == null) {
break;
}
}
}
return out;
}
private CommandLine findSubcommandLine(CommandLine cmdline, String command) {
for (CommandLine s : cmdline.getSubcommands().values()) {
if (s.getCommandName().equals(command) || Arrays.asList(s.getCommandSpec().aliases()).contains(command)) {
return s;
}
}
return null;
}
/**
*
* @param args
* @return command description for JLine TailTipWidgets to be displayed in terminal status bar.
*/
@Override
public CmdDesc commandDescription(List<String> args) {
CommandLine sub = findSubcommandLine(args, args.size());
if (sub == null) {
return null;
}
CommandSpec spec = sub.getCommandSpec();
Help cmdhelp= new picocli.CommandLine.Help(spec);
List<AttributedString> main = new ArrayList<>();
Map<String, List<AttributedString>> options = new HashMap<>();
String synopsis = AttributedString.stripAnsi(spec.usageMessage().sectionMap().get("synopsis").render(cmdhelp).toString());
main.add(HelpException.highlightSyntax(synopsis.trim(), HelpException.defaultStyle()));
// using JLine help highlight because the statement below does not work well...
// main.add(new AttributedString(spec.usageMessage().sectionMap().get("synopsis").render(cmdhelp).toString()));
for (OptionSpec o : spec.options()) {
String key = Arrays.stream(o.names()).collect(Collectors.joining(" "));
List<AttributedString> val = new ArrayList<>();
for (String d: o.description()) {
val.add(new AttributedString(d));
}
if (o.arity().max() > 0) {
key += "=" + o.paramLabel();
}
options.put(key, val);
}
return new CmdDesc(main, ArgDesc.doArgNames(Arrays.asList("")), options);
}
@Override
public List<String> commandInfo(String command) {
List<String> out = new ArrayList<>();
CommandSpec spec = cmd.getSubcommands().get(command).getCommandSpec();
Help cmdhelp = new picocli.CommandLine.Help(spec);
String description = AttributedString.stripAnsi(spec.usageMessage().sectionMap().get("description").render(cmdhelp).toString());
out.addAll(Arrays.asList(description.split("\\r?\\n")));
return out;
}
// For JLine >= 3.16.0
@Override
public Object invoke(CommandRegistry.CommandSession session, String command, Object[] args) throws Exception {
List<String> arguments = new ArrayList<>();
arguments.add( command );
arguments.addAll( Arrays.stream( args ).map( Object::toString ).collect( Collectors.toList() ) );
cmd.execute( arguments.toArray( new String[0] ) );
return null;
}
// @Override This method was removed in JLine 3.16.0; keep it in case this component is used with an older version of JLine
public Object execute(CommandRegistry.CommandSession session, String command, String[] args) throws Exception {
List<String> arguments = new ArrayList<>();
arguments.add(command);
arguments.addAll(Arrays.asList(args));
cmd.execute(arguments.toArray(new String[0]));
return null;
}
@Override
public Set<String> commandNames() {
return commands;
}
@Override
public Map<String, String> commandAliases() {
return aliasCommand;
}
// @Override This method was removed in JLine 3.16.0; keep it in case this component is used with an older version of JLine
public CmdDesc commandDescription(String command) {
return null;
}
}

View File

@ -0,0 +1,63 @@
package picocli.shell.jline3;
import org.jline.reader.LineReader;
import org.jline.reader.Completer;
import org.jline.reader.Candidate;
import org.jline.reader.ParsedLine;
import picocli.AutoComplete;
import picocli.CommandLine.Model.CommandSpec;
import java.util.List;
import java.util.ArrayList;
import java.lang.CharSequence;
/**
* Implementation of the JLine 3 {@link Completer} interface that generates completion
* candidates for the specified command line based on the {@link CommandSpec} that
* this {@code PicocliJLineCompleter} was constructed with.
*
* @since 3.9
*/
public class PicocliJLineCompleter implements Completer {
private final CommandSpec spec;
/**
* Constructs a new {@code PicocliJLineCompleter} for the given command spec.
* @param spec the command specification to generate completions for. Must be non-{@code null}.
*/
public PicocliJLineCompleter(CommandSpec spec) {
if (spec == null) { throw new NullPointerException("spec"); }
this.spec = spec;
}
/**
* Populates <i>candidates</i> with a list of possible completions for the <i>command line</i>.
*
* The list of candidates will be sorted and filtered by the LineReader, so that
* the list of candidates displayed to the user will usually be smaller than
* the list given by the completer. Thus it is not necessary for the completer
* to do any matching based on the current buffer. On the contrary, in order
* for the typo matcher to work, all possible candidates for the word being
* completed should be returned.
*
* @param reader The line reader
* @param line The parsed command line
* @param candidates The {@link List} of candidates to populate
*/
//@Override
public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) {
// let picocli generate completion candidates for the token where the cursor is at
String[] words = new String[line.words().size()];
words = line.words().toArray(words);
List<CharSequence> cs = new ArrayList<CharSequence>();
AutoComplete.complete(spec,
words,
line.wordIndex(),
0,
line.cursor(),
cs);
for(CharSequence c: cs){
candidates.add(new Candidate((String)c));
}
}
}

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

@ -0,0 +1,13 @@
package picocli.shell.jline3.example;
import it.tdlight.client.InputParameter;
import it.tdlight.client.ParameterInfo;
import java.util.function.Consumer;
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

@ -0,0 +1,738 @@
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.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;
import org.fusesource.jansi.AnsiConsole;
import org.jline.console.SystemRegistry;
import org.jline.console.impl.Builtins;
import org.jline.console.impl.SystemRegistryImpl;
import org.jline.keymap.KeyMap;
import org.jline.reader.Binding;
import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.MaskingCallback;
import org.jline.reader.Parser;
import org.jline.reader.Reference;
import org.jline.reader.UserInterruptException;
import org.jline.reader.impl.DefaultParser;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.widget.TailTipWidgets;
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.
* This example requires JLine 3.16+ and picocli 4.4+.
* <p>
* The built-in {@code PicocliCommands.ClearScreen} command was introduced in picocli 4.6.
* </p>
*/
public class Example {
static ObjectMapper mapper = new ObjectMapper();
static String currentInstance = null;
static boolean waitingLogin = false;
static Map<String, TDLibInstance> instanceMap = new HashMap<>();
static final LinkedBlockingDeque<ClientInteractionRequest> interactionRequests = new LinkedBlockingDeque<>();
private static TDLibInstance getInstance() {
var ci = currentInstance;
return Objects.requireNonNull(instanceMap.get(ci), "No instance is selected.");
}
/**
* Top-level command that just prints help.
*/
@Command(name = "",
description = {
"Example interactive shell with completion and autosuggestions. " +
"Hit @|magenta <TAB>|@ to see available commands.",
"Hit @|magenta ALT-S|@ to toggle tailtips.",
""},
footer = {"", "Press Ctrl-D to exit."},
subcommands = {InstanceCommand.class, ChatsCommand.class,
ChatCommand.class, PicocliCommands.ClearScreen.class,
CommandLine.HelpCommand.class})
static class CliCommands implements Runnable {
PrintWriter out;
CliCommands() {}
public void setReader(LineReader reader){
out = reader.getTerminal().writer();
}
public void run() {
out.println(new CommandLine(this).getUsageMessage());
}
}
@Command(name = "instance", mixinStandardHelpOptions = true, version = "1.0",
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`"})
private boolean[] verbosity = {};
@ParentCommand CliCommands parent;
public void run() {
if (currentInstance == null) {
System.out.println("No instance is active.");
return;
}
System.out.printf("The current instance is %s.%n", currentInstance);
if (verbosity.length > 0) {
System.out.println("Instances:");
for (String instanceName : instanceMap.keySet()) {
System.out.printf("\t- %s%n", instanceName);
}
}
}
@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(API_TOKEN, name, interactionRequests));
}
@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;
}
currentInstance = name;
}
}
@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."})
private Path path;
@Option(names = {"--use-messages-db"},
description = { "Use the messages database."}, negatable = true)
private Boolean useMessagesDb;
@Option(names = {"--use-files-db"},
description = { "Use the files database."}, negatable = true)
private Boolean useFilesDb;
@Option(names = {"--use-chat-info-db"},
description = { "Use the chats database."}, negatable = true)
private Boolean useChatInfoDb;
@Option(names = {"-t", "--use-test-dc"},
description = { "Use the chats database."}, negatable = true)
private Boolean useTestDc;
@ParentCommand
InstanceCommand parent;
public void run() {
if (currentInstance == null) {
System.out.println("No instance is active.");
return;
}
var instance = getInstance();
var settings = instance.getSettings();
if (path != null) {
instance.setPath(path);
System.out.printf("Path set to \"%s\".%n", path);
}
if (useMessagesDb != null) {
settings.setMessageDatabaseEnabled(useMessagesDb);
System.out.printf("Messages database set to: %b.%n", useMessagesDb);
}
if (useFilesDb != null) {
settings.setFileDatabaseEnabled(useFilesDb);
System.out.printf("Files database set to: %b.%n", useFilesDb);
}
if (useChatInfoDb != null) {
settings.setChatInfoDatabaseEnabled(useChatInfoDb);
System.out.printf("Chat info database set to: %b.%n", useChatInfoDb);
}
if (useTestDc != null) {
settings.setUseTestDatacenter(useTestDc);
System.out.printf("Test datacenter set to: %b.%n", useTestDc);
}
}
}
@Command(name = "start", mixinStandardHelpOptions = true, version = "1.0", description = {"Start the instance."})
static class StartCommand implements Runnable {
@ParentCommand InstanceCommand parent;
@ArgGroup(exclusive = true, multiplicity = "1")
private StartCommand.AuthMode authMode = new StartCommand.AuthMode();
static class AuthMode {
@Option(names = {"-q", "--qr"},
description = "QR-code login.")
private boolean qrLogin;
@Option(names = {"-t", "--token"},
description = "Bot token login.")
private String botLogin;
@Option(names = {"-p", "--phone-number"},
description = "User phone number login.")
private String phoneNumberLogin;
}
public void run() {
if (currentInstance == null) {
System.out.println("No instance is active.");
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();
} else if (authMode.botLogin != null) {
authenticationData = AuthenticationData.bot(authMode.botLogin);
} else if (authMode.phoneNumberLogin != null) {
var phone = Long.parseLong(authMode.phoneNumberLogin.replaceAll("[^\\d.]", ""));
authenticationData = AuthenticationData.user(phone);
} else {
throw new UnsupportedOperationException("Unknown choice");
}
waitingLogin = true;
instance.start(authenticationData);
System.out.println("Client started.");
}
}
@Command(name = "stop", mixinStandardHelpOptions = true, version = "1.0", description = {"Stop the instance."})
static class StopCommand implements Runnable {
@ParentCommand InstanceCommand parent;
public void run() {
if (currentInstance == null) {
System.out.println("No instance is active.");
return;
}
var instance = getInstance();
System.out.println("Stopping client...");
try {
instance.stop();
} catch (InterruptedException e) {
throw new RuntimeException("Failed to close", e);
}
System.out.println("Client stopped.");
instanceMap.remove(currentInstance);
currentInstance = null;
}
}
public static void main(String[] args) {
AnsiConsole.systemInstall();
try {
Supplier<Path> workDir = () -> Paths.get(System.getProperty("user.dir"));
// set up JLine built-in commands
Builtins builtins = new Builtins(workDir, null, null);
builtins.rename(Builtins.Command.TTOP, "top");
builtins.alias("zle", "widget");
builtins.alias("bindkey", "keymap");
// set up picocli commands
CliCommands commands = new CliCommands();
PicocliCommandsFactory factory = new PicocliCommandsFactory();
// Or, if you have your own factory, you can chain them like this:
// MyCustomFactory customFactory = createCustomFactory(); // your application custom factory
// PicocliCommandsFactory factory = new PicocliCommandsFactory(customFactory); // chain the factories
CommandLine cmd = new CommandLine(commands, factory);
PicocliCommands picocliCommands = new PicocliCommands(cmd);
Parser parser = new DefaultParser();
try (Terminal terminal = TerminalBuilder.builder().build()) {
SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, workDir, null);
systemRegistry.setCommandRegistries(builtins, picocliCommands);
systemRegistry.register("help", picocliCommands);
LineReader reader = LineReaderBuilder.builder()
.terminal(terminal)
.completer(systemRegistry.completer())
.parser(parser)
.variable(LineReader.LIST_MAX, 50) // max tab completion candidates
.build();
builtins.setLineReader(reader);
commands.setReader(reader);
factory.setTerminal(terminal);
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"));
String prompt;
String rightPrompt = null;
// start the shell and process input until the user quits with Ctrl-D
String line;
shell:
while (true) {
try {
systemRegistry.cleanUp();
if (currentInstance != null) {
prompt = "%s> ".formatted(currentInstance);
} else {
prompt = "jelegram> ";
}
ClientInteractionRequest lastIr;
if (waitingLogin) {
lastIr = interactionRequests.poll(100, TimeUnit.MILLISECONDS);
} else {
lastIr = interactionRequests.poll();
}
if (lastIr != null) {
var requestAndResponse = getInteractionRequest(lastIr);
prompt = requestAndResponse.getKey().orElse(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) {
line = reader.readLine(prompt, null, (MaskingCallback) null, null);
systemRegistry.execute(line);
}
} catch (UserInterruptException e) {
// Ignore
} catch (EndOfFileException e) {
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 customParameter = interactionRequest.parameter();
var parameterInfo = interactionRequest.parameterInfo();
var authenticationData = interactionRequest.instance().getAuthenticationData();
String who;
if (authenticationData.isQrCode()) {
who = "QR login";
} else if (authenticationData.isBot()) {
who = authenticationData.getBotToken().split(":", 2)[0];
} else {
who = "+" + authenticationData.getUserPhoneNumber();
}
String question;
boolean trim = false;
if (customParameter instanceof StandardInputParameter inputParameter) {
var parameter = inputParameter.value();
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("[" + 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("[" + who + "] " + question), Optional.empty());
}
private static String getQr(String url) {
int width = 40;
int height = 40;
Hashtable<EncodeHintType, Object> qrParam = new Hashtable<>();
qrParam.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
qrParam.put(EncodeHintType.CHARACTER_SET, "utf-8");
try {
BitMatrix bitMatrix = new MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, width, height, qrParam);
return toAscii(bitMatrix);
} catch (WriterException ex) {
throw new IllegalStateException("Can't encode QR code", ex);
}
}
private static String toAscii(BitMatrix bitMatrix) {
StringBuilder sb = new StringBuilder();
for (int rows = 0; rows < bitMatrix.getHeight(); rows++) {
for (int cols = 0; cols < bitMatrix.getWidth(); cols++) {
boolean x = bitMatrix.get(rows, cols);
sb.append(x ? " " : "██");
}
sb.append("\n");
}
return sb.toString();
}
}

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

@ -0,0 +1,170 @@
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;
import it.tdlight.common.utils.CantLoadLibrary;
import it.tdlight.jni.TdApi;
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(APIToken apiToken, String name, Queue<ClientInteractionRequest> interactionRequests)
throws CantLoadLibrary {
Init.start();
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) {
this.setPath(Path.of(".").resolve(name));
}
public void setPath(Path path) {
this.path = path;
InstanceSettings.setTdPaths(settings, path);
}
public TDLibSettings getSettings() {
if (client != null) {
throw new UnsupportedOperationException("Client is already started");
}
return settings;
}
public void start(AuthenticationData authenticationData) {
this.authenticationData = authenticationData;
if (client != null) {
throw new UnsupportedOperationException("Client is already started");
}
client = new SimpleTelegramClient(settings);
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) {
case AuthorizationStateReady.CONSTRUCTOR,
AuthorizationStateLoggingOut.CONSTRUCTOR,
AuthorizationStateClosed.CONSTRUCTOR,
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;
}
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>