diff --git a/.gitignore b/.gitignore index f652a866d..4b29dd985 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ auto/ db_backup *.pyc -docs +docs/ diff --git a/example/java/.gitignore b/example/java/.gitignore new file mode 100644 index 000000000..8f846b80d --- /dev/null +++ b/example/java/.gitignore @@ -0,0 +1,5 @@ +**/*build/ +bin/ +docs/ +org/drinkless/tdlib/TdApi.java +td/ diff --git a/example/java/CMakeLists.txt b/example/java/CMakeLists.txt new file mode 100644 index 000000000..f0b0ac6e2 --- /dev/null +++ b/example/java/CMakeLists.txt @@ -0,0 +1,68 @@ +cmake_minimum_required(VERSION 3.1) + +project(TdJavaExample VERSION 1.0 LANGUAGES CXX) + +find_package(Td REQUIRED) + +if (NOT JNI_FOUND) + find_package(JNI REQUIRED) +endif() +message(STATUS "Found JNI: ${JNI_INCLUDE_DIRS} ${JNI_LIBRARIES}") + +if (NOT Java_FOUND) + find_package(Java 1.6 REQUIRED) +endif() +message(STATUS "Found Java: ${Java_JAVAC_EXECUTABLE} ${Java_JAVADOC_EXECUTABLE}") + +# Generating TdApi.java +find_program(PHP_EXECUTABLE php) + +set(TD_API_JAVA_PACKAGE "org/drinkless/tdlib") +set(TD_API_JAVA_PATH ${CMAKE_CURRENT_SOURCE_DIR}) +set(TD_API_TLO_PATH ${CMAKE_CURRENT_SOURCE_DIR}/td/bin/td/generate/scheme/td_api.tlo) +set(TD_API_TL_PATH ${CMAKE_CURRENT_SOURCE_DIR}/td/bin/td/generate/scheme/td_api.tl) +set(JAVADOC_TL_DOCUMENTATION_GENERATOR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/td/bin/td/generate/JavadocTlDocumentationGenerator.php) +set(GENERATE_JAVA_API_CMD ${CMAKE_CURRENT_SOURCE_DIR}/td/bin/td_generate_java_api TdApi ${TD_API_TLO_PATH} ${TD_API_JAVA_PATH} ${TD_API_JAVA_PACKAGE}) +if (PHP_EXECUTABLE) + set(GENERATE_JAVA_API_CMD ${GENERATE_JAVA_API_CMD} && ${PHP_EXECUTABLE} ${JAVADOC_TL_DOCUMENTATION_GENERATOR_PATH} ${TD_API_TL_PATH} ${TD_API_JAVA_PATH}/${TD_API_JAVA_PACKAGE}/TdApi.java) +endif() + +add_custom_target(td_generate_java_api + COMMAND ${GENERATE_JAVA_API_CMD} + COMMENT "Generate Java TDLib API source files" + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/td/bin/td_generate_java_api ${TD_API_TLO_PATH} ${TD_API_TL_PATH} ${JAVADOC_TL_DOCUMENTATION_GENERATOR_PATH} +) + +set(JAVA_SOURCE_PATH "${TD_API_JAVA_PATH}/${TD_API_JAVA_PACKAGE}") +get_filename_component(JAVA_OUTPUT_DIRECTORY ${CMAKE_INSTALL_PREFIX}/bin REALPATH BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}") +file(MAKE_DIRECTORY ${JAVA_OUTPUT_DIRECTORY}) +add_custom_target(build_java + COMMAND ${Java_JAVAC_EXECUTABLE} -d ${JAVA_OUTPUT_DIRECTORY} ${JAVA_SOURCE_PATH}/example/Example.java ${JAVA_SOURCE_PATH}/Client.java ${JAVA_SOURCE_PATH}/Log.java ${JAVA_SOURCE_PATH}/TdApi.java + COMMENT "Build Java code" + DEPENDS td_generate_java_api +) + +set(JAVA_SOURCE_PATH "${TD_API_JAVA_PATH}/${TD_API_JAVA_PACKAGE}") +add_custom_target(generate_javadoc + COMMAND ${Java_JAVADOC_EXECUTABLE} -d ${JAVA_OUTPUT_DIRECTORY}/../docs org.drinkless.tdlib + WORKING_DIRECTORY ${TD_API_JAVA_PATH} + COMMENT "Generate Javadoc documentation" + DEPENDS td_generate_java_api +) + +# Building shared library +add_library(tdjni SHARED + td_jni.cpp +) +target_include_directories(tdjni PRIVATE ${JAVA_INCLUDE_PATH} ${JAVA_INCLUDE_PATH2}) +target_link_libraries(tdjni PRIVATE Td::TdStatic ${JAVA_JVM_LIBRARY}) +target_compile_definitions(tdjni PRIVATE PACKAGE_NAME="${TD_API_JAVA_PACKAGE}") + +set_property(TARGET tdjni PROPERTY CXX_STANDARD 14) + +add_dependencies(tdjni td_generate_java_api build_java generate_javadoc) + +install(TARGETS tdjni + LIBRARY DESTINATION bin + RUNTIME DESTINATION bin +) diff --git a/example/java/README.md b/example/java/README.md new file mode 100644 index 000000000..13b3d65d0 --- /dev/null +++ b/example/java/README.md @@ -0,0 +1,33 @@ +# TDLib Java example + +To run this example, you will need installed JDK >= 1.6. +For Javadoc documentation generation PHP is needed. + +TDLib should be prebuilt for using with Java and installed to local subdirectory `td/`: +``` +cd +mkdir jnibuild +cd jnibuild +cmake -DCMAKE_BUILD_TYPE=Release -DTD_ENABLE_JNI=ON -DCMAKE_INSTALL_PREFIX:PATH=../example/java/td .. +cmake --build . --target install +``` +If you want to compile TDLib for 64-bit Java on Windows, you will also need to add `-G "Visual Studio 14 2015 Win64"` option to CMake. + +Then you can build this example: +``` +cd /example/java +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=Release -DTd_DIR=/example/java/td/lib/cmake/Td -DCMAKE_INSTALL_PREFIX:PATH=.. .. +cmake --build . --target install +``` + +Compiled TDLib shared library and Java example after that will be placed in bin/ and Javadoc documentation in docs/. + +Now you can run Java example: +``` +cd /example/java/bin +java -Djava.library.path=. org/drinkless/tdlib/example/Example +``` + +If you get java.lang.UnsatisfiedLinkError with "Can't find dependent libraries" you may also need to copy some dependent shared libraries to bin/. diff --git a/example/java/org/drinkless/tdlib/Client.java b/example/java/org/drinkless/tdlib/Client.java new file mode 100644 index 000000000..efb38e9c5 --- /dev/null +++ b/example/java/org/drinkless/tdlib/Client.java @@ -0,0 +1,285 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +package org.drinkless.tdlib; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Main class for interaction with the TDLib. + */ +public final class Client implements Runnable { + /** + * Interface for handler for results of queries to TDLib and incoming updates from TDLib. + */ + public interface ResultHandler { + /** + * Callback called on result of query to TDLib or incoming update from TDLib. + * + * @param object Result of query or update of type TdApi.Update about new events. + */ + void onResult(TdApi.Object object); + } + + /** + * Interface for handler of exceptions thrown while invoking ResultHandler. + * By default, all such exceptions are ignored. + * All exceptions thrown from ExceptionHandler are ignored. + */ + public interface ExceptionHandler { + /** + * Callback called on exceptions thrown while invoking ResultHandler. + * + * @param e Exception thrown by ResultHandler. + */ + void onException(Throwable e); + } + + /** + * Sends a request to the TDLib. + * + * @param query Object representing a query to the TDLib. + * @param resultHandler Result handler with onResult method which will be called with result + * of the query or with TdApi.Error as parameter. If it is null, nothing + * will be called. + * @param exceptionHandler Exception handler with onException method which will be called on + * exception thrown from resultHandler. If it is null, then + * defaultExceptionHandler will be called. + * @throws NullPointerException if query is null. + */ + public void send(TdApi.Function query, ResultHandler resultHandler, ExceptionHandler exceptionHandler) { + if (query == null) { + throw new NullPointerException("query is null"); + } + + readLock.lock(); + try { + if (isClientDestroyed) { + if (resultHandler != null) { + handleResult(new TdApi.Error(500, "Client is closed"), resultHandler, exceptionHandler); + } + return; + } + + long queryId = currentQueryId.incrementAndGet(); + handlers.put(queryId, new Handler(resultHandler, exceptionHandler)); + nativeClientSend(nativeClientId, queryId, query); + } finally { + readLock.unlock(); + } + } + + /** + * Sends a request to the TDLib with an empty ExceptionHandler. + * + * @param query Object representing a query to the TDLib. + * @param resultHandler Result handler with onResult method which will be called with result + * of the query or with TdApi.Error as parameter. If it is null, then + * defaultExceptionHandler will be called. + * @throws NullPointerException if query is null. + */ + public void send(TdApi.Function query, ResultHandler resultHandler) { + send(query, resultHandler, null); + } + + /** + * Synchronously executes a TDLib request. Only a few marked accordingly requests can be executed synchronously. + * + * @param query Object representing a query to the TDLib. + * @return request result. + * @throws NullPointerException if query is null. + */ + public static TdApi.Object execute(TdApi.Function query) { + if (query == null) { + throw new NullPointerException("query is null"); + } + return nativeClientExecute(query); + } + + /** + * Replaces handler for incoming updates from the TDLib. + * + * @param updatesHandler Handler with onResult method which will be called for every incoming + * update from the TDLib. + * @param exceptionHandler Exception handler with onException method which will be called on + * exception thrown from updatesHandler, if it is null, defaultExceptionHandler will be invoked. + */ + public void setUpdatesHandler(ResultHandler updatesHandler, ExceptionHandler exceptionHandler) { + handlers.put(0L, new Handler(updatesHandler, exceptionHandler)); + } + + /** + * Replaces handler for incoming updates from the TDLib. Sets empty ExceptionHandler. + * + * @param updatesHandler Handler with onResult method which will be called for every incoming + * update from the TDLib. + */ + public void setUpdatesHandler(ResultHandler updatesHandler) { + setUpdatesHandler(updatesHandler, null); + } + + /** + * Replaces default exception handler to be invoked on exceptions thrown from updatesHandler and all other ResultHandler. + * + * @param defaultExceptionHandler Default exception handler. If null Exceptions are ignored. + */ + public void setDefaultExceptionHandler(Client.ExceptionHandler defaultExceptionHandler) { + this.defaultExceptionHandler = defaultExceptionHandler; + } + + /** + * Overridden method from Runnable, do not call it directly. + */ + @Override + public void run() { + while (!stopFlag) { + receiveQueries(300.0 /*seconds*/); + } + } + + /** + * Creates new Client. + * + * @param updatesHandler Handler for incoming updates. + * @param updatesExceptionHandler Handler for exceptions thrown from updatesHandler. If it is null, exceptions will be iggnored. + * @param defaultExceptionHandler Default handler for exceptions thrown from all ResultHandler. If it is null, exceptions will be iggnored. + * @return created Client + */ + public static Client create(ResultHandler updatesHandler, ExceptionHandler updatesExceptionHandler, ExceptionHandler defaultExceptionHandler) { + Client client = new Client(updatesHandler, updatesExceptionHandler, defaultExceptionHandler); + new Thread(client, "TDLib thread").start(); + return client; + } + + /** + * Closes Client. + */ + public void close() { + writeLock.lock(); + try { + if (isClientDestroyed) { + return; + } + if (!stopFlag) { + send(new TdApi.Close(), null); + } + isClientDestroyed = true; + while (!stopFlag) { + Thread.yield(); + } + while (handlers.size() != 1) { + receiveQueries(300.0); + } + destroyNativeClient(nativeClientId); + } finally { + writeLock.unlock(); + } + } + + private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + private final Lock readLock = readWriteLock.readLock(); + private final Lock writeLock = readWriteLock.writeLock(); + + private volatile boolean stopFlag = false; + private volatile boolean isClientDestroyed = false; + private final long nativeClientId; + + private final ConcurrentHashMap handlers = new ConcurrentHashMap(); + private final AtomicLong currentQueryId = new AtomicLong(); + + private volatile ExceptionHandler defaultExceptionHandler = null; + + private static final int MAX_EVENTS = 1000; + private final long[] eventIds = new long[MAX_EVENTS]; + private final TdApi.Object[] events = new TdApi.Object[MAX_EVENTS]; + + private static class Handler { + final ResultHandler resultHandler; + final ExceptionHandler exceptionHandler; + + Handler(ResultHandler resultHandler, ExceptionHandler exceptionHandler) { + this.resultHandler = resultHandler; + this.exceptionHandler = exceptionHandler; + } + } + + private Client(ResultHandler updatesHandler, ExceptionHandler updateExceptionHandler, ExceptionHandler defaultExceptionHandler) { + nativeClientId = createNativeClient(); + handlers.put(0L, new Handler(updatesHandler, updateExceptionHandler)); + this.defaultExceptionHandler = defaultExceptionHandler; + } + + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } + + private void processResult(long id, TdApi.Object object) { + if (object instanceof TdApi.UpdateAuthorizationState) { + if (((TdApi.UpdateAuthorizationState) object).authorizationState instanceof TdApi.AuthorizationStateClosed) { + stopFlag = true; + } + } + Handler handler; + if (id == 0) { + // update handler stays forever + handler = handlers.get(id); + } else { + handler = handlers.remove(id); + } + if (handler == null) { + return; + } + + handleResult(object, handler.resultHandler, handler.exceptionHandler); + } + + private void handleResult(TdApi.Object object, ResultHandler resultHandler, ExceptionHandler exceptionHandler) { + if (resultHandler == null) { + return; + } + + try { + resultHandler.onResult(object); + } catch (Throwable cause) { + if (exceptionHandler == null) { + exceptionHandler = defaultExceptionHandler; + } + if (exceptionHandler != null) { + try { + exceptionHandler.onException(cause); + } catch (Throwable ignored) { + } + } + } + } + + private void receiveQueries(double timeout) { + int resultN = nativeClientReceive(nativeClientId, eventIds, events, timeout); + for (int i = 0; i < resultN; i++) { + processResult(eventIds[i], events[i]); + events[i] = null; + } + } + + private static native long createNativeClient(); + + private static native void nativeClientSend(long nativeClientId, long eventId, TdApi.Function function); + + private static native int nativeClientReceive(long nativeClientId, long[] eventIds, TdApi.Object[] events, double timeout); + + private static native TdApi.Object nativeClientExecute(TdApi.Function function); + + private static native void destroyNativeClient(long nativeClientId); +} diff --git a/example/java/org/drinkless/tdlib/Log.java b/example/java/org/drinkless/tdlib/Log.java new file mode 100644 index 000000000..1b3b67134 --- /dev/null +++ b/example/java/org/drinkless/tdlib/Log.java @@ -0,0 +1,74 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +package org.drinkless.tdlib; + +/** + * Class for managing internal TDLib logging. + */ +public final class Log { + /** + * Changes TDLib log verbosity. + * + * @param verbosityLevel New value of log verbosity level. Must be non-negative. + * Value 0 corresponds to fatal errors, + * value 1 corresponds to java.util.logging.Level.SEVERE, + * value 2 corresponds to java.util.logging.Level.WARNING, + * value 3 corresponds to java.util.logging.Level.INFO, + * value 4 corresponds to java.util.logging.Level.FINE, + * value 5 corresponds to java.util.logging.Level.FINER, + * value greater than 5 can be used to enable even more logging. + * Default value of the log verbosity level is 5. + */ + public static native void setVerbosityLevel(int verbosityLevel); + + /** + * Sets file path for writing TDLib internal log. By default TDLib writes logs to the System.err. + * Use this method to write the log to a file instead. + * + * @param filePath Path to a file for writing TDLib internal log. Use an empty path to + * switch back to logging to the System.err. + */ + public static native void setFilePath(String filePath); + + /** + * Changes maximum size of TDLib log file. + * + * @param maxFileSize Maximum size of the file to where the internal TDLib log is written + * before the file will be auto-rotated. Must be positive. Defaults to 10 MB. + */ + public static native void setMaxFileSize(long maxFileSize); + + /** + * This function is called from the JNI when a fatal error happens to provide a better error message. + * The function does not return. + * + * @param errorMessage Error message. + */ + private static void onFatalError(String errorMessage) { + class ThrowError implements Runnable { + private ThrowError(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public void run() { + throw new RuntimeException("TDLib fatal error: " + errorMessage); + } + + private final String errorMessage; + } + + new Thread(new ThrowError(errorMessage), "TDLib fatal error thread").start(); + while (true) { + try { + Thread.sleep(1000); // milliseconds + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/example/java/org/drinkless/tdlib/example/Example.java b/example/java/org/drinkless/tdlib/example/Example.java new file mode 100644 index 000000000..d03f6ea8e --- /dev/null +++ b/example/java/org/drinkless/tdlib/example/Example.java @@ -0,0 +1,503 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +package org.drinkless.tdlib.example; + +import org.drinkless.tdlib.Client; +import org.drinkless.tdlib.Log; +import org.drinkless.tdlib.TdApi; + +import java.io.Console; +import java.util.NavigableSet; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Example class for TDLib usage from Java. + */ +public final class Example { + private static Client client = null; + + private static TdApi.AuthorizationState authorizationState = null; + private static volatile boolean haveAuthorization = false; + private static volatile boolean quiting = false; + + private static final Client.ResultHandler defaultHandler = new DefaultHandler(); + + private static final Lock authorizationLock = new ReentrantLock(); + private static final Condition gotAuthorization = authorizationLock.newCondition(); + + private static final ConcurrentMap users = new ConcurrentHashMap(); + private static final ConcurrentMap basicGroups = new ConcurrentHashMap(); + private static final ConcurrentMap supergroups = new ConcurrentHashMap(); + private static final ConcurrentMap secretChats = new ConcurrentHashMap(); + + private static final ConcurrentMap chats = new ConcurrentHashMap(); + private static final NavigableSet chatList = new TreeSet(); + private static boolean haveFullChatList = false; + + private static final ConcurrentMap usersFullInfo = new ConcurrentHashMap(); + private static final ConcurrentMap basicGroupsFullInfo = new ConcurrentHashMap(); + private static final ConcurrentMap supergroupsFullInfo = new ConcurrentHashMap(); + + private static final String newLine = System.getProperty("line.separator"); + + static { + System.loadLibrary("tdjni"); + } + + private static void print(String str) { + System.out.println(); + System.out.println(str); + } + + private static void setChatOrder(TdApi.Chat chat, long order) { + synchronized (chatList) { + if (chat.order != 0) { + boolean isRemoved = chatList.remove(new OrderedChat(chat.order, chat.id)); + assert isRemoved; + } + + chat.order = order; + + if (chat.order != 0) { + boolean isAdded = chatList.add(new OrderedChat(chat.order, chat.id)); + assert isAdded; + } + } + } + + private static void onAuthorizationStateUpdated(TdApi.AuthorizationState authorizationState) { + if (authorizationState != null) { + Example.authorizationState = authorizationState; + } + switch (Example.authorizationState.getConstructor()) { + case TdApi.AuthorizationStateWaitTdlibParameters.CONSTRUCTOR: + TdApi.TdlibParameters parameters = new TdApi.TdlibParameters(); + parameters.useMessageDatabase = true; + parameters.useSecretChats = true; + parameters.apiId = 94575; + parameters.apiHash = "a3406de8d171bb422bb6ddf3bbd800e2"; + parameters.systemLanguageCode = "en"; + parameters.deviceModel = "Desktop"; + parameters.systemVersion = "Unknown"; + parameters.applicationVersion = "1.0"; + parameters.enableStorageOptimizer = true; + + client.send(new TdApi.SetTdlibParameters(parameters), new AuthorizationRequestHandler()); + break; + case TdApi.AuthorizationStateWaitEncryptionKey.CONSTRUCTOR: + client.send(new TdApi.CheckDatabaseEncryptionKey(), new AuthorizationRequestHandler()); + break; + case TdApi.AuthorizationStateWaitPhoneNumber.CONSTRUCTOR: { + Console console = System.console(); + String phoneNumber = console.readLine("Please enter phone number: "); + client.send(new TdApi.SetAuthenticationPhoneNumber(phoneNumber, false, false), new AuthorizationRequestHandler()); + break; + } + case TdApi.AuthorizationStateWaitCode.CONSTRUCTOR: { + Console console = System.console(); + String code = console.readLine("Please enter authentication code: "); + client.send(new TdApi.CheckAuthenticationCode(code, "", ""), new AuthorizationRequestHandler()); + break; + } + case TdApi.AuthorizationStateWaitPassword.CONSTRUCTOR: { + Console console = System.console(); + String password = console.readLine("Please enter password: "); + client.send(new TdApi.CheckAuthenticationPassword(password), new AuthorizationRequestHandler()); + break; + } + case TdApi.AuthorizationStateReady.CONSTRUCTOR: + haveAuthorization = true; + authorizationLock.lock(); + try { + gotAuthorization.signal(); + } finally { + authorizationLock.unlock(); + } + break; + case TdApi.AuthorizationStateLoggingOut.CONSTRUCTOR: + haveAuthorization = false; + print("Logging out"); + break; + case TdApi.AuthorizationStateClosing.CONSTRUCTOR: + haveAuthorization = false; + print("Closing"); + break; + case TdApi.AuthorizationStateClosed.CONSTRUCTOR: + print("Closed"); + if (!quiting) { + client = Client.create(new UpdatesHandler(), null, null); // recreate client after previous has closed + } + break; + default: + System.err.println("Unsupported authorization state:" + newLine + Example.authorizationState); + } + } + + private static int toInt(String arg) { + int result = 0; + try { + result = Integer.parseInt(arg); + } catch (NumberFormatException ignored) { + } + return result; + } + + private static long getChatId(String arg) { + long chatId = 0; + try { + chatId = Long.parseLong(arg); + } catch (NumberFormatException ignored) { + } + return chatId; + } + + private static void getCommand() { + String command = System.console().readLine("Enter command (gcs - GetChats, gc - GetChat, me - GetMe, sm - SendMessage, lo - LogOut, q - Quit): "); + String[] commands = command.split(" ", 2); + switch (commands[0]) { + case "gcs": { + int limit = 20; + if (commands.length > 1) { + limit = toInt(commands[1]); + } + getChatList(limit); + break; + } + case "gc": + client.send(new TdApi.GetChat(getChatId(commands[1])), defaultHandler); + break; + case "me": + client.send(new TdApi.GetMe(), defaultHandler); + break; + case "sm": { + String[] args = commands[1].split(" ", 2); + sendMessage(getChatId(args[0]), args[1]); + break; + } + case "lo": + haveAuthorization = false; + client.send(new TdApi.LogOut(), defaultHandler); + break; + case "q": + quiting = true; + haveAuthorization = false; + client.send(new TdApi.Close(), defaultHandler); + break; + default: + System.err.println("Unsupported command: " + command); + } + } + + private static void getChatList(final int limit) { + synchronized (chatList) { + if (!haveFullChatList && limit > chatList.size()) { + // have enough chats in the chat list or chat list is too small + long offsetOrder = Long.MAX_VALUE; + long offsetChatId = 0; + if (!chatList.isEmpty()) { + OrderedChat last = chatList.last(); + offsetOrder = last.order; + offsetChatId = last.chatId; + } + client.send(new TdApi.GetChats(offsetOrder, offsetChatId, limit - chatList.size()), new Client.ResultHandler() { + @Override + public void onResult(TdApi.Object object) { + switch (object.getConstructor()) { + case TdApi.Error.CONSTRUCTOR: + System.err.println("Receive an error for GetChats:" + newLine + object); + break; + case TdApi.Chats.CONSTRUCTOR: + long[] chatIds = ((TdApi.Chats) object).chatIds; + if (chatIds.length == 0) { + synchronized (chatList) { + haveFullChatList = true; + } + } + // chats had already been received through updates, let's retry request + getChatList(limit); + break; + default: + System.err.println("Receive wrong response from TDLib:" + newLine + object); + } + } + }); + return; + } + + // have enough chats in the chat list to answer request + java.util.Iterator iter = chatList.iterator(); + System.out.println(); + System.out.println("First " + limit + " chat(s) out of " + chatList.size() + " known chat(s):"); + for (int i = 0; i < limit; i++) { + long chatId = iter.next().chatId; + TdApi.Chat chat = chats.get(chatId); + synchronized (chat) { + System.out.println(chatId + ": " + chat.title); + } + } + } + } + + private static void sendMessage(long chatId, String message) { + // initialize reply markup just for testing + TdApi.InlineKeyboardButton[] row = {new TdApi.InlineKeyboardButton("https://telegram.org?1", new TdApi.InlineKeyboardButtonTypeUrl()), new TdApi.InlineKeyboardButton("https://telegram.org?2", new TdApi.InlineKeyboardButtonTypeUrl()), new TdApi.InlineKeyboardButton("https://telegram.org?3", new TdApi.InlineKeyboardButtonTypeUrl())}; + TdApi.ReplyMarkup replyMarkup = new TdApi.ReplyMarkupInlineKeyboard(new TdApi.InlineKeyboardButton[][]{row, row, row}); + + TdApi.InputMessageContent content = new TdApi.InputMessageText(message, false, true, null, null); + client.send(new TdApi.SendMessage(chatId, 0, false, false, replyMarkup, content), defaultHandler); + } + + public static void main(String[] args) throws InterruptedException { + // disable TDLib log + Log.setVerbosityLevel(0); + + // create client + client = Client.create(new UpdatesHandler(), null, null); + + // test Client.execute + defaultHandler.onResult(Client.execute(new TdApi.GetTextEntities("@telegram /test_command https://telegram.org telegram.me @gif @test"))); + + // main loop + while (!quiting) { + // await authorization + authorizationLock.lock(); + try { + while (!haveAuthorization) { + gotAuthorization.await(); + } + } finally { + authorizationLock.unlock(); + } + + while (haveAuthorization) { + getCommand(); + } + } + } + + private static class OrderedChat implements Comparable { + final long order; + final long chatId; + + OrderedChat(long order, long chatId) { + this.order = order; + this.chatId = chatId; + } + + @Override + public int compareTo(OrderedChat o) { + if (this.order != o.order) { + return o.order < this.order ? -1 : 1; + } + if (this.chatId != o.chatId) { + return o.chatId < this.chatId ? -1 : 1; + } + return 0; + } + + @Override + public boolean equals(Object obj) { + OrderedChat o = (OrderedChat) obj; + return this.order == o.order && this.chatId == o.chatId; + } + } + + private static class DefaultHandler implements Client.ResultHandler { + @Override + public void onResult(TdApi.Object object) { + print(object.toString()); + } + } + + private static class UpdatesHandler implements Client.ResultHandler { + @Override + public void onResult(TdApi.Object object) { + switch (object.getConstructor()) { + case TdApi.UpdateAuthorizationState.CONSTRUCTOR: + onAuthorizationStateUpdated(((TdApi.UpdateAuthorizationState) object).authorizationState); + break; + + case TdApi.UpdateUser.CONSTRUCTOR: + TdApi.UpdateUser updateUser = (TdApi.UpdateUser) object; + users.put(updateUser.user.id, updateUser.user); + break; + case TdApi.UpdateUserStatus.CONSTRUCTOR: { + TdApi.UpdateUserStatus updateUserStatus = (TdApi.UpdateUserStatus) object; + TdApi.User user = users.get(updateUserStatus.userId); + synchronized (user) { + user.status = updateUserStatus.status; + } + break; + } + case TdApi.UpdateBasicGroup.CONSTRUCTOR: + TdApi.UpdateBasicGroup updateBasicGroup = (TdApi.UpdateBasicGroup) object; + basicGroups.put(updateBasicGroup.basicGroup.id, updateBasicGroup.basicGroup); + break; + case TdApi.UpdateSupergroup.CONSTRUCTOR: + TdApi.UpdateSupergroup updateSupergroup = (TdApi.UpdateSupergroup) object; + supergroups.put(updateSupergroup.supergroup.id, updateSupergroup.supergroup); + break; + case TdApi.UpdateSecretChat.CONSTRUCTOR: + TdApi.UpdateSecretChat updateSecretChat = (TdApi.UpdateSecretChat) object; + secretChats.put(updateSecretChat.secretChat.id, updateSecretChat.secretChat); + break; + + case TdApi.UpdateNewChat.CONSTRUCTOR: { + TdApi.UpdateNewChat updateNewChat = (TdApi.UpdateNewChat) object; + TdApi.Chat chat = updateNewChat.chat; + synchronized (chat) { + chats.put(chat.id, chat); + + long order = chat.order; + chat.order = 0; + setChatOrder(chat, order); + } + break; + } + case TdApi.UpdateChatTitle.CONSTRUCTOR: { + TdApi.UpdateChatTitle updateChat = (TdApi.UpdateChatTitle) object; + TdApi.Chat chat = chats.get(updateChat.chatId); + synchronized (chat) { + chat.title = updateChat.title; + } + break; + } + case TdApi.UpdateChatPhoto.CONSTRUCTOR: { + TdApi.UpdateChatPhoto updateChat = (TdApi.UpdateChatPhoto) object; + TdApi.Chat chat = chats.get(updateChat.chatId); + synchronized (chat) { + chat.photo = updateChat.photo; + } + break; + } + case TdApi.UpdateChatLastMessage.CONSTRUCTOR: { + TdApi.UpdateChatLastMessage updateChat = (TdApi.UpdateChatLastMessage) object; + TdApi.Chat chat = chats.get(updateChat.chatId); + synchronized (chat) { + chat.lastMessage = updateChat.lastMessage; + setChatOrder(chat, updateChat.order); + } + break; + } + case TdApi.UpdateChatOrder.CONSTRUCTOR: { + TdApi.UpdateChatOrder updateChat = (TdApi.UpdateChatOrder) object; + TdApi.Chat chat = chats.get(updateChat.chatId); + synchronized (chat) { + setChatOrder(chat, updateChat.order); + } + break; + } + case TdApi.UpdateChatIsPinned.CONSTRUCTOR: { + TdApi.UpdateChatIsPinned updateChat = (TdApi.UpdateChatIsPinned) object; + TdApi.Chat chat = chats.get(updateChat.chatId); + synchronized (chat) { + chat.isPinned = updateChat.isPinned; + setChatOrder(chat, updateChat.order); + } + break; + } + case TdApi.UpdateChatReadInbox.CONSTRUCTOR: { + TdApi.UpdateChatReadInbox updateChat = (TdApi.UpdateChatReadInbox) object; + TdApi.Chat chat = chats.get(updateChat.chatId); + synchronized (chat) { + chat.lastReadInboxMessageId = updateChat.lastReadInboxMessageId; + chat.unreadCount = updateChat.unreadCount; + } + break; + } + case TdApi.UpdateChatReadOutbox.CONSTRUCTOR: { + TdApi.UpdateChatReadOutbox updateChat = (TdApi.UpdateChatReadOutbox) object; + TdApi.Chat chat = chats.get(updateChat.chatId); + synchronized (chat) { + chat.lastReadOutboxMessageId = updateChat.lastReadOutboxMessageId; + } + break; + } + case TdApi.UpdateChatUnreadMentionCount.CONSTRUCTOR: { + TdApi.UpdateChatUnreadMentionCount updateChat = (TdApi.UpdateChatUnreadMentionCount) object; + TdApi.Chat chat = chats.get(updateChat.chatId); + synchronized (chat) { + chat.unreadMentionCount = updateChat.unreadMentionCount; + } + break; + } + case TdApi.UpdateMessageMentionRead.CONSTRUCTOR: { + TdApi.UpdateMessageMentionRead updateChat = (TdApi.UpdateMessageMentionRead) object; + TdApi.Chat chat = chats.get(updateChat.chatId); + synchronized (chat) { + chat.unreadMentionCount = updateChat.unreadMentionCount; + } + break; + } + case TdApi.UpdateChatReplyMarkup.CONSTRUCTOR: { + TdApi.UpdateChatReplyMarkup updateChat = (TdApi.UpdateChatReplyMarkup) object; + TdApi.Chat chat = chats.get(updateChat.chatId); + synchronized (chat) { + chat.replyMarkupMessageId = updateChat.replyMarkupMessageId; + } + break; + } + case TdApi.UpdateChatDraftMessage.CONSTRUCTOR: { + TdApi.UpdateChatDraftMessage updateChat = (TdApi.UpdateChatDraftMessage) object; + TdApi.Chat chat = chats.get(updateChat.chatId); + synchronized (chat) { + chat.draftMessage = updateChat.draftMessage; + setChatOrder(chat, updateChat.order); + } + break; + } + case TdApi.UpdateNotificationSettings.CONSTRUCTOR: { + TdApi.UpdateNotificationSettings update = (TdApi.UpdateNotificationSettings) object; + if (update.scope instanceof TdApi.NotificationSettingsScopeChat) { + TdApi.Chat chat = chats.get(((TdApi.NotificationSettingsScopeChat) update.scope).chatId); + synchronized (chat) { + chat.notificationSettings = update.notificationSettings; + } + } + break; + } + + case TdApi.UpdateUserFullInfo.CONSTRUCTOR: + TdApi.UpdateUserFullInfo updateUserFullInfo = (TdApi.UpdateUserFullInfo) object; + usersFullInfo.put(updateUserFullInfo.userId, updateUserFullInfo.userFullInfo); + break; + case TdApi.UpdateBasicGroupFullInfo.CONSTRUCTOR: + TdApi.UpdateBasicGroupFullInfo updateBasicGroupFullInfo = (TdApi.UpdateBasicGroupFullInfo) object; + basicGroupsFullInfo.put(updateBasicGroupFullInfo.basicGroupId, updateBasicGroupFullInfo.basicGroupFullInfo); + break; + case TdApi.UpdateSupergroupFullInfo.CONSTRUCTOR: + TdApi.UpdateSupergroupFullInfo updateSupergroupFullInfo = (TdApi.UpdateSupergroupFullInfo) object; + supergroupsFullInfo.put(updateSupergroupFullInfo.supergroupId, updateSupergroupFullInfo.supergroupFullInfo); + break; + default: + // print("Unsupported update:" + newLine + object); + } + } + } + + private static class AuthorizationRequestHandler implements Client.ResultHandler { + @Override + public void onResult(TdApi.Object object) { + switch (object.getConstructor()) { + case TdApi.Error.CONSTRUCTOR: + System.err.println("Receive an error:" + newLine + object); + onAuthorizationStateUpdated(null); // repeat last action + break; + case TdApi.Ok.CONSTRUCTOR: + // result is already received through UpdateAuthorizationState, nothing to do + break; + default: + System.err.println("Receive wrong response from TDLib:" + newLine + object); + } + } + } +} \ No newline at end of file diff --git a/example/java/td_jni.cpp b/example/java/td_jni.cpp new file mode 100644 index 000000000..ca02d8431 --- /dev/null +++ b/example/java/td_jni.cpp @@ -0,0 +1,157 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2018 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include + +#include + +#include +#include +#include +#include + +namespace td_jni { + +static td::td_api::object_ptr fetch_function(JNIEnv *env, jobject function) { + td::jni::reset_parse_error(); + auto result = td::td_api::Function::fetch(env, function); + if (td::jni::have_parse_error()) { + std::abort(); + } + return result; +} + +static td::Client *get_client(jlong client_id) { + return reinterpret_cast(static_cast(client_id)); +} + +static jlong Client_createNativeClient(JNIEnv *env, jclass clazz) { + return static_cast(reinterpret_cast(new td::Client())); +} + +static void Client_nativeClientSend(JNIEnv *env, jclass clazz, jlong client_id, jlong id, jobject function) { + get_client(client_id)->send({static_cast(id), fetch_function(env, function)}); +} + +static jint Client_nativeClientReceive(JNIEnv *env, jclass clazz, jlong client_id, jlongArray ids, jobjectArray events, + jdouble timeout) { + auto client = get_client(client_id); + jsize events_size = env->GetArrayLength(ids); // ids and events size must be of equal size + jsize result_size = 0; + + auto response = client->receive(timeout); + while (response.object && result_size < events_size) { + jlong result_id = static_cast(response.id); + env->SetLongArrayRegion(ids, result_size, 1, &result_id); + + jobject object; + response.object->store(env, object); + env->SetObjectArrayElement(events, result_size, object); + env->DeleteLocalRef(object); + + result_size++; + response = client->receive(0); + } + return result_size; +} + +static jobject Client_nativeClientExecute(JNIEnv *env, jclass clazz, jobject function) { + jobject result; + td::Client::execute({0, fetch_function(env, function)}).object->store(env, result); + return result; +} + +static void Client_destroyNativeClient(JNIEnv *env, jclass clazz, jlong client_id) { + delete get_client(client_id); +} + +static void Log_setVerbosityLevel(JNIEnv *env, jclass clazz, jint new_log_verbosity_level) { + td::Log::set_verbosity_level(static_cast(new_log_verbosity_level)); +} + +static void Log_setFilePath(JNIEnv *env, jclass clazz, jstring file_path) { + td::Log::set_file_path(td::jni::from_jstring(env, file_path)); +} + +static void Log_setMaxFileSize(JNIEnv *env, jclass clazz, jlong max_file_size) { + td::Log::set_max_file_size(max_file_size); +} + +static jstring Object_toString(JNIEnv *env, jobject object) { + return td::jni::to_jstring(env, to_string(td::td_api::Object::fetch(env, object))); +} + +static jstring Function_toString(JNIEnv *env, jobject object) { + return td::jni::to_jstring(env, to_string(td::td_api::Function::fetch(env, object))); +} + +static constexpr jint JAVA_VERSION = JNI_VERSION_1_6; +static JavaVM *java_vm; +static jclass log_class; + +static void on_fatal_error(const char *error_message) { + auto env = td::jni::get_jni_env(java_vm, JAVA_VERSION); + jmethodID on_fatal_error_method = env->GetStaticMethodID(log_class, "onFatalError", "(Ljava/lang/String;)V"); + if (env && on_fatal_error_method) { + jstring error_str = td::jni::to_jstring(env.get(), error_message); + env->CallStaticVoidMethod(log_class, on_fatal_error_method, error_str); + if (error_str) { + env->DeleteLocalRef(error_str); + } + } +} + +static jint register_native(JavaVM *vm) { + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast(&env), JAVA_VERSION) != JNI_OK) { + return -1; + } + + java_vm = vm; + + auto register_method = [env](jclass clazz, std::string name, std::string signature, auto function_ptr) { + td::jni::register_native_method(env, clazz, std::move(name), std::move(signature), + reinterpret_cast(function_ptr)); + }; + + auto client_class = td::jni::get_jclass(env, PACKAGE_NAME "/Client"); + log_class = td::jni::get_jclass(env, PACKAGE_NAME "/Log"); + auto object_class = td::jni::get_jclass(env, PACKAGE_NAME "/TdApi$Object"); + auto function_class = td::jni::get_jclass(env, PACKAGE_NAME "/TdApi$Function"); + +#define TD_OBJECT "L" PACKAGE_NAME "/TdApi$Object;" +#define TD_FUNCTION "L" PACKAGE_NAME "/TdApi$Function;" + register_method(client_class, "createNativeClient", "()J", Client_createNativeClient); + register_method(client_class, "nativeClientSend", "(JJ" TD_FUNCTION ")V", Client_nativeClientSend); + register_method(client_class, "nativeClientReceive", "(J[J[" TD_OBJECT "D)I", Client_nativeClientReceive); + register_method(client_class, "nativeClientExecute", "(" TD_FUNCTION ")" TD_OBJECT, Client_nativeClientExecute); + register_method(client_class, "destroyNativeClient", "(J)V", Client_destroyNativeClient); + + register_method(log_class, "setVerbosityLevel", "(I)V", Log_setVerbosityLevel); + register_method(log_class, "setFilePath", "(Ljava/lang/String;)V", Log_setFilePath); + register_method(log_class, "setMaxFileSize", "(J)V", Log_setMaxFileSize); + + register_method(object_class, "toString", "()Ljava/lang/String;", Object_toString); + + register_method(function_class, "toString", "()Ljava/lang/String;", Function_toString); +#undef TD_FUNCTION +#undef TD_OBJECT + + td::jni::init_vars(env, PACKAGE_NAME); + td::td_api::Object::init_jni_vars(env, PACKAGE_NAME); + td::td_api::Function::init_jni_vars(env, PACKAGE_NAME); + td::Log::set_fatal_error_callback(on_fatal_error); + + return JAVA_VERSION; +} + +} // namespace td_jni + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + static jint jni_version = td_jni::register_native(vm); // call_once + return jni_version; +}