From 74559bab15af2ccb2cdadadc4467ce3fac97f4b9 Mon Sep 17 00:00:00 2001 From: levlam Date: Tue, 3 Nov 2020 19:34:10 +0300 Subject: [PATCH] Telegram Bot APi server initial commit. --- .clang-format | 122 + .gitattributes | 7 + .gitignore | 5 + .gitmodules | 3 + CMakeLists.txt | 80 + LICENSE_1_0.txt | 23 + README.md | 90 + build.html | 494 ++ td | 1 + telegram-bot-api/Client.cpp | 9039 +++++++++++++++++++++++ telegram-bot-api/Client.h | 935 +++ telegram-bot-api/ClientManager.cpp | 406 + telegram-bot-api/ClientManager.h | 80 + telegram-bot-api/ClientParameters.h | 74 + telegram-bot-api/HttpConnection.cpp | 97 + telegram-bot-api/HttpConnection.h | 53 + telegram-bot-api/HttpServer.h | 72 + telegram-bot-api/HttpStatConnection.cpp | 53 + telegram-bot-api/HttpStatConnection.h | 40 + telegram-bot-api/Query.cpp | 119 + telegram-bot-api/Query.h | 278 + telegram-bot-api/Stats.cpp | 159 + telegram-bot-api/Stats.h | 197 + telegram-bot-api/WebhookActor.cpp | 751 ++ telegram-bot-api/WebhookActor.h | 230 + telegram-bot-api/telegram-bot-api.cpp | 523 ++ 26 files changed, 13931 insertions(+) create mode 100644 .clang-format create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 LICENSE_1_0.txt create mode 100644 README.md create mode 100644 build.html create mode 160000 td create mode 100644 telegram-bot-api/Client.cpp create mode 100644 telegram-bot-api/Client.h create mode 100644 telegram-bot-api/ClientManager.cpp create mode 100644 telegram-bot-api/ClientManager.h create mode 100644 telegram-bot-api/ClientParameters.h create mode 100644 telegram-bot-api/HttpConnection.cpp create mode 100644 telegram-bot-api/HttpConnection.h create mode 100644 telegram-bot-api/HttpServer.h create mode 100644 telegram-bot-api/HttpStatConnection.cpp create mode 100644 telegram-bot-api/HttpStatConnection.h create mode 100644 telegram-bot-api/Query.cpp create mode 100644 telegram-bot-api/Query.h create mode 100644 telegram-bot-api/Stats.cpp create mode 100644 telegram-bot-api/Stats.h create mode 100644 telegram-bot-api/WebhookActor.cpp create mode 100644 telegram-bot-api/WebhookActor.h create mode 100644 telegram-bot-api/telegram-bot-api.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..78d519b --- /dev/null +++ b/.clang-format @@ -0,0 +1,122 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None # All +AllowShortIfStatementsOnASingleLine: Never # WithoutElse +AllowShortLambdasOnASingleLine: Inline # All +AllowShortLoopsOnASingleLine: false # true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: true # false +BreakInheritanceList: BeforeComma # BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: true # false +BreakConstructorInitializers: BeforeComma # BeforeColon +# BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 # 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: true +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - Q_FOREACH_THIS_LIST_MUST_BE_NON_EMPTY +IncludeBlocks: Preserve +#IndentCaseBlocks: false +IndentCaseLabels: true +IndentGotoLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +# InsertTrailingCommas: None +# JavaScriptQuotes: Leave +# JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +# ObjCBinPackProtocolList: Never +# ObjCBlockIndentWidth: 2 +# ObjCBreakBeforeNestedBlockParam: true +# ObjCSpaceAfterProperty: false +# ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +ReflowComments: false # true +SortIncludes: false # disabled, because we need case insensitive sort +SortUsingDeclarations: false # true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 100 # 8 +UseCRLF: false +UseTab: Never +... diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bc0e1bd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +* text=auto + +*.cpp text whitespace=blank-at-eol,space-before-tab,blank-at-eof,tab-in-indent +*.h text whitespace=blank-at-eol,space-before-tab,blank-at-eof,tab-in-indent +*.md text whitespace=blank-at-eol,space-before-tab,blank-at-eof,tab-in-indent +*.txt text whitespace=blank-at-eol,space-before-tab,blank-at-eof,tab-in-indent +*.html text whitespace=blank-at-eol,space-before-tab,blank-at-eof,tab-in-indent diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..282cabc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +**/*build*/ +**/.*.swp +**/.DS_Store +bin/ +vcpkg/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c009a81 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "td"] + path = td + url = http://github.com/tdlib/td.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2d34610 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,80 @@ +cmake_minimum_required(VERSION 3.0.2 FATAL_ERROR) + +project(TelegramBotApi VERSION 5.0 LANGUAGES CXX) + +add_subdirectory(td EXCLUDE_FROM_ALL) + +if (NOT DEFINED CMAKE_MODULE_PATH) + set(CMAKE_MODULE_PATH "") +endif() +set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/td/CMake" "${CMAKE_MODULE_PATH}") + +if (NOT DEFINED CMAKE_INSTALL_BINDIR) + set(CMAKE_INSTALL_BINDIR "bin") +endif() + +if (POLICY CMP0054) + # do not expand quoted arguments + cmake_policy(SET CMP0054 NEW) +endif() +if (POLICY CMP0060) + # link libraries by full path + cmake_policy(SET CMP0060 NEW) +endif() + +include(PreventInSourceBuild) +prevent_in_source_build() + +set(CMAKE_THREAD_PREFER_PTHREAD ON) +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +if (THREADS_HAVE_PTHREAD_ARG) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") +endif() + +include(TdSetUpCompiler) +td_set_up_compiler() + +if (CLANG OR GCC) + if (MEMPROF) + include(CheckCXXCompilerFlag) + check_cxx_compiler_flag(-no-pie CXX_NO_PIE_FLAG) + if (CXX_NO_PIE_FLAG) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -no-pie") + elseif (APPLE) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-no_pie") + endif() + endif() +endif() + +set(TG_HTTP_CLIENT_SOURCE + telegram-bot-api/telegram-bot-api.cpp + + telegram-bot-api/Client.cpp + telegram-bot-api/ClientManager.cpp + telegram-bot-api/HttpConnection.cpp + telegram-bot-api/HttpStatConnection.cpp + telegram-bot-api/Query.cpp + telegram-bot-api/Stats.cpp + telegram-bot-api/WebhookActor.cpp + + telegram-bot-api/Client.h + telegram-bot-api/ClientManager.h + telegram-bot-api/ClientParameters.h + telegram-bot-api/HttpConnection.h + telegram-bot-api/HttpServer.h + telegram-bot-api/HttpStatConnection.h + telegram-bot-api/Query.h + telegram-bot-api/Stats.h + telegram-bot-api/WebhookActor.h +) + +add_executable(telegram-bot-api ${TG_HTTP_CLIENT_SOURCE}) +target_include_directories(telegram-bot-api PRIVATE $) +target_link_libraries(telegram-bot-api PRIVATE memprof tdactor tdcore tddb tdnet tdutils) + +install(TARGETS telegram-bot-api RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") +if (MSVC AND VCPKG_TOOLCHAIN) + install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/$/" DESTINATION "${CMAKE_INSTALL_BINDIR}" FILES_MATCHING PATTERN "*.dll") +endif() diff --git a/LICENSE_1_0.txt b/LICENSE_1_0.txt new file mode 100644 index 0000000..36b7cd9 --- /dev/null +++ b/LICENSE_1_0.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6adf1d --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# Telegram Bot API + +The Telegram Bot API provides an HTTP API for creating [Telegram Bots](https://core.telegram.org/bots). + +If you've got any questions about bots or would like to report an issue with your bot, kindly contact us at [@BotSupport](https://t.me/BotSupport) in Telegram. + +Please note that only global Bot API issues that affect all bots are suitable for this repository. + +## Table of Contents +- [Installation](#installation) +- [Dependencies](#dependencies) +- [Usage](#usage) +- [Documentation](#documentation) +- [Moving a bot to a local server](#switching) +- [Moving a bot from one local server to another](#moving) +- [License](#license) + + +## Installation + +The simplest way to build and install `Telegram Bot API server` is to use our [Telegram Bot API server build instructions generator](https://tdlib.github.io/telegram-bot-api/build.html). +If you do that, you'll only need to choose the target operating system to receive the complete build instructions. + +In general, you need to install all `Telegram Bot API server` [dependencies](#dependencies) and compile the source code using CMake: + +``` +git clone --recursive https://github.com/tdlib/telegram-bot-api.git +cd telegram-bot-api +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=Release .. +cmake --build . --target install +``` + + +## Dependencies +To build and run `Telegram Bot API server` you will need: + +* OpenSSL +* zlib +* C++14 compatible compiler (e.g., Clang 3.4+, GCC 4.9+, MSVC 19.0+ (Visual Studio 2015+), Intel C++ Compiler 17+) (build only) +* gperf (build only) +* CMake (3.0.2+, build only) + + +## Usage + +Use `telegram-bot-api --help` to receive the list of all available options of the Telegram Bot API server. + +The only mandatory options are `--api-id` and `--api-hash`. You must obtain your own `api_id` and `api_hash` +as described in https://core.telegram.org/api/obtaining_api_id and specify them using the `--api-id` and `--api-hash` options +or the `TELEGRAM_API_ID` and `TELEGRAM_API_HASH` environment variables. + +To enable Bot API features not available at `https://api.telegram.org`, such as downloading files without a size limit, specify the option `--local`. + +The Telegram Bot API server accepts only HTTP requests, so a TLS termination proxy needs to be used to handle remote HTTPS requests. + +By default the Telegram Bot API server is launched on the port 8081, which can be changed using the option `--http-port`. + + +## Documentation +See [Bots: An introduction for developers](https://core.telegram.org/bots) for a brief description of Telegram Bots and their features. + +See the [Telegram Bot API documentation](https://core.telegram.org/bots/api) for a description of the Bot API interface and a complete list of available classes, methods and updates. + +See the [Telegram Bot API server build instructions generator](https://tdlib.github.io/telegram-bot-api/build.html) for detailed instructions on how to build the Telegram Bot API server. + +Subscribe to [@BotNews](https://t.me/botnews) to be the first to know about the latest updates and join the discussion in [@BotTalk](https://t.me/bottalk). + + +## Moving a bot to a local server + +To guarantee that your bot will receive all updates, you must deregister it with the `https://api.telegram.org` server by calling the method [logOut](https://core.telegram.org/bots/api#logout). +After the bot is logged out, you can replace the address to which the bot sends requests with the address of your local server and use it in the usual way. +If the server is launched in `--local` mode, make sure that the bot can correctly handle absolute file paths in response to `getFile` requests. + + +## Moving a bot from one local server to another + +If the bot is logged in on more than one server simultaneously, there is no guarantee that it will receive all updates. +To move a bot from one local server to another you can use the method [logOut](https://core.telegram.org/bots/api#logout) to log out on the old server before switching to the new one. + +If you want to avoid losing updates between logging out on the old server and launching on the new server, you can remove the bot's webhook using the method +[deleteWebhook](https://core.telegram.org/bots/api#deletewebhook), then use the method [close](https://core.telegram.org/bots/api#close) to close the bot instance. +After the instance is closed, locate the bot's subdirectory in the working directory of the old server by the bot's user ID, move the subdirectory to the working directory of the new server +and continue sending requests to the new server as usual. + + +## License +`Telegram Bot API server` source code is licensed under the terms of the Boost Software License. See [LICENSE_1_0.txt](http://www.boost.org/LICENSE_1_0.txt) for more information. diff --git a/build.html b/build.html new file mode 100644 index 0000000..a81e39e --- /dev/null +++ b/build.html @@ -0,0 +1,494 @@ + + + + +Telegram Bot API server build instructions + + + + + +
+
+

Choose an operating system, on which you want to use the Telegram Bot API server:

+ +

+
+ +
+

Choose a Linux distro, on which you want to use the Telegram Bot API server:

+ +

+
+ +
+
+ +
+ +
+ +
+ +

+ +
+ Choose which compiler you want to use to build the Telegram Bot API server:
+ + +

+
+ +
+ Choose which shell application you want to use for building:
+ + +

+
+ +
+ Choose which shell application you want to use for building:
+ + +

+
+ +
+ Choose for which bitness you want to build the Telegram Bot API server:
+ + +

+
+ +
+ +
+ +

+
+ +
+

Hidden text

+
+ +
+

Hidden text

+ Empty commands +
+
+ + + + + diff --git a/td b/td new file mode 160000 index 0000000..c9a70fc --- /dev/null +++ b/td @@ -0,0 +1 @@ +Subproject commit c9a70fcd49757084a0c3124e610e92bf0586894b diff --git a/telegram-bot-api/Client.cpp b/telegram-bot-api/Client.cpp new file mode 100644 index 0000000..83f88ae --- /dev/null +++ b/telegram-bot-api/Client.cpp @@ -0,0 +1,9039 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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 "telegram-bot-api/Client.h" + +#include "telegram-bot-api/ClientParameters.h" + +#include "td/actor/PromiseFuture.h" +#include "td/actor/SleepActor.h" + +#include "td/db/TQueue.h" + +#include "td/utils/base64.h" +#include "td/utils/filesystem.h" +#include "td/utils/HttpUrl.h" +#include "td/utils/JsonBuilder.h" +#include "td/utils/logging.h" +#include "td/utils/misc.h" +#include "td/utils/PathView.h" +#include "td/utils/port/Clocks.h" +#include "td/utils/port/path.h" +#include "td/utils/port/Stat.h" +#include "td/utils/Random.h" +#include "td/utils/Slice.h" +#include "td/utils/Span.h" +#include "td/utils/StackAllocator.h" +#include "td/utils/Status.h" +#include "td/utils/StringBuilder.h" +#include "td/utils/Time.h" +#include "td/utils/utf8.h" + +#include + +namespace telegram_bot_api { + +using td::Jsonable; +using td::JsonValueScope; +using td::JsonValue; + +using td_api::make_object; +using td_api::move_object_as; + +void Client::fail_query_with_error(PromisedQueryPtr query, int32 error_code, Slice error_message, + Slice default_message) { + if (error_code == 429) { + Slice prefix = "Too Many Requests: retry after "; + if (begins_with(error_message, prefix)) { + auto r_retry_after = td::to_integer_safe(error_message.substr(prefix.size())); + if (r_retry_after.is_ok() && r_retry_after.ok() > 0) { + return query->set_retry_after_error(r_retry_after.ok()); + } + } + LOG(ERROR) << "Wrong error message: " << error_message; + return fail_query(500, error_message, std::move(query)); + } + int32 real_error_code = error_code; + Slice real_error_message = error_message; + if (error_code < 300 || error_code == 404) { + error_code = 400; + } + if (error_code == 400) { + if (!default_message.empty()) { + error_message = default_message; + } + if (error_message == "MESSAGE_NOT_MODIFIED") { + error_message = Slice( + "message is not modified: specified new message content and reply markup are exactly the same as a current " + "content and reply markup of the message"); + } else if (error_message == "WC_CONVERT_URL_INVALID" || error_message == "EXTERNAL_URL_INVALID") { + error_message = "Wrong HTTP URL specified"; + } else if (error_message == "WEBPAGE_CURL_FAILED") { + error_message = "Failed to get HTTP URL content"; + } else if (error_message == "WEBPAGE_MEDIA_EMPTY") { + error_message = "Wrong type of the web page content"; + } else if (error_message == "MEDIA_GROUPED_INVALID") { + error_message = "Can't use the media of the specified type in the album"; + } else if (error_message == "REPLY_MARKUP_TOO_LONG") { + error_message = Slice("reply markup is too long"); + } else if (error_message == "INPUT_USER_DEACTIVATED") { + error_code = 403; + error_message = Slice("Forbidden: user is deactivated"); + } else if (error_message == "USER_IS_BLOCKED") { + error_code = 403; + error_message = Slice("bot was blocked by the user"); + } else if (error_message == "USER_ADMIN_INVALID") { + error_code = 400; + error_message = Slice("user is an administrator of the chat"); + } else if (error_message == "File generation failed") { + error_code = 400; + error_message = Slice("can't upload file by URL"); + } else if (error_message == "CHAT_ABOUT_NOT_MODIFIED") { + error_code = 400; + error_message = Slice("chat description is not modified"); + } else if (error_message == "PACK_SHORT_NAME_INVALID") { + error_code = 400; + error_message = Slice("invalid sticker set name is specified"); + } else if (error_message == "PACK_SHORT_NAME_OCCUPIED") { + error_code = 400; + error_message = Slice("sticker set name is already occupied"); + } else if (error_message == "STICKER_EMOJI_INVALID") { + error_code = 400; + error_message = Slice("invalid sticker emojis"); + } else if (error_message == "QUERY_ID_INVALID") { + error_code = 400; + error_message = Slice("query is too old and response timeout expired or query ID is invalid"); + } else if (error_message == "MESSAGE_DELETE_FORBIDDEN") { + error_code = 400; + error_message = Slice("message can't be deleted"); + } else if (error_message == "Requested data is inaccessible") { + LOG(ERROR) << "Receive 'Requested data is inaccessible' from " << *query; + } + } + Slice prefix; + switch (error_code) { + case 400: + prefix = Slice("Bad Request"); + break; + case 401: + prefix = Slice("Unauthorized"); + break; + case 403: + prefix = Slice("Forbidden"); + break; + case 500: + prefix = Slice("Internal Server Error"); + break; + default: + LOG(ERROR) << "Unsupported error " << real_error_code << ": " << real_error_message; + return fail_query(400, PSLICE() << "Bad Request: " << error_message, std::move(query)); + } + + if (begins_with(error_message, prefix)) { + return fail_query(error_code, error_message, std::move(query)); + } else { + td::string error_str = prefix.str(); + if (error_message.empty()) { + LOG(ERROR) << "Empty error message with code " << real_error_code; + } else { + error_str += ": "; + if (error_message.size() >= 2u && + (error_message[1] == '_' || ('A' <= error_message[1] && error_message[1] <= 'Z'))) { + error_str += error_message.str(); + } else { + error_str += td::to_lower(error_message[0]); + error_str += error_message.substr(1).str(); + } + } + return fail_query(error_code, error_str, std::move(query)); + } +} + +void Client::fail_query_with_error(PromisedQueryPtr &&query, object_ptr error, Slice default_message) { + fail_query_with_error(std::move(query), error->code_, error->message_, default_message); +} + +Client::Client(td::ActorShared<> parent, const td::string &bot_token, bool is_test_dc, int64 tqueue_id, + std::shared_ptr parameters, td::ActorId stat_actor) + : parent_(std::move(parent)) + , bot_token_(bot_token) + , bot_token_id_("") + , is_test_dc_(is_test_dc) + , tqueue_id_(tqueue_id) + , parameters_(std::move(parameters)) + , stat_actor_(std::move(stat_actor)) { + messages_lru_root_.lru_next = &messages_lru_root_; + messages_lru_root_.lru_prev = &messages_lru_root_; + + static auto is_inited = init_methods(); + CHECK(is_inited); +} + +bool Client::init_methods() { + methods_.emplace("getme", &Client::process_get_me_query); + methods_.emplace("getmycommands", &Client::process_get_my_commands_query); + methods_.emplace("setmycommands", &Client::process_set_my_commands_query); + methods_.emplace("getuserprofilephotos", &Client::process_get_user_profile_photos_query); + methods_.emplace("sendmessage", &Client::process_send_message_query); + methods_.emplace("sendanimation", &Client::process_send_animation_query); + methods_.emplace("sendaudio", &Client::process_send_audio_query); + methods_.emplace("senddice", &Client::process_send_dice_query); + methods_.emplace("senddocument", &Client::process_send_document_query); + methods_.emplace("sendphoto", &Client::process_send_photo_query); + methods_.emplace("sendsticker", &Client::process_send_sticker_query); + methods_.emplace("sendvideo", &Client::process_send_video_query); + methods_.emplace("sendvideonote", &Client::process_send_video_note_query); + methods_.emplace("sendvoice", &Client::process_send_voice_query); + methods_.emplace("sendgame", &Client::process_send_game_query); + methods_.emplace("sendinvoice", &Client::process_send_invoice_query); + methods_.emplace("sendlocation", &Client::process_send_location_query); + methods_.emplace("sendvenue", &Client::process_send_venue_query); + methods_.emplace("sendcontact", &Client::process_send_contact_query); + methods_.emplace("sendpoll", &Client::process_send_poll_query); + methods_.emplace("stoppoll", &Client::process_stop_poll_query); + methods_.emplace("copymessage", &Client::process_copy_message_query); + methods_.emplace("forwardmessage", &Client::process_forward_message_query); + methods_.emplace("sendmediagroup", &Client::process_send_media_group_query); + methods_.emplace("sendchataction", &Client::process_send_chat_action_query); + methods_.emplace("editmessagetext", &Client::process_edit_message_text_query); + methods_.emplace("editmessagelivelocation", &Client::process_edit_message_live_location_query); + methods_.emplace("stopmessagelivelocation", &Client::process_edit_message_live_location_query); + methods_.emplace("editmessagemedia", &Client::process_edit_message_media_query); + methods_.emplace("editmessagecaption", &Client::process_edit_message_caption_query); + methods_.emplace("editmessagereplymarkup", &Client::process_edit_message_reply_markup_query); + methods_.emplace("deletemessage", &Client::process_delete_message_query); + methods_.emplace("setgamescore", &Client::process_set_game_score_query); + methods_.emplace("getgamehighscores", &Client::process_get_game_high_scores_query); + methods_.emplace("answerinlinequery", &Client::process_answer_inline_query_query); + methods_.emplace("answercallbackquery", &Client::process_answer_callback_query_query); + methods_.emplace("answershippingquery", &Client::process_answer_shipping_query_query); + methods_.emplace("answerprecheckoutquery", &Client::process_answer_pre_checkout_query_query); + methods_.emplace("exportchatinvitelink", &Client::process_export_chat_invite_link_query); + methods_.emplace("getchat", &Client::process_get_chat_query); + methods_.emplace("setchatphoto", &Client::process_set_chat_photo_query); + methods_.emplace("deletechatphoto", &Client::process_delete_chat_photo_query); + methods_.emplace("setchattitle", &Client::process_set_chat_title_query); + methods_.emplace("setchatpermissions", &Client::process_set_chat_permissions_query); + methods_.emplace("setchatdescription", &Client::process_set_chat_description_query); + methods_.emplace("pinchatmessage", &Client::process_pin_chat_message_query); + methods_.emplace("unpinchatmessage", &Client::process_unpin_chat_message_query); + methods_.emplace("unpinallchatmessages", &Client::process_unpin_all_chat_messages_query); + methods_.emplace("setchatstickerset", &Client::process_set_chat_sticker_set_query); + methods_.emplace("deletechatstickerset", &Client::process_delete_chat_sticker_set_query); + methods_.emplace("getchatmember", &Client::process_get_chat_member_query); + methods_.emplace("getchatadministrators", &Client::process_get_chat_administrators_query); + methods_.emplace("getchatmembercount", &Client::process_get_chat_member_count_query); + methods_.emplace("getchatmemberscount", &Client::process_get_chat_member_count_query); + methods_.emplace("leavechat", &Client::process_leave_chat_query); + methods_.emplace("promotechatmember", &Client::process_promote_chat_member_query); + methods_.emplace("setchatadministratorcustomtitle", &Client::process_set_chat_administrator_custom_title_query); + methods_.emplace("banchatmember", &Client::process_ban_chat_member_query); + methods_.emplace("kickchatmember", &Client::process_ban_chat_member_query); + methods_.emplace("restrictchatmember", &Client::process_restrict_chat_member_query); + methods_.emplace("unbanchatmember", &Client::process_unban_chat_member_query); + methods_.emplace("getstickerset", &Client::process_get_sticker_set_query); + methods_.emplace("uploadstickerfile", &Client::process_upload_sticker_file_query); + methods_.emplace("createnewstickerset", &Client::process_create_new_sticker_set_query); + methods_.emplace("addstickertoset", &Client::process_add_sticker_to_set_query); + methods_.emplace("setstickersetthumb", &Client::process_set_sticker_set_thumb_query); + methods_.emplace("setstickerpositioninset", &Client::process_set_sticker_position_in_set_query); + methods_.emplace("deletestickerfromset", &Client::process_delete_sticker_from_set_query); + methods_.emplace("setpassportdataerrors", &Client::process_set_passport_data_errors_query); + methods_.emplace("sendcustomrequest", &Client::process_send_custom_request_query); + methods_.emplace("answercustomquery", &Client::process_answer_custom_query_query); + methods_.emplace("getupdates", &Client::process_get_updates_query); + methods_.emplace("setwebhook", &Client::process_set_webhook_query); + methods_.emplace("deletewebhook", &Client::process_set_webhook_query); + methods_.emplace("getwebhookinfo", &Client::process_get_webhook_info_query); + methods_.emplace("getfile", &Client::process_get_file_query); + return true; +} + +class Client::JsonFile : public Jsonable { + public: + JsonFile(const td_api::file *file, const Client *client) : file_(file), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + client_->json_store_file(object, file_, true); + } + + private: + const td_api::file *file_; + const Client *client_; +}; + +class Client::JsonDatedFile : public Jsonable { + public: + JsonDatedFile(const td_api::datedFile *file, const Client *client) : file_(file), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + client_->json_store_file(object, file_->file_.get()); + object("file_date", file_->date_); + } + + private: + const td_api::datedFile *file_; + const Client *client_; +}; + +class Client::JsonDatedFiles : public Jsonable { + public: + JsonDatedFiles(const td::vector> &files, const Client *client) + : files_(files), client_(client) { + } + void store(JsonValueScope *scope) const { + auto array = scope->enter_array(); + for (auto &file : files_) { + array << JsonDatedFile(file.get(), client_); + } + } + + private: + const td::vector> &files_; + const Client *client_; +}; + +class Client::JsonUser : public Jsonable { + public: + JsonUser(int32 user_id, const Client *client, bool full_bot_info = false) + : user_id_(user_id), client_(client), full_bot_info_(full_bot_info) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + auto user_info = client_->get_user_info(user_id_); + object("id", user_id_); + bool is_bot = user_info != nullptr && user_info->type == UserInfo::Type::Bot; + object("is_bot", td::JsonBool(is_bot)); + object("first_name", user_info == nullptr ? "" : user_info->first_name); + if (user_info != nullptr && !user_info->last_name.empty()) { + object("last_name", user_info->last_name); + } + if (user_info != nullptr && !user_info->username.empty()) { + object("username", user_info->username); + } + if (user_info != nullptr && !user_info->language_code.empty()) { + object("language_code", user_info->language_code); + } + if (is_bot && full_bot_info_) { + object("can_join_groups", td::JsonBool(user_info->can_join_groups)); + object("can_read_all_group_messages", td::JsonBool(user_info->can_read_all_group_messages)); + object("supports_inline_queries", td::JsonBool(user_info->is_inline_bot)); + } + } + + private: + int32 user_id_; + const Client *client_; + bool full_bot_info_; +}; + +class Client::JsonUsers : public Jsonable { + public: + JsonUsers(const td::vector &user_ids, const Client *client) : user_ids_(user_ids), client_(client) { + } + void store(JsonValueScope *scope) const { + auto array = scope->enter_array(); + for (auto &user_id : user_ids_) { + array << JsonUser(user_id, client_); + } + } + + private: + const td::vector &user_ids_; + const Client *client_; +}; + +class Client::JsonEntity : public Jsonable { + public: + JsonEntity(const td_api::textEntity *entity, const Client *client) : entity_(entity), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("offset", entity_->offset_); + object("length", entity_->length_); + switch (entity_->type_->get_id()) { + case td_api::textEntityTypeMention::ID: + object("type", "mention"); + break; + case td_api::textEntityTypeHashtag::ID: + object("type", "hashtag"); + break; + case td_api::textEntityTypeCashtag::ID: + object("type", "cashtag"); + break; + case td_api::textEntityTypeBotCommand::ID: + object("type", "bot_command"); + break; + case td_api::textEntityTypeUrl::ID: + object("type", "url"); + break; + case td_api::textEntityTypeEmailAddress::ID: + object("type", "email"); + break; + case td_api::textEntityTypePhoneNumber::ID: + object("type", "phone_number"); + break; + case td_api::textEntityTypeBankCardNumber::ID: + object("type", "bank_card_number"); + break; + case td_api::textEntityTypeBold::ID: + object("type", "bold"); + break; + case td_api::textEntityTypeItalic::ID: + object("type", "italic"); + break; + case td_api::textEntityTypeUnderline::ID: + object("type", "underline"); + break; + case td_api::textEntityTypeStrikethrough::ID: + object("type", "strikethrough"); + break; + case td_api::textEntityTypeCode::ID: + object("type", "code"); + break; + case td_api::textEntityTypePre::ID: + object("type", "pre"); + break; + case td_api::textEntityTypePreCode::ID: { + auto entity = static_cast(entity_->type_.get()); + object("type", "pre"); + object("language", entity->language_); + break; + } + case td_api::textEntityTypeTextUrl::ID: { + auto entity = static_cast(entity_->type_.get()); + object("type", "text_link"); + object("url", entity->url_); + break; + } + case td_api::textEntityTypeMentionName::ID: { + auto entity = static_cast(entity_->type_.get()); + object("type", "text_mention"); + object("user", JsonUser(entity->user_id_, client_)); + break; + } + default: + UNREACHABLE(); + } + } + + private: + const td_api::textEntity *entity_; + const Client *client_; +}; + +class Client::JsonVectorEntities : public Jsonable { + public: + JsonVectorEntities(const td::vector> &entities, const Client *client) + : entities_(entities), client_(client) { + } + void store(JsonValueScope *scope) const { + auto array = scope->enter_array(); + for (auto &entity : entities_) { + if (entity->type_->get_id() != td_api::textEntityTypeBankCardNumber::ID) { + array << JsonEntity(entity.get(), client_); + } + } + } + + private: + const td::vector> &entities_; + const Client *client_; +}; + +class Client::JsonLocation : public Jsonable { + public: + explicit JsonLocation(const td_api::location *location, double expires_in = 0.0, int32 live_period = 0, + int32 heading = 0, int32 proximity_alert_radius = 0) + : location_(location) + , expires_in_(expires_in) + , live_period_(live_period) + , heading_(heading) + , proximity_alert_radius_(proximity_alert_radius) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("latitude", location_->latitude_); + object("longitude", location_->longitude_); + if (expires_in_ > 0.0) { + object("live_period", live_period_); + if (heading_ > 0) { + object("heading", heading_); + } + if (proximity_alert_radius_ > 0) { + object("proximity_alert_radius", proximity_alert_radius_); + } + } + if (location_->horizontal_accuracy_ > 0) { + object("horizontal_accuracy", location_->horizontal_accuracy_); + } + } + + private: + const td_api::location *location_; + double expires_in_; + int32 live_period_; + int32 heading_; + int32 proximity_alert_radius_; +}; + +class Client::JsonChatPermissions : public Jsonable { + public: + explicit JsonChatPermissions(const td_api::chatPermissions *chat_permissions) : chat_permissions_(chat_permissions) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + Client::json_store_permissions(object, chat_permissions_); + } + + private: + const td_api::chatPermissions *chat_permissions_; +}; + +class Client::JsonChatPhotoInfo : public Jsonable { + public: + explicit JsonChatPhotoInfo(const td_api::chatPhotoInfo *chat_photo) : chat_photo_(chat_photo) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("small_file_id", chat_photo_->small_->remote_->id_); + object("small_file_unique_id", chat_photo_->small_->remote_->unique_id_); + object("big_file_id", chat_photo_->big_->remote_->id_); + object("big_file_unique_id", chat_photo_->big_->remote_->unique_id_); + } + + private: + const td_api::chatPhotoInfo *chat_photo_; +}; + +class Client::JsonChatLocation : public Jsonable { + public: + explicit JsonChatLocation(const td_api::chatLocation *chat_location) : chat_location_(chat_location) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("location", JsonLocation(chat_location_->location_.get())); + object("address", chat_location_->address_); + } + + private: + const td_api::chatLocation *chat_location_; +}; + +class Client::JsonMessage : public Jsonable { + public: + JsonMessage(const MessageInfo *message, bool need_reply, const td::string &source, const Client *client) + : message_(message), need_reply_(need_reply), source_(source), client_(client) { + } + void store(JsonValueScope *scope) const; + + private: + const MessageInfo *message_; + bool need_reply_; + const td::string &source_; + const Client *client_; + + void add_caption(td::JsonObjectScope &object, const object_ptr &caption) const { + CHECK(caption != nullptr); + if (!caption->text_.empty()) { + object("caption", caption->text_); + + if (!caption->entities_.empty()) { + object("caption_entities", JsonVectorEntities(caption->entities_, client_)); + } + } + } +}; + +class Client::JsonChat : public Jsonable { + public: + JsonChat(int64 chat_id, bool is_full, const Client *client, int64 pinned_message_id = -1) + : chat_id_(chat_id), is_full_(is_full), client_(client), pinned_message_id_(pinned_message_id) { + } + void store(JsonValueScope *scope) const { + auto chat_info = client_->get_chat(chat_id_); + CHECK(chat_info != nullptr); + auto object = scope->enter_object(); + object("id", chat_id_); + switch (chat_info->type) { + case ChatInfo::Type::Private: { + auto user_info = client_->get_user_info(chat_info->user_id); + CHECK(user_info != nullptr); + object("first_name", user_info->first_name); + if (!user_info->last_name.empty()) { + object("last_name", user_info->last_name); + } + if (!user_info->username.empty()) { + object("username", user_info->username); + } + object("type", "private"); + if (is_full_) { + if (!user_info->bio.empty()) { + object("bio", user_info->bio); + } + } + break; + } + case ChatInfo::Type::Group: { + object("title", chat_info->title); + object("type", "group"); + + const auto *permissions = chat_info->permissions.get(); + + auto group_info = client_->get_group_info(chat_info->group_id); + CHECK(group_info != nullptr); + if (is_full_) { + if (!group_info->description.empty()) { + object("description", group_info->description); + } + if (!group_info->invite_link.empty()) { + object("invite_link", group_info->invite_link); + } + object("permissions", JsonChatPermissions(permissions)); + } + auto everyone_is_administrator = permissions->can_send_messages_ && permissions->can_send_media_messages_ && + permissions->can_send_polls_ && permissions->can_send_other_messages_ && + permissions->can_add_web_page_previews_ && permissions->can_change_info_ && + permissions->can_invite_users_ && permissions->can_pin_messages_; + object("all_members_are_administrators", td::JsonBool(everyone_is_administrator)); + break; + } + case ChatInfo::Type::Supergroup: { + object("title", chat_info->title); + + auto supergroup_info = client_->get_supergroup_info(chat_info->supergroup_id); + CHECK(supergroup_info != nullptr); + if (!supergroup_info->username.empty()) { + object("username", supergroup_info->username); + } + + if (supergroup_info->is_supergroup) { + object("type", "supergroup"); + } else { + object("type", "channel"); + } + if (is_full_) { + if (!supergroup_info->description.empty()) { + object("description", supergroup_info->description); + } + if (!supergroup_info->invite_link.empty()) { + object("invite_link", supergroup_info->invite_link); + } + if (supergroup_info->sticker_set_id != 0) { + auto sticker_set_name = client_->get_sticker_set_name(supergroup_info->sticker_set_id); + if (!sticker_set_name.empty()) { + object("sticker_set_name", sticker_set_name); + } else { + LOG(ERROR) << "Not found chat sticker set " << supergroup_info->sticker_set_id; + } + } + if (supergroup_info->can_set_sticker_set) { + object("can_set_sticker_set", td::JsonTrue()); + } + if (supergroup_info->is_supergroup) { + object("permissions", JsonChatPermissions(chat_info->permissions.get())); + } + if (supergroup_info->slow_mode_delay != 0) { + object("slow_mode_delay", supergroup_info->slow_mode_delay); + } + if (supergroup_info->linked_chat_id != 0) { + object("linked_chat_id", supergroup_info->linked_chat_id); + } + if (supergroup_info->location != nullptr) { + object("location", JsonChatLocation(supergroup_info->location.get())); + } + } + break; + } + case ChatInfo::Type::Unknown: + default: + UNREACHABLE(); + } + if (is_full_) { + if (chat_info->photo != nullptr) { + object("photo", JsonChatPhotoInfo(chat_info->photo.get())); + } + if (pinned_message_id_ != 0) { + CHECK(pinned_message_id_ != -1); + const MessageInfo *pinned_message = client_->get_message(chat_id_, pinned_message_id_); + if (pinned_message != nullptr) { + object("pinned_message", JsonMessage(pinned_message, false, "pin in JsonChat", client_)); + } else { + LOG(ERROR) << "Pinned unknown, inaccessible or deleted message " << pinned_message_id_; + } + } + } + } + + private: + int64 chat_id_; + bool is_full_; + const Client *client_; + int64 pinned_message_id_; +}; + +class Client::JsonMessageSender : public Jsonable { + public: + JsonMessageSender(const td_api::MessageSender *sender, const Client *client) : sender_(sender), client_(client) { + } + void store(JsonValueScope *scope) const { + CHECK(sender_ != nullptr); + switch (sender_->get_id()) { + case td_api::messageSenderUser::ID: { + auto sender_user_id = static_cast(sender_)->user_id_; + JsonUser(sender_user_id, client_).store(scope); + break; + } + case td_api::messageSenderChat::ID: { + auto sender_chat_id = static_cast(sender_)->chat_id_; + JsonChat(sender_chat_id, false, client_).store(scope); + break; + } + default: + UNREACHABLE(); + } + } + + private: + const td_api::MessageSender *sender_; + const Client *client_; +}; + +class Client::JsonMessages : public Jsonable { + public: + explicit JsonMessages(const td::vector &messages) : messages_(messages) { + } + void store(JsonValueScope *scope) const { + auto array = scope->enter_array(); + for (auto &message : messages_) { + array << td::JsonRaw(message); + } + } + + private: + const td::vector &messages_; +}; + +class Client::JsonAnimation : public Jsonable { + public: + JsonAnimation(const td_api::animation *animation, bool as_document, const Client *client) + : animation_(animation), as_document_(as_document), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + if (!animation_->file_name_.empty()) { + object("file_name", animation_->file_name_); + } + if (!animation_->mime_type_.empty()) { + object("mime_type", animation_->mime_type_); + } + if (!as_document_) { + object("duration", animation_->duration_); + object("width", animation_->width_); + object("height", animation_->height_); + } + client_->json_store_thumbnail(object, animation_->thumbnail_.get()); + client_->json_store_file(object, animation_->animation_.get()); + } + + private: + const td_api::animation *animation_; + bool as_document_; + const Client *client_; +}; + +class Client::JsonAudio : public Jsonable { + public: + JsonAudio(const td_api::audio *audio, const Client *client) : audio_(audio), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("duration", audio_->duration_); + if (!audio_->file_name_.empty()) { + object("file_name", audio_->file_name_); + } + if (!audio_->mime_type_.empty()) { + object("mime_type", audio_->mime_type_); + } + if (!audio_->title_.empty()) { + object("title", audio_->title_); + } + if (!audio_->performer_.empty()) { + object("performer", audio_->performer_); + } + client_->json_store_thumbnail(object, audio_->album_cover_thumbnail_.get()); + client_->json_store_file(object, audio_->audio_.get()); + } + + private: + const td_api::audio *audio_; + const Client *client_; +}; + +class Client::JsonDocument : public Jsonable { + public: + JsonDocument(const td_api::document *document, const Client *client) : document_(document), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + if (!document_->file_name_.empty()) { + object("file_name", document_->file_name_); + } + if (!document_->mime_type_.empty()) { + object("mime_type", document_->mime_type_); + } + client_->json_store_thumbnail(object, document_->thumbnail_.get()); + client_->json_store_file(object, document_->document_.get()); + } + + private: + const td_api::document *document_; + const Client *client_; +}; + +class Client::JsonPhotoSize : public Jsonable { + public: + JsonPhotoSize(const td_api::photoSize *photo_size, const Client *client) : photo_size_(photo_size), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + client_->json_store_file(object, photo_size_->photo_.get()); + object("width", photo_size_->width_); + object("height", photo_size_->height_); + } + + private: + const td_api::photoSize *photo_size_; + const Client *client_; +}; + +class Client::JsonThumbnail : public Jsonable { + public: + JsonThumbnail(const td_api::thumbnail *thumbnail, const Client *client) : thumbnail_(thumbnail), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + client_->json_store_file(object, thumbnail_->file_.get()); + object("width", thumbnail_->width_); + object("height", thumbnail_->height_); + } + + private: + const td_api::thumbnail *thumbnail_; + const Client *client_; +}; + +class Client::JsonPhoto : public Jsonable { + public: + JsonPhoto(const td_api::photo *photo, const Client *client) : photo_(photo), client_(client) { + } + void store(JsonValueScope *scope) const { + auto array = scope->enter_array(); + for (auto &photo_size : photo_->sizes_) { + if (photo_size->type_ != "i" && photo_size->type_ != "t" && !photo_size->photo_->remote_->id_.empty()) { + array << JsonPhotoSize(photo_size.get(), client_); + } + } + } + + private: + const td_api::photo *photo_; + const Client *client_; +}; + +class Client::JsonChatPhoto : public Jsonable { + public: + JsonChatPhoto(const td_api::chatPhoto *photo, const Client *client) : photo_(photo), client_(client) { + } + void store(JsonValueScope *scope) const { + auto array = scope->enter_array(); + for (auto &photo_size : photo_->sizes_) { + if (photo_size->type_ != "i" && photo_size->type_ != "t" && !photo_size->photo_->remote_->id_.empty()) { + array << JsonPhotoSize(photo_size.get(), client_); + } + } + } + + private: + const td_api::chatPhoto *photo_; + const Client *client_; +}; + +class Client::JsonMaskPosition : public Jsonable { + public: + explicit JsonMaskPosition(const td_api::maskPosition *mask_position) : mask_position_(mask_position) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("point", Client::MASK_POINTS[Client::mask_point_to_index(mask_position_->point_)]); + object("x_shift", mask_position_->x_shift_); + object("y_shift", mask_position_->y_shift_); + object("scale", mask_position_->scale_); + } + + private: + const td_api::maskPosition *mask_position_; +}; + +class Client::JsonSticker : public Jsonable { + public: + JsonSticker(const td_api::sticker *sticker, const Client *client) : sticker_(sticker), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("width", sticker_->width_); + object("height", sticker_->height_); + if (!sticker_->emoji_.empty()) { + object("emoji", sticker_->emoji_); + } + auto set_name = client_->get_sticker_set_name(sticker_->set_id_); + if (!set_name.empty()) { + object("set_name", set_name); + } + object("is_animated", td::JsonBool(sticker_->is_animated_)); + if (sticker_->mask_position_ != nullptr) { + object("mask_position", JsonMaskPosition(sticker_->mask_position_.get())); + } + client_->json_store_thumbnail(object, sticker_->thumbnail_.get()); + client_->json_store_file(object, sticker_->sticker_.get()); + } + + private: + const td_api::sticker *sticker_; + const Client *client_; +}; + +class Client::JsonStickers : public Jsonable { + public: + JsonStickers(const td::vector> &stickers, const Client *client) + : stickers_(stickers), client_(client) { + } + void store(JsonValueScope *scope) const { + auto array = scope->enter_array(); + for (auto &sticker : stickers_) { + array << JsonSticker(sticker.get(), client_); + } + } + + private: + const td::vector> &stickers_; + const Client *client_; +}; + +class Client::JsonVideo : public Jsonable { + public: + JsonVideo(const td_api::video *video, const Client *client) : video_(video), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("duration", video_->duration_); + object("width", video_->width_); + object("height", video_->height_); + if (!video_->file_name_.empty()) { + object("file_name", video_->file_name_); + } + if (!video_->mime_type_.empty()) { + object("mime_type", video_->mime_type_); + } + client_->json_store_thumbnail(object, video_->thumbnail_.get()); + client_->json_store_file(object, video_->video_.get()); + } + + private: + const td_api::video *video_; + const Client *client_; +}; + +class Client::JsonVideoNote : public Jsonable { + public: + JsonVideoNote(const td_api::videoNote *video_note, const Client *client) : video_note_(video_note), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("duration", video_note_->duration_); + object("length", video_note_->length_); + client_->json_store_thumbnail(object, video_note_->thumbnail_.get()); + client_->json_store_file(object, video_note_->video_.get()); + } + + private: + const td_api::videoNote *video_note_; + const Client *client_; +}; + +class Client::JsonVoiceNote : public Jsonable { + public: + JsonVoiceNote(const td_api::voiceNote *voice_note, const Client *client) : voice_note_(voice_note), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("duration", voice_note_->duration_); + if (!voice_note_->mime_type_.empty()) { + object("mime_type", voice_note_->mime_type_); + } + client_->json_store_file(object, voice_note_->voice_.get()); + } + + private: + const td_api::voiceNote *voice_note_; + const Client *client_; +}; + +class Client::JsonVenue : public Jsonable { + public: + explicit JsonVenue(const td_api::venue *venue) : venue_(venue) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("location", JsonLocation(venue_->location_.get())); + object("title", venue_->title_); + object("address", venue_->address_); + if (venue_->provider_ == "foursquare") { + if (!venue_->id_.empty()) { + object("foursquare_id", venue_->id_); + } + if (!venue_->type_.empty()) { + object("foursquare_type", venue_->type_); + } + } + if (venue_->provider_ == "gplaces") { + if (!venue_->id_.empty()) { + object("google_place_id", venue_->id_); + } + if (!venue_->type_.empty()) { + object("google_place_type", venue_->type_); + } + } + } + + private: + const td_api::venue *venue_; +}; + +class Client::JsonContact : public Jsonable { + public: + explicit JsonContact(const td_api::contact *contact) : contact_(contact) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("phone_number", contact_->phone_number_); + object("first_name", contact_->first_name_); + if (!contact_->last_name_.empty()) { + object("last_name", contact_->last_name_); + } + if (!contact_->vcard_.empty()) { + object("vcard", contact_->vcard_); + } + if (contact_->user_id_) { + object("user_id", contact_->user_id_); + } + } + + private: + const td_api::contact *contact_; +}; + +class Client::JsonDice : public Jsonable { + public: + JsonDice(const td::string &emoji, int32 value) : emoji_(emoji), value_(value) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("emoji", emoji_); + object("value", value_); + } + + private: + const td::string &emoji_; + int32 value_; +}; + +class Client::JsonGame : public Jsonable { + public: + JsonGame(const td_api::game *game, const Client *client) : game_(game), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("title", game_->title_); + if (!game_->text_->text_.empty()) { + object("text", game_->text_->text_); + } + if (!game_->text_->entities_.empty()) { + object("text_entities", JsonVectorEntities(game_->text_->entities_, client_)); + } + object("description", game_->description_); + CHECK(game_->photo_ != nullptr); + object("photo", JsonPhoto(game_->photo_.get(), client_)); + if (game_->animation_ != nullptr) { + object("animation", JsonAnimation(game_->animation_.get(), false, client_)); + } + } + + private: + const td_api::game *game_; + const Client *client_; +}; + +class Client::JsonInvoice : public Jsonable { + public: + explicit JsonInvoice(const td_api::messageInvoice *invoice) : invoice_(invoice) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("title", invoice_->title_); + object("description", invoice_->description_); + object("start_parameter", invoice_->start_parameter_); + object("currency", invoice_->currency_); + object("total_amount", invoice_->total_amount_); + // skip photo + // skip is_test + // skip need_shipping_address + // skip receipt_message_id + } + + private: + const td_api::messageInvoice *invoice_; +}; + +class Client::JsonPollOption : public Jsonable { + public: + explicit JsonPollOption(const td_api::pollOption *option) : option_(option) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("text", option_->text_); + object("voter_count", option_->voter_count_); + // ignore is_chosen + } + + private: + const td_api::pollOption *option_; +}; + +class Client::JsonPoll : public Jsonable { + public: + JsonPoll(const td_api::poll *poll, const Client *client) : poll_(poll), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("id", td::to_string(poll_->id_)); + object("question", poll_->question_); + object("options", td::json_array(poll_->options_, [](auto &option) { return JsonPollOption(option.get()); })); + object("total_voter_count", poll_->total_voter_count_); + if (poll_->open_period_ != 0 && poll_->close_date_ != 0) { + object("open_period", poll_->open_period_); + object("close_date", poll_->close_date_); + } + object("is_closed", td::JsonBool(poll_->is_closed_)); + object("is_anonymous", td::JsonBool(poll_->is_anonymous_)); + switch (poll_->type_->get_id()) { + case td_api::pollTypeQuiz::ID: { + object("type", "quiz"); + object("allows_multiple_answers", td::JsonFalse()); + auto quiz = static_cast(poll_->type_.get()); + int32 correct_option_id = quiz->correct_option_id_; + if (correct_option_id != -1) { + object("correct_option_id", correct_option_id); + } + auto *explanation = quiz->explanation_.get(); + if (!explanation->text_.empty()) { + object("explanation", explanation->text_); + object("explanation_entities", JsonVectorEntities(explanation->entities_, client_)); + } + break; + } + case td_api::pollTypeRegular::ID: + object("type", "regular"); + object("allows_multiple_answers", + td::JsonBool(static_cast(poll_->type_.get())->allow_multiple_answers_)); + break; + default: + UNREACHABLE(); + } + } + + private: + const td_api::poll *poll_; + const Client *client_; +}; + +class Client::JsonPollAnswer : public Jsonable { + public: + JsonPollAnswer(const td_api::updatePollAnswer *poll_answer, const Client *client) + : poll_answer_(poll_answer), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("poll_id", td::to_string(poll_answer_->poll_id_)); + object("user", JsonUser(poll_answer_->user_id_, client_)); + object("option_ids", td::json_array(poll_answer_->option_ids_, [](int32 option_id) { return option_id; })); + } + + private: + const td_api::updatePollAnswer *poll_answer_; + const Client *client_; +}; + +class Client::JsonAddress : public Jsonable { + public: + explicit JsonAddress(const td_api::address *address) : address_(address) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("country_code", address_->country_code_); + object("state", address_->state_); + object("city", address_->city_); + object("street_line1", address_->street_line1_); + object("street_line2", address_->street_line2_); + object("post_code", address_->postal_code_); + } + + private: + const td_api::address *address_; +}; + +class Client::JsonOrderInfo : public Jsonable { + public: + explicit JsonOrderInfo(const td_api::orderInfo *order_info) : order_info_(order_info) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + if (!order_info_->name_.empty()) { + object("name", order_info_->name_); + } + if (!order_info_->phone_number_.empty()) { + object("phone_number", order_info_->phone_number_); + } + if (!order_info_->email_address_.empty()) { + object("email", order_info_->email_address_); + } + if (order_info_->shipping_address_ != nullptr) { + object("shipping_address", JsonAddress(order_info_->shipping_address_.get())); + } + } + + private: + const td_api::orderInfo *order_info_; +}; + +class Client::JsonSuccessfulPaymentBot : public Jsonable { + public: + explicit JsonSuccessfulPaymentBot(const td_api::messagePaymentSuccessfulBot *successful_payment) + : successful_payment_(successful_payment) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("currency", successful_payment_->currency_); + object("total_amount", successful_payment_->total_amount_); + if (!td::check_utf8(successful_payment_->invoice_payload_)) { + LOG(WARNING) << "Receive non-UTF-8 invoice payload"; + object("invoice_payload", td::JsonRawString(successful_payment_->invoice_payload_)); + } else { + object("invoice_payload", successful_payment_->invoice_payload_); + } + if (!successful_payment_->shipping_option_id_.empty()) { + object("shipping_option_id", successful_payment_->shipping_option_id_); + } + if (successful_payment_->order_info_ != nullptr) { + object("order_info", JsonOrderInfo(successful_payment_->order_info_.get())); + } + + object("telegram_payment_charge_id", successful_payment_->telegram_payment_charge_id_); + object("provider_payment_charge_id", successful_payment_->provider_payment_charge_id_); + } + + private: + const td_api::messagePaymentSuccessfulBot *successful_payment_; +}; + +class Client::JsonEncryptedPassportElement : public Jsonable { + public: + JsonEncryptedPassportElement(const td_api::encryptedPassportElement *element, const Client *client) + : element_(element), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + auto id = element_->type_->get_id(); + object("type", Client::get_passport_element_type(id)); + switch (id) { + case td_api::passportElementTypePhoneNumber::ID: + object("phone_number", element_->value_); + break; + case td_api::passportElementTypeEmailAddress::ID: + object("email", element_->value_); + break; + case td_api::passportElementTypePersonalDetails::ID: + case td_api::passportElementTypePassport::ID: + case td_api::passportElementTypeDriverLicense::ID: + case td_api::passportElementTypeIdentityCard::ID: + case td_api::passportElementTypeInternalPassport::ID: + case td_api::passportElementTypeAddress::ID: + object("data", td::base64_encode(element_->data_)); + break; + } + switch (id) { + case td_api::passportElementTypeUtilityBill::ID: + case td_api::passportElementTypeBankStatement::ID: + case td_api::passportElementTypeRentalAgreement::ID: + case td_api::passportElementTypePassportRegistration::ID: + case td_api::passportElementTypeTemporaryRegistration::ID: + object("files", JsonDatedFiles(element_->files_, client_)); + if (!element_->translation_.empty()) { + object("translation", JsonDatedFiles(element_->translation_, client_)); + } + break; + } + switch (id) { + case td_api::passportElementTypePassport::ID: + case td_api::passportElementTypeDriverLicense::ID: + case td_api::passportElementTypeIdentityCard::ID: + case td_api::passportElementTypeInternalPassport::ID: + CHECK(element_->front_side_ != nullptr); + object("front_side", JsonDatedFile(element_->front_side_.get(), client_)); + if (element_->reverse_side_ != nullptr) { + CHECK(id == td_api::passportElementTypeIdentityCard::ID || + id == td_api::passportElementTypeDriverLicense::ID); + object("reverse_side", JsonDatedFile(element_->reverse_side_.get(), client_)); + } else { + CHECK(id == td_api::passportElementTypePassport::ID || id == td_api::passportElementTypeInternalPassport::ID); + } + if (element_->selfie_ != nullptr) { + object("selfie", JsonDatedFile(element_->selfie_.get(), client_)); + } + if (!element_->translation_.empty()) { + object("translation", JsonDatedFiles(element_->translation_, client_)); + } + break; + } + object("hash", td::base64_encode(element_->hash_)); + } + + private: + const td_api::encryptedPassportElement *element_; + const Client *client_; +}; + +class Client::JsonEncryptedCredentials : public Jsonable { + public: + explicit JsonEncryptedCredentials(const td_api::encryptedCredentials *credentials) : credentials_(credentials) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("data", td::base64_encode(credentials_->data_)); + object("hash", td::base64_encode(credentials_->hash_)); + object("secret", td::base64_encode(credentials_->secret_)); + } + + private: + const td_api::encryptedCredentials *credentials_; +}; + +class Client::JsonPassportData : public Jsonable { + public: + JsonPassportData(const td_api::messagePassportDataReceived *passport_data, const Client *client) + : passport_data_(passport_data), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("data", td::json_array(passport_data_->elements_, [client = client_](auto &element) { + return JsonEncryptedPassportElement(element.get(), client); + })); + object("credentials", JsonEncryptedCredentials(passport_data_->credentials_.get())); + } + + private: + const td_api::messagePassportDataReceived *passport_data_; + const Client *client_; +}; + +class Client::JsonProximityAlertTriggered : public Jsonable { + public: + JsonProximityAlertTriggered(const td_api::messageProximityAlertTriggered *proximity_alert_triggered, + const Client *client) + : proximity_alert_triggered_(proximity_alert_triggered), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("traveler", JsonMessageSender(proximity_alert_triggered_->traveler_.get(), client_)); + object("watcher", JsonMessageSender(proximity_alert_triggered_->watcher_.get(), client_)); + object("distance", proximity_alert_triggered_->distance_); + } + + private: + const td_api::messageProximityAlertTriggered *proximity_alert_triggered_; + const Client *client_; +}; + +class Client::JsonCallbackGame : public Jsonable { + public: + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + } +}; + +class Client::JsonInlineKeyboardButton : public Jsonable { + public: + explicit JsonInlineKeyboardButton(const td_api::inlineKeyboardButton *button) : button_(button) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("text", button_->text_); + switch (button_->type_->get_id()) { + case td_api::inlineKeyboardButtonTypeUrl::ID: { + auto type = static_cast(button_->type_.get()); + object("url", type->url_); + break; + } + case td_api::inlineKeyboardButtonTypeLoginUrl::ID: { + auto type = static_cast(button_->type_.get()); + object("url", type->url_); + break; + } + case td_api::inlineKeyboardButtonTypeCallback::ID: + case td_api::inlineKeyboardButtonTypeCallbackWithPassword::ID: { + auto data = get_callback_data(button_->type_); + if (!td::check_utf8(data)) { + object("callback_data", td::JsonRawString(data)); + } else { + object("callback_data", data); + } + break; + } + case td_api::inlineKeyboardButtonTypeCallbackGame::ID: + object("callback_game", JsonCallbackGame()); + break; + case td_api::inlineKeyboardButtonTypeSwitchInline::ID: { + auto type = static_cast(button_->type_.get()); + if (type->in_current_chat_) { + object("switch_inline_query_current_chat", type->query_); + } else { + object("switch_inline_query", type->query_); + } + break; + } + case td_api::inlineKeyboardButtonTypeBuy::ID: + object("pay", td::JsonTrue()); + break; + default: + UNREACHABLE(); + break; + } + } + + private: + const td_api::inlineKeyboardButton *button_; +}; + +class Client::JsonInlineKeyboard : public Jsonable { + public: + explicit JsonInlineKeyboard(const td_api::replyMarkupInlineKeyboard *inline_keyboard) + : inline_keyboard_(inline_keyboard) { + } + void store(JsonValueScope *scope) const { + auto array = scope->enter_array(); + for (auto &row : inline_keyboard_->rows_) { + array << td::json_array(row, [](auto &button) { return JsonInlineKeyboardButton(button.get()); }); + } + } + + private: + const td_api::replyMarkupInlineKeyboard *inline_keyboard_; +}; + +class Client::JsonReplyMarkup : public Jsonable { + public: + explicit JsonReplyMarkup(const td_api::ReplyMarkup *reply_markup) : reply_markup_(reply_markup) { + } + void store(JsonValueScope *scope) const { + CHECK(reply_markup_->get_id() == td_api::replyMarkupInlineKeyboard::ID); + auto object = scope->enter_object(); + object("inline_keyboard", + JsonInlineKeyboard(static_cast(reply_markup_))); + } + + private: + const td_api::ReplyMarkup *reply_markup_; +}; + +void Client::JsonMessage::store(JsonValueScope *scope) const { + CHECK(message_ != nullptr); + auto object = scope->enter_object(); + object("message_id", as_client_message_id(message_->id)); + if (message_->sender_user_id != 0) { + object("from", JsonUser(message_->sender_user_id, client_)); + } + if (!message_->author_signature.empty()) { + object("author_signature", message_->author_signature); + } + if (message_->sender_chat_id != 0) { + object("sender_chat", JsonChat(message_->sender_chat_id, false, client_)); + } + object("chat", JsonChat(message_->chat_id, false, client_)); + object("date", message_->date); + if (message_->edit_date > 0) { + object("edit_date", message_->edit_date); + } + if (message_->initial_send_date > 0) { + if (message_->initial_sender_user_id != 0) { + object("forward_from", JsonUser(message_->initial_sender_user_id, client_)); + } + if (message_->initial_sender_chat_id != 0) { + object("forward_from_chat", JsonChat(message_->initial_sender_chat_id, false, client_)); + } + if (message_->initial_chat_id != 0) { + object("forward_from_chat", JsonChat(message_->initial_chat_id, false, client_)); + if (message_->initial_message_id != 0) { + object("forward_from_message_id", as_client_message_id(message_->initial_message_id)); + } + } + if (!message_->initial_author_signature.empty()) { + object("forward_signature", message_->initial_author_signature); + } + if (!message_->initial_sender_name.empty()) { + object("forward_sender_name", message_->initial_sender_name); + } + object("forward_date", message_->initial_send_date); + } + if (message_->reply_to_message_id > 0 && need_reply_ && !message_->is_reply_to_message_deleted) { + const MessageInfo *reply_to_message = client_->get_message(message_->chat_id, message_->reply_to_message_id); + if (reply_to_message != nullptr) { + object("reply_to_message", JsonMessage(reply_to_message, false, "reply in " + source_, client_)); + } else { + LOG(WARNING) << "Replied to unknown or deleted message " << message_->reply_to_message_id << " in chat " + << message_->chat_id << " while storing " << source_ << " " << message_->id; + } + } + if (message_->media_album_id > 0) { + object("media_group_id", td::to_string(message_->media_album_id)); + } + switch (message_->content->get_id()) { + case td_api::messageText::ID: { + auto message_text = static_cast(message_->content.get()); + object("text", message_text->text_->text_); + if (!message_text->text_->entities_.empty()) { + object("entities", JsonVectorEntities(message_text->text_->entities_, client_)); + } + break; + } + case td_api::messageAnimation::ID: { + auto message_animation = static_cast(message_->content.get()); + object("animation", JsonAnimation(message_animation->animation_.get(), false, client_)); + object("document", JsonAnimation(message_animation->animation_.get(), true, client_)); + add_caption(object, message_animation->caption_); + break; + } + case td_api::messageAudio::ID: { + auto message_audio = static_cast(message_->content.get()); + object("audio", JsonAudio(message_audio->audio_.get(), client_)); + add_caption(object, message_audio->caption_); + break; + } + case td_api::messageDocument::ID: { + auto message_document = static_cast(message_->content.get()); + object("document", JsonDocument(message_document->document_.get(), client_)); + add_caption(object, message_document->caption_); + break; + } + case td_api::messagePhoto::ID: { + auto message_photo = static_cast(message_->content.get()); + if (message_photo->photo_ == nullptr) { + LOG(ERROR) << "Got empty messagePhoto"; + break; + } + object("photo", JsonPhoto(message_photo->photo_.get(), client_)); + add_caption(object, message_photo->caption_); + break; + } + case td_api::messageSticker::ID: { + auto message_sticker = static_cast(message_->content.get()); + object("sticker", JsonSticker(message_sticker->sticker_.get(), client_)); + break; + } + case td_api::messageVideo::ID: { + auto message_video = static_cast(message_->content.get()); + object("video", JsonVideo(message_video->video_.get(), client_)); + add_caption(object, message_video->caption_); + break; + } + case td_api::messageVideoNote::ID: { + auto message_video_note = static_cast(message_->content.get()); + object("video_note", JsonVideoNote(message_video_note->video_note_.get(), client_)); + break; + } + case td_api::messageVoiceNote::ID: { + auto message_voice_note = static_cast(message_->content.get()); + object("voice", JsonVoiceNote(message_voice_note->voice_note_.get(), client_)); + add_caption(object, message_voice_note->caption_); + break; + } + case td_api::messageContact::ID: { + auto message_contact = static_cast(message_->content.get()); + object("contact", JsonContact(message_contact->contact_.get())); + break; + } + case td_api::messageDice::ID: { + auto message_dice = static_cast(message_->content.get()); + object("dice", JsonDice(message_dice->emoji_, message_dice->value_)); + break; + } + case td_api::messageGame::ID: { + auto message_game = static_cast(message_->content.get()); + object("game", JsonGame(message_game->game_.get(), client_)); + break; + } + case td_api::messageInvoice::ID: { + auto message_invoice = static_cast(message_->content.get()); + object("invoice", JsonInvoice(message_invoice)); + break; + } + case td_api::messageLocation::ID: { + auto message_location = static_cast(message_->content.get()); + object("location", JsonLocation(message_location->location_.get(), message_location->expires_in_, + message_location->live_period_, message_location->heading_, + message_location->proximity_alert_radius_)); + break; + } + case td_api::messageVenue::ID: { + auto message_venue = static_cast(message_->content.get()); + object("location", JsonLocation(message_venue->venue_->location_.get())); + object("venue", JsonVenue(message_venue->venue_.get())); + break; + } + case td_api::messagePoll::ID: { + auto message_poll = static_cast(message_->content.get()); + object("poll", JsonPoll(message_poll->poll_.get(), client_)); + break; + } + case td_api::messageChatAddMembers::ID: { + auto message_add_members = static_cast(message_->content.get()); + int32 user_id = client_->choose_added_member_id(message_add_members); + if (user_id > 0) { + object("new_chat_participant", JsonUser(user_id, client_)); + object("new_chat_member", JsonUser(user_id, client_)); + object("new_chat_members", JsonUsers(message_add_members->member_user_ids_, client_)); + } else { + LOG(ERROR) << "Can't choose added member for new_chat_member field"; + } + break; + } + case td_api::messageChatJoinByLink::ID: { + if (message_->sender_user_id > 0) { + object("new_chat_participant", JsonUser(message_->sender_user_id, client_)); + object("new_chat_member", JsonUser(message_->sender_user_id, client_)); + object("new_chat_members", JsonUsers({message_->sender_user_id}, client_)); + } + break; + } + case td_api::messageChatDeleteMember::ID: { + auto message_delete_member = static_cast(message_->content.get()); + int32 user_id = message_delete_member->user_id_; + object("left_chat_participant", JsonUser(user_id, client_)); + object("left_chat_member", JsonUser(user_id, client_)); + break; + } + case td_api::messageChatChangeTitle::ID: { + auto message_change_title = static_cast(message_->content.get()); + object("new_chat_title", message_change_title->title_); + break; + } + case td_api::messageChatChangePhoto::ID: { + auto message_change_photo = static_cast(message_->content.get()); + if (message_change_photo->photo_ == nullptr) { + LOG(ERROR) << "Got empty messageChatChangePhoto"; + break; + } + object("new_chat_photo", JsonChatPhoto(message_change_photo->photo_.get(), client_)); + break; + } + case td_api::messageChatDeletePhoto::ID: + object("delete_chat_photo", td::JsonTrue()); + break; + case td_api::messageBasicGroupChatCreate::ID: + object("group_chat_created", td::JsonTrue()); + break; + case td_api::messageSupergroupChatCreate::ID: { + auto chat = client_->get_chat(message_->chat_id); + if (chat->type != ChatInfo::Type::Supergroup) { + LOG(ERROR) << "Receive messageSupergroupChatCreate in the non-supergroup chat " << message_->chat_id; + break; + } + auto supergroup_info = client_->get_supergroup_info(chat->supergroup_id); + CHECK(supergroup_info != nullptr); + if (supergroup_info->is_supergroup) { + object("supergroup_chat_created", td::JsonTrue()); + } else { + object("channel_chat_created", td::JsonTrue()); + } + break; + } + case td_api::messageChatUpgradeTo::ID: { + auto message_chat_upgrade_to = static_cast(message_->content.get()); + auto chat_id = get_supergroup_chat_id(message_chat_upgrade_to->supergroup_id_); + object("migrate_to_chat_id", td::JsonLong(chat_id)); + break; + } + case td_api::messageChatUpgradeFrom::ID: { + auto message_chat_upgrade_from = static_cast(message_->content.get()); + auto chat_id = get_basic_group_chat_id(message_chat_upgrade_from->basic_group_id_); + object("migrate_from_chat_id", td::JsonLong(chat_id)); + break; + } + case td_api::messagePinMessage::ID: { + auto message_pin_message = static_cast(message_->content.get()); + auto message_id = message_pin_message->message_id_; + if (message_id > 0) { + const MessageInfo *pinned_message = client_->get_message(message_->chat_id, message_id); + if (pinned_message != nullptr) { + object("pinned_message", JsonMessage(pinned_message, false, "pin in " + source_, client_)); + } else { + LOG_IF(ERROR, need_reply_) << "Pinned unknown, inaccessible or deleted message " << message_id; + } + } + break; + } + case td_api::messageGameScore::ID: + break; + case td_api::messagePaymentSuccessful::ID: + break; + case td_api::messagePaymentSuccessfulBot::ID: { + auto message_payment_sent_bot = static_cast(message_->content.get()); + object("successful_payment", JsonSuccessfulPaymentBot(message_payment_sent_bot)); + break; + } + case td_api::messageCall::ID: + break; + case td_api::messageScreenshotTaken::ID: + break; + case td_api::messageChatSetTtl::ID: + break; + case td_api::messageUnsupported::ID: + break; + case td_api::messageContactRegistered::ID: + break; + case td_api::messageExpiredPhoto::ID: + break; + case td_api::messageExpiredVideo::ID: + break; + case td_api::messageCustomServiceAction::ID: + break; + case td_api::messageWebsiteConnected::ID: { + auto chat = client_->get_chat(message_->chat_id); + if (chat->type != ChatInfo::Type::Private) { + break; + } + + auto message_website_connected = static_cast(message_->content.get()); + if (!message_website_connected->domain_name_.empty()) { + object("connected_website", message_website_connected->domain_name_); + } + break; + } + case td_api::messagePassportDataSent::ID: + break; + case td_api::messagePassportDataReceived::ID: { + auto message_passport_data_received = + static_cast(message_->content.get()); + object("passport_data", JsonPassportData(message_passport_data_received, client_)); + break; + } + case td_api::messageProximityAlertTriggered::ID: { + auto content = static_cast(message_->content.get()); + object("proximity_alert_triggered", JsonProximityAlertTriggered(content, client_)); + break; + } + default: + UNREACHABLE(); + } + if (message_->reply_markup != nullptr) { + object("reply_markup", JsonReplyMarkup(message_->reply_markup.get())); + } + if (message_->via_bot_user_id > 0) { + object("via_bot", JsonUser(message_->via_bot_user_id, client_)); + } +} + +class Client::JsonDeletedMessage : public Jsonable { + public: + JsonDeletedMessage(int64 chat_id, int64 message_id, const Client *client) + : chat_id_(chat_id), message_id_(message_id), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("message_id", as_client_message_id(message_id_)); + object("chat", JsonChat(chat_id_, false, client_)); + object("date", 0); + } + + private: + int64 chat_id_; + int64 message_id_; + const Client *client_; +}; + +class Client::JsonMessageId : public Jsonable { + public: + explicit JsonMessageId(int64 message_id) : message_id_(message_id) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("message_id", as_client_message_id(message_id_)); + } + + private: + int64 message_id_; +}; + +class Client::JsonInlineQuery : public Jsonable { + public: + JsonInlineQuery(int64 inline_query_id, int32 sender_user_id, const td_api::location *user_location, + const td::string &query, const td::string &offset, const Client *client) + : inline_query_id_(inline_query_id) + , sender_user_id_(sender_user_id) + , user_location_(user_location) + , query_(query) + , offset_(offset) + , client_(client) { + } + + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("id", td::to_string(inline_query_id_)); + object("from", JsonUser(sender_user_id_, client_)); + if (user_location_ != nullptr) { + object("location", JsonLocation(user_location_)); + } + object("query", query_); + object("offset", offset_); + } + + private: + int64 inline_query_id_; + int32 sender_user_id_; + const td_api::location *user_location_; + const td::string &query_; + const td::string &offset_; + const Client *client_; +}; + +class Client::JsonChosenInlineResult : public Jsonable { + public: + JsonChosenInlineResult(int32 sender_user_id, const td_api::location *user_location, const td::string &query, + const td::string &result_id, const td::string &inline_message_id, const Client *client) + : sender_user_id_(sender_user_id) + , user_location_(user_location) + , query_(query) + , result_id_(result_id) + , inline_message_id_(inline_message_id) + , client_(client) { + } + + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("from", JsonUser(sender_user_id_, client_)); + if (user_location_ != nullptr) { + object("location", JsonLocation(user_location_)); + } + if (!inline_message_id_.empty()) { + object("inline_message_id", inline_message_id_); + } + object("query", query_); + object("result_id", result_id_); + } + + private: + int32 sender_user_id_; + const td_api::location *user_location_; + const td::string &query_; + const td::string &result_id_; + const td::string &inline_message_id_; + const Client *client_; +}; + +class Client::JsonCallbackQuery : public Jsonable { + public: + JsonCallbackQuery(int64 callback_query_id, int32 sender_user_id, int64 chat_id, int64 message_id, + const MessageInfo *message_info, int64 chat_instance, td_api::CallbackQueryPayload *payload, + const Client *client) + : callback_query_id_(callback_query_id) + , sender_user_id_(sender_user_id) + , chat_id_(chat_id) + , message_id_(message_id) + , message_info_(message_info) + , chat_instance_(chat_instance) + , payload_(payload) + , client_(client) { + } + + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("id", td::to_string(callback_query_id_)); + object("from", JsonUser(sender_user_id_, client_)); + if (message_info_ != nullptr) { + object("message", JsonMessage(message_info_, true, "callback query", client_)); + } else { + object("message", JsonDeletedMessage(chat_id_, message_id_, client_)); + } + object("chat_instance", td::to_string(chat_instance_)); + client_->json_store_callback_query_payload(object, payload_); + } + + private: + int64 callback_query_id_; + int32 sender_user_id_; + int64 chat_id_; + int64 message_id_; + const MessageInfo *message_info_; + int64 chat_instance_; + td_api::CallbackQueryPayload *payload_; + const Client *client_; +}; + +class Client::JsonInlineCallbackQuery : public Jsonable { + public: + JsonInlineCallbackQuery(int64 callback_query_id, int32 sender_user_id, const td::string &inline_message_id, + int64 chat_instance, td_api::CallbackQueryPayload *payload, const Client *client) + : callback_query_id_(callback_query_id) + , sender_user_id_(sender_user_id) + , inline_message_id_(inline_message_id) + , chat_instance_(chat_instance) + , payload_(payload) + , client_(client) { + } + + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("id", td::to_string(callback_query_id_)); + object("from", JsonUser(sender_user_id_, client_)); + object("inline_message_id", inline_message_id_); + object("chat_instance", td::to_string(chat_instance_)); + client_->json_store_callback_query_payload(object, payload_); + } + + private: + int64 callback_query_id_; + int32 sender_user_id_; + const td::string &inline_message_id_; + int64 chat_instance_; + td_api::CallbackQueryPayload *payload_; + const Client *client_; +}; + +class Client::JsonShippingQuery : public Jsonable { + public: + JsonShippingQuery(const td_api::updateNewShippingQuery *query, const Client *client) + : query_(query), client_(client) { + } + + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("id", td::to_string(query_->id_)); + object("from", JsonUser(query_->sender_user_id_, client_)); + if (!td::check_utf8(query_->invoice_payload_)) { + LOG(WARNING) << "Receive non-UTF-8 invoice payload"; + object("invoice_payload", td::JsonRawString(query_->invoice_payload_)); + } else { + object("invoice_payload", query_->invoice_payload_); + } + object("shipping_address", JsonAddress(query_->shipping_address_.get())); + } + + private: + const td_api::updateNewShippingQuery *query_; + const Client *client_; +}; + +class Client::JsonPreCheckoutQuery : public Jsonable { + public: + JsonPreCheckoutQuery(const td_api::updateNewPreCheckoutQuery *query, const Client *client) + : query_(query), client_(client) { + } + + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("id", td::to_string(query_->id_)); + object("from", JsonUser(query_->sender_user_id_, client_)); + object("currency", query_->currency_); + object("total_amount", query_->total_amount_); + if (!td::check_utf8(query_->invoice_payload_)) { + LOG(WARNING) << "Receive non-UTF-8 invoice payload"; + object("invoice_payload", td::JsonRawString(query_->invoice_payload_)); + } else { + object("invoice_payload", query_->invoice_payload_); + } + if (!query_->shipping_option_id_.empty()) { + object("shipping_option_id", query_->shipping_option_id_); + } + if (query_->order_info_ != nullptr) { + object("order_info", JsonOrderInfo(query_->order_info_.get())); + } + } + + private: + const td_api::updateNewPreCheckoutQuery *query_; + const Client *client_; +}; + +class Client::JsonCustomJson : public Jsonable { + public: + explicit JsonCustomJson(const td::string &json) : json_(json) { + } + + void store(JsonValueScope *scope) const { + *scope << td::JsonRaw(json_); + } + + private: + const td::string &json_; +}; + +class Client::JsonBotCommand : public Jsonable { + public: + explicit JsonBotCommand(const td_api::botCommand *command) : command_(command) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("command", command_->command_); + object("description", command_->description_); + } + + private: + const td_api::botCommand *command_; +}; + +class Client::JsonChatPhotos : public Jsonable { + public: + JsonChatPhotos(const td_api::chatPhotos *photos, const Client *client) : photos_(photos), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("total_count", photos_->total_count_); + object("photos", td::json_array(photos_->photos_, + [client = client_](auto &photo) { return JsonChatPhoto(photo.get(), client); })); + } + + private: + const td_api::chatPhotos *photos_; + const Client *client_; +}; + +class Client::JsonChatMember : public Jsonable { + public: + JsonChatMember(const td_api::chatMember *member, Client::ChatType chat_type, const Client *client) + : member_(member), chat_type_(chat_type), client_(client) { + } + + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("user", JsonUser(member_->user_id_, client_)); + object("status", Client::get_chat_member_status(member_->status_)); + switch (member_->status_->get_id()) { + case td_api::chatMemberStatusCreator::ID: { + auto creator = static_cast(member_->status_.get()); + if (!creator->custom_title_.empty()) { + object("custom_title", creator->custom_title_); + } + object("is_anonymous", td::JsonBool(creator->is_anonymous_)); + // object("is_member", creator->is_member_); only creator itself knows that he is a left creator + break; + } + case td_api::chatMemberStatusAdministrator::ID: { + auto administrator = static_cast(member_->status_.get()); + object("can_be_edited", td::JsonBool(administrator->can_be_edited_)); + object("can_change_info", td::JsonBool(administrator->can_change_info_)); + if (chat_type_ == Client::ChatType::Channel) { + object("can_post_messages", td::JsonBool(administrator->can_post_messages_)); + object("can_edit_messages", td::JsonBool(administrator->can_edit_messages_)); + } + object("can_delete_messages", td::JsonBool(administrator->can_delete_messages_)); + object("can_invite_users", td::JsonBool(administrator->can_invite_users_)); + object("can_restrict_members", td::JsonBool(administrator->can_restrict_members_)); + if (chat_type_ == Client::ChatType::Group || chat_type_ == Client::ChatType::Supergroup) { + object("can_pin_messages", td::JsonBool(administrator->can_pin_messages_)); + } + object("can_promote_members", td::JsonBool(administrator->can_promote_members_)); + if (!administrator->custom_title_.empty()) { + object("custom_title", administrator->custom_title_); + } + object("is_anonymous", td::JsonBool(administrator->is_anonymous_)); + break; + } + case td_api::chatMemberStatusMember::ID: + break; + case td_api::chatMemberStatusRestricted::ID: + if (chat_type_ == Client::ChatType::Supergroup) { + auto restricted = static_cast(member_->status_.get()); + object("until_date", restricted->restricted_until_date_); + Client::json_store_permissions(object, restricted->permissions_.get()); + object("is_member", td::JsonBool(restricted->is_member_)); + } + break; + case td_api::chatMemberStatusLeft::ID: + break; + case td_api::chatMemberStatusBanned::ID: { + auto banned = static_cast(member_->status_.get()); + object("until_date", banned->banned_until_date_); + break; + } + default: + UNREACHABLE(); + } + } + + private: + const td_api::chatMember *member_; + Client::ChatType chat_type_; + const Client *client_; +}; + +class Client::JsonChatMembers : public Jsonable { + public: + JsonChatMembers(const td::vector> &members, Client::ChatType chat_type, + bool administrators_only, const Client *client) + : members_(members), chat_type_(chat_type), administrators_only_(administrators_only), client_(client) { + } + void store(JsonValueScope *scope) const { + auto array = scope->enter_array(); + for (auto &member : members_) { + CHECK(member != nullptr); + bool is_member_bot = member->bot_info_ != nullptr; + if (!is_member_bot) { + // bot info may be unknown + auto user_info = client_->get_user_info(member->user_id_); + if (user_info != nullptr && user_info->type == UserInfo::Type::Bot) { + is_member_bot = true; + } + } + if (is_member_bot && member->user_id_ != client_->my_id_) { + continue; + } + if (administrators_only_) { + auto status = Client::get_chat_member_status(member->status_); + if (status != "creator" && status != "administrator") { + continue; + } + } + array << JsonChatMember(member.get(), chat_type_, client_); + } + } + + private: + const td::vector> &members_; + Client::ChatType chat_type_; + bool administrators_only_; + const Client *client_; +}; + +class Client::JsonGameHighScore : public Jsonable { + public: + JsonGameHighScore(const td_api::gameHighScore *score, const Client *client) : score_(score), client_(client) { + } + + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("position", score_->position_); + object("user", JsonUser(score_->user_id_, client_)); + object("score", score_->score_); + } + + private: + const td_api::gameHighScore *score_; + const Client *client_; +}; + +class Client::JsonUpdateTypes : public Jsonable { + public: + explicit JsonUpdateTypes(td::uint32 update_types) : update_types_(update_types) { + } + void store(JsonValueScope *scope) const { + auto array = scope->enter_array(); + for (int32 i = 0; i < static_cast(UpdateType::Size); i++) { + if (((update_types_ >> i) & 1) != 0) { + auto update_type = static_cast(i); + if (update_type != UpdateType::CustomEvent && update_type != UpdateType::CustomQuery) { + array << get_update_type_name(update_type); + } + } + } + } + + private: + td::uint32 update_types_; +}; + +class Client::JsonWebhookInfo : public Jsonable { + public: + explicit JsonWebhookInfo(const Client *client) : client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + td::CSlice url = client_->webhook_url_; + if (td::check_utf8(url)) { + object("url", url); + } else { + object("url", td::JsonRawString(url)); + } + object("has_custom_certificate", td::JsonBool(client_->has_webhook_certificate_)); + object("pending_update_count", td::narrow_cast(client_->get_pending_update_count())); + if (client_->last_webhook_error_date_ > 0) { + object("last_error_date", client_->last_webhook_error_date_); + td::CSlice error_message = client_->last_webhook_error_.message(); + if (td::check_utf8(error_message)) { + object("last_error_message", error_message); + } else { + object("last_error_message", td::JsonRawString(error_message)); + } + } + if (client_->webhook_max_connections_ > 0) { + object("max_connections", client_->webhook_max_connections_); + } + if (!url.empty()) { + object("ip_address", client_->webhook_ip_address_.empty() ? "" : client_->webhook_ip_address_); + } + if (client_->allowed_update_types_ != DEFAULT_ALLOWED_UPDATE_TYPES) { + object("allowed_updates", JsonUpdateTypes(client_->allowed_update_types_)); + } + } + + private: + const Client *client_; +}; + +class Client::JsonStickerSet : public Jsonable { + public: + JsonStickerSet(const td_api::stickerSet *sticker_set, const Client *client) + : sticker_set_(sticker_set), client_(client) { + } + void store(JsonValueScope *scope) const { + auto object = scope->enter_object(); + if (sticker_set_->id_ == Client::GREAT_MINDS_SET_ID) { + object("name", GREAT_MINDS_SET_NAME); + } else { + object("name", sticker_set_->name_); + } + object("title", sticker_set_->title_); + if (sticker_set_->thumbnail_ != nullptr) { + client_->json_store_thumbnail(object, sticker_set_->thumbnail_.get()); + } + object("is_animated", td::JsonBool(sticker_set_->is_animated_)); + object("contains_masks", td::JsonBool(sticker_set_->is_masks_)); + object("stickers", JsonStickers(sticker_set_->stickers_, client_)); + } + + private: + const td_api::stickerSet *sticker_set_; + const Client *client_; +}; + +class Client::TdOnOkCallback : public TdQueryCallback { + public: + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + auto error = move_object_as(result); + if (error->code_ != 401 && error->code_ != 406 && error->code_ != 500) { + LOG(ERROR) << "Query has failed: " << td::oneline(to_string(error)); + } + } + } +}; + +class Client::TdOnAuthorizationCallback : public TdQueryCallback { + public: + explicit TdOnAuthorizationCallback(Client *client) : client_(client) { + } + + void on_result(object_ptr result) override { + bool was_ready = client_->authorization_state_->get_id() != td_api::authorizationStateWaitPhoneNumber::ID; + if (result->get_id() == td_api::error::ID) { + auto error = move_object_as(result); + if (error->code_ == 429 || error->code_ >= 500 || (error->code_ != 401 && was_ready)) { + // try again + return client_->on_update_authorization_state(); + } + + LOG(WARNING) << "Logging out due to " << td::oneline(to_string(error)); + client_->log_out(); + } else if (was_ready) { + client_->on_update_authorization_state(); + } + } + + private: + Client *client_; +}; + +class Client::TdOnInitCallback : public TdQueryCallback { + public: + explicit TdOnInitCallback(Client *client) : client_(client) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + LOG(WARNING) << "Failed to initialize due to " << td::oneline(to_string(result)); + client_->close(); + } + } + + private: + Client *client_; +}; + +class Client::TdOnGetUserProfilePhotosCallback : public TdQueryCallback { + public: + TdOnGetUserProfilePhotosCallback(const Client *client, PromisedQueryPtr query) + : client_(client), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::chatPhotos::ID); + auto profile_photos = move_object_as(result); + answer_query(JsonChatPhotos(profile_photos.get(), client_), std::move(query_)); + } + + private: + const Client *client_; + PromisedQueryPtr query_; +}; + +class Client::TdOnSendMessageCallback : public TdQueryCallback { + public: + TdOnSendMessageCallback(Client *client, PromisedQueryPtr query) : client_(client), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::message::ID); + auto query_id = client_->get_send_message_query_id(std::move(query_), false); + client_->on_sent_message(move_object_as(result), query_id); + } + + private: + Client *client_; + PromisedQueryPtr query_; +}; + +class Client::TdOnSendMessageAlbumCallback : public TdQueryCallback { + public: + TdOnSendMessageAlbumCallback(Client *client, PromisedQueryPtr query) : client_(client), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::messages::ID); + auto messages = move_object_as(result); + auto query_id = client_->get_send_message_query_id(std::move(query_), true); + for (auto &message : messages->messages_) { + client_->on_sent_message(std::move(message), query_id); + } + } + + private: + Client *client_; + PromisedQueryPtr query_; +}; + +class Client::TdOnDeleteFailedToSendMessageCallback : public TdQueryCallback { + public: + TdOnDeleteFailedToSendMessageCallback(Client *client, int64 chat_id, int64 message_id) + : client_(client) + , chat_id_(chat_id) + , message_id_(message_id) + , old_chat_description_(client->get_chat_description(chat_id)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + auto error = move_object_as(result); + if (error->code_ != 401) { + LOG(ERROR) << "Can't delete failed to send message " << message_id_ << " because of " + << td::oneline(to_string(error)) << " in " << client_->get_chat_description(chat_id_) + << ". Old chat description: " << old_chat_description_; + } + return; + } + + CHECK(result->get_id() == td_api::ok::ID); + if (client_->get_message(chat_id_, message_id_) != nullptr) { + LOG(ERROR) << "Have cache for message " << message_id_ << " in the chat " << chat_id_; + client_->delete_message(chat_id_, message_id_, false); + } + } + + private: + Client *client_; + int64 chat_id_; + int64 message_id_; + td::string old_chat_description_; +}; + +class Client::TdOnEditMessageCallback : public TdQueryCallback { + public: + TdOnEditMessageCallback(const Client *client, PromisedQueryPtr query) : client_(client), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::message::ID); + auto message = move_object_as(result); + int64 chat_id = message->chat_id_; + int64 message_id = message->id_; + + auto message_info = client_->get_message(chat_id, message_id); + if (message_info == nullptr) { + return fail_query_with_error(std::move(query_), 400, "message not found"); + } + message_info->is_content_changed = false; + answer_query(JsonMessage(message_info, false, "edited message", client_), std::move(query_)); + } + + private: + const Client *client_; + PromisedQueryPtr query_; +}; + +class Client::TdOnEditInlineMessageCallback : public TdQueryCallback { + public: + explicit TdOnEditInlineMessageCallback(PromisedQueryPtr query) : query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::ok::ID); + answer_query(td::JsonTrue(), std::move(query_)); + } + + private: + PromisedQueryPtr query_; +}; + +class Client::TdOnStopPollCallback : public TdQueryCallback { + public: + TdOnStopPollCallback(const Client *client, int64 chat_id, int64 message_id, PromisedQueryPtr query) + : client_(client), chat_id_(chat_id), message_id_(message_id), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::ok::ID); + auto message_info = client_->get_message(chat_id_, message_id_); + if (message_info == nullptr) { + return fail_query_with_error(std::move(query_), 400, "message not found"); + } + if (message_info->content->get_id() != td_api::messagePoll::ID) { + LOG(ERROR) << "Poll not found in " << message_id_ << " in " << chat_id_; + return fail_query_with_error(std::move(query_), 400, "message poll not found"); + } + auto message_poll = static_cast(message_info->content.get()); + answer_query(JsonPoll(message_poll->poll_.get(), client_), std::move(query_)); + } + + private: + const Client *client_; + int64 chat_id_; + int64 message_id_; + PromisedQueryPtr query_; +}; + +class Client::TdOnOkQueryCallback : public TdQueryCallback { + public: + explicit TdOnOkQueryCallback(PromisedQueryPtr query) : query_(std::move(query)) { + CHECK(query_ != nullptr); + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::ok::ID); + answer_query(td::JsonTrue(), std::move(query_)); + } + + private: + PromisedQueryPtr query_; +}; + +template +class Client::TdOnCheckUserCallback : public TdQueryCallback { + public: + TdOnCheckUserCallback(const Client *client, PromisedQueryPtr query, OnSuccess on_success) + : client_(client), query_(std::move(query)), on_success_(std::move(on_success)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result), "user not found"); + } + + CHECK(result->get_id() == td_api::user::ID); + auto user = move_object_as(result); + auto user_info = client_->get_user_info(user->id_); + CHECK(user_info != nullptr); // it must have already been got through updates + + return client_->check_user_read_access(user_info, std::move(query_), std::move(on_success_)); + } + + private: + const Client *client_; + PromisedQueryPtr query_; + OnSuccess on_success_; +}; + +template +class Client::TdOnCheckUserNoFailCallback : public TdQueryCallback { + public: + TdOnCheckUserNoFailCallback(PromisedQueryPtr query, OnSuccess on_success) + : query_(std::move(query)), on_success_(std::move(on_success)) { + } + + void on_result(object_ptr result) override { + on_success_(std::move(query_)); + } + + private: + PromisedQueryPtr query_; + OnSuccess on_success_; +}; + +template +class Client::TdOnCheckChatCallback : public TdQueryCallback { + public: + TdOnCheckChatCallback(const Client *client, bool only_supergroup, AccessRights access_rights, PromisedQueryPtr query, + OnSuccess on_success) + : client_(client) + , only_supergroup_(only_supergroup) + , access_rights_(access_rights) + , query_(std::move(query)) + , on_success_(std::move(on_success)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result), "chat not found"); + } + + CHECK(result->get_id() == td_api::chat::ID); + auto chat = move_object_as(result); + auto chat_info = client_->get_chat(chat->id_); + CHECK(chat_info != nullptr); // it must have already been got through updates + CHECK(chat_info->title == chat->title_); + if (only_supergroup_ && chat_info->type != ChatInfo::Type::Supergroup) { + return fail_query(400, "Bad Request: chat not found", std::move(query_)); + } + + return client_->check_chat_access(chat->id_, access_rights_, chat_info, std::move(query_), std::move(on_success_)); + } + + private: + const Client *client_; + bool only_supergroup_; + AccessRights access_rights_; + PromisedQueryPtr query_; + OnSuccess on_success_; +}; + +template +class Client::TdOnSearchStickerSetCallback : public TdQueryCallback { + public: + TdOnSearchStickerSetCallback(PromisedQueryPtr query, OnSuccess on_success) + : query_(std::move(query)), on_success_(std::move(on_success)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result), "sticker set not found"); + } + + CHECK(result->get_id() == td_api::stickerSet::ID); + auto sticker_set = move_object_as(result); + on_success_(sticker_set->id_, std::move(query_)); + } + + private: + PromisedQueryPtr query_; + OnSuccess on_success_; +}; + +class Client::TdOnResolveBotUsernameCallback : public TdQueryCallback { + public: + TdOnResolveBotUsernameCallback(Client *client, td::string username) + : client_(client), username_(std::move(username)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return client_->on_resolve_bot_username(username_, 0); + } + + CHECK(result->get_id() == td_api::chat::ID); + auto chat = move_object_as(result); + auto chat_info = client_->get_chat(chat->id_); + CHECK(chat_info != nullptr); // it must have already been got through updates + if (chat_info->type != ChatInfo::Type::Private) { + return client_->on_resolve_bot_username(username_, 0); + } + auto user_info = client_->get_user_info(chat_info->user_id); + CHECK(user_info != nullptr); + if (user_info->type != UserInfo::Type::Bot) { + return client_->on_resolve_bot_username(username_, 0); + } + + client_->on_resolve_bot_username(username_, chat_info->user_id); + } + + private: + Client *client_; + td::string username_; +}; + +template +class Client::TdOnCheckMessageCallback : public TdQueryCallback { + public: + TdOnCheckMessageCallback(Client *client, int64 chat_id, bool allow_empty, Slice message_type, PromisedQueryPtr query, + OnSuccess on_success) + : client_(client) + , chat_id_(chat_id) + , allow_empty_(allow_empty) + , message_type_(message_type) + , query_(std::move(query)) + , on_success_(std::move(on_success)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + auto error = move_object_as(result); + if (error->code_ == 429) { + LOG(WARNING) << "Failed to get " << message_type_; + } + if (allow_empty_) { + return on_success_(chat_id_, 0, std::move(query_)); + } + return fail_query_with_error(std::move(query_), std::move(error), PSLICE() << message_type_ << " not found"); + } + + CHECK(result->get_id() == td_api::message::ID); + auto full_message_id = client_->add_message(move_object_as(result)); + CHECK(full_message_id.chat_id == chat_id_); + on_success_(full_message_id.chat_id, full_message_id.message_id, std::move(query_)); + } + + private: + Client *client_; + int64 chat_id_; + bool allow_empty_; + Slice message_type_; + PromisedQueryPtr query_; + OnSuccess on_success_; +}; + +template +class Client::TdOnCheckRemoteFileIdCallback : public TdQueryCallback { + public: + TdOnCheckRemoteFileIdCallback(PromisedQueryPtr query, OnSuccess on_success) + : query_(std::move(query)), on_success_(std::move(on_success)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result), "invalid file_id"); + } + + CHECK(result->get_id() == td_api::file::ID); + on_success_(move_object_as(result), std::move(query_)); + } + + private: + PromisedQueryPtr query_; + OnSuccess on_success_; +}; + +template +class Client::TdOnGetChatMemberCallback : public TdQueryCallback { + public: + TdOnGetChatMemberCallback(PromisedQueryPtr query, OnSuccess on_success) + : query_(std::move(query)), on_success_(std::move(on_success)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result), "user not found"); + } + + CHECK(result->get_id() == td_api::chatMember::ID); + on_success_(move_object_as(result), std::move(query_)); + } + + private: + PromisedQueryPtr query_; + OnSuccess on_success_; +}; + +class Client::TdOnDownloadFileCallback : public TdQueryCallback { + public: + TdOnDownloadFileCallback(Client *client, int32 file_id) : client_(client), file_id_(file_id) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + auto error = move_object_as(result); + return client_->on_file_download(file_id_, Status::Error(error->code_, error->message_)); + } + CHECK(result->get_id() == td_api::file::ID); + if (client_->is_file_being_downloaded(file_id_)) { // if download is yet not finished + client_->download_started_file_ids_.insert(file_id_); + } + client_->on_update_file(move_object_as(result)); + } + + private: + Client *client_; + int32 file_id_; +}; + +class Client::TdOnCancelDownloadFileCallback : public TdQueryCallback { + public: + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + LOG(ERROR) << "Failed to cancel download file"; + return; + } + CHECK(result->get_id() == td_api::ok::ID); + } +}; + +class Client::TdOnGetReplyMessageCallback : public TdQueryCallback { + public: + TdOnGetReplyMessageCallback(Client *client, int64 chat_id) : client_(client), chat_id_(chat_id) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return client_->on_get_reply_message(chat_id_, nullptr); + } + + CHECK(result->get_id() == td_api::message::ID); + client_->on_get_reply_message(chat_id_, move_object_as(result)); + } + + private: + Client *client_; + int64 chat_id_; +}; + +class Client::TdOnGetEditedMessageCallback : public TdQueryCallback { + public: + explicit TdOnGetEditedMessageCallback(Client *client) : client_(client) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + auto error = move_object_as(result); + if (error->code_ == 429) { + LOG(WARNING) << "Failed to get edited message"; + } + return client_->on_get_edited_message(nullptr); + } + + CHECK(result->get_id() == td_api::message::ID); + client_->on_get_edited_message(move_object_as(result)); + } + + private: + Client *client_; +}; + +class Client::TdOnGetCallbackQueryMessageCallback : public TdQueryCallback { + public: + TdOnGetCallbackQueryMessageCallback(Client *client, int32 user_id, int state) + : client_(client), user_id_(user_id), state_(state) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + auto error = move_object_as(result); + if (error->code_ == 429) { + LOG(WARNING) << "Failed to get callback query message"; + } + return client_->on_get_callback_query_message(nullptr, user_id_, state_); + } + + CHECK(result->get_id() == td_api::message::ID); + client_->on_get_callback_query_message(move_object_as(result), user_id_, state_); + } + + private: + Client *client_; + int32 user_id_; + int state_; +}; + +class Client::TdOnGetStickerSetCallback : public TdQueryCallback { + public: + TdOnGetStickerSetCallback(Client *client, int64 set_id, int32 new_callback_query_user_id, int64 new_message_chat_id) + : client_(client) + , set_id_(set_id) + , new_callback_query_user_id_(new_callback_query_user_id) + , new_message_chat_id_(new_message_chat_id) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + auto error = move_object_as(result); + if (error->message_ != "STICKERSET_INVALID" && error->code_ != 401 && error->code_ != 500) { + LOG(ERROR) << "Failed to get sticker set " << set_id_ << " from callback query by user " + << new_callback_query_user_id_ << "/new message in chat " << new_message_chat_id_ << ": " + << td::oneline(to_string(error)); + } + return client_->on_get_sticker_set(set_id_, new_callback_query_user_id_, new_message_chat_id_, nullptr); + } + + CHECK(result->get_id() == td_api::stickerSet::ID); + client_->on_get_sticker_set(set_id_, new_callback_query_user_id_, new_message_chat_id_, + move_object_as(result)); + } + + private: + Client *client_; + int64 set_id_; + int32 new_callback_query_user_id_; + int64 new_message_chat_id_; +}; + +class Client::TdOnGetChatStickerSetCallback : public TdQueryCallback { + public: + TdOnGetChatStickerSetCallback(Client *client, int64 chat_id, int64 pinned_message_id, PromisedQueryPtr query) + : client_(client), chat_id_(chat_id), pinned_message_id_(pinned_message_id), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + auto chat_info = client_->get_chat(chat_id_); + CHECK(chat_info != nullptr); + CHECK(chat_info->type == ChatInfo::Type::Supergroup); + client_->set_supergroup_sticker_set_id(chat_info->supergroup_id, 0); + } else { + CHECK(result->get_id() == td_api::stickerSet::ID); + auto sticker_set = move_object_as(result); + client_->on_get_sticker_set_name(sticker_set->id_, sticker_set->name_); + } + + answer_query(JsonChat(chat_id_, true, client_, pinned_message_id_), std::move(query_)); + } + + private: + Client *client_; + int64 chat_id_; + int64 pinned_message_id_; + PromisedQueryPtr query_; +}; + +class Client::TdOnGetChatPinnedMessageCallback : public TdQueryCallback { + public: + TdOnGetChatPinnedMessageCallback(Client *client, int64 chat_id, PromisedQueryPtr query) + : client_(client), chat_id_(chat_id), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + int64 pinned_message_id = 0; + if (result->get_id() == td_api::error::ID) { + auto error = move_object_as(result); + if (error->code_ == 429) { + return fail_query_with_error(std::move(query_), std::move(error)); + } else if (error->code_ != 404 && error->message_ != "CHANNEL_PRIVATE") { + LOG(ERROR) << "Failed to get chat pinned message: " << to_string(error); + } + } else { + CHECK(result->get_id() == td_api::message::ID); + auto full_message_id = client_->add_message(move_object_as(result)); + pinned_message_id = full_message_id.message_id; + CHECK(full_message_id.chat_id == chat_id_); + CHECK(pinned_message_id > 0); + } + + auto chat_info = client_->get_chat(chat_id_); + CHECK(chat_info != nullptr); + if (chat_info->type == ChatInfo::Type::Supergroup) { + auto supergroup_info = client_->get_supergroup_info(chat_info->supergroup_id); + CHECK(supergroup_info != nullptr); + + auto sticker_set_id = supergroup_info->sticker_set_id; + if (sticker_set_id != 0 && client_->get_sticker_set_name(sticker_set_id).empty()) { + return client_->send_request( + make_object(sticker_set_id), + std::make_unique(client_, chat_id_, pinned_message_id, std::move(query_))); + } + } + + answer_query(JsonChat(chat_id_, true, client_, pinned_message_id), std::move(query_)); + } + + private: + Client *client_; + int64 chat_id_; + PromisedQueryPtr query_; +}; + +class Client::TdOnGetChatPinnedMessageToUnpinCallback : public TdQueryCallback { + public: + TdOnGetChatPinnedMessageToUnpinCallback(Client *client, int64 chat_id, PromisedQueryPtr query) + : client_(client), chat_id_(chat_id), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + int64 pinned_message_id = 0; + if (result->get_id() == td_api::error::ID) { + auto error = move_object_as(result); + if (error->code_ == 429) { + return fail_query_with_error(std::move(query_), std::move(error)); + } else { + return fail_query_with_error(std::move(query_), make_object(400, "Message to unpin not found")); + } + } + + CHECK(result->get_id() == td_api::message::ID); + auto full_message_id = client_->add_message(move_object_as(result)); + pinned_message_id = full_message_id.message_id; + CHECK(full_message_id.chat_id == chat_id_); + CHECK(pinned_message_id > 0); + + client_->send_request(make_object(chat_id_, pinned_message_id), + std::make_unique(std::move(query_))); + } + + private: + Client *client_; + int64 chat_id_; + PromisedQueryPtr query_; +}; + +class Client::TdOnGetMyCommandsCallback : public TdQueryCallback { + public: + explicit TdOnGetMyCommandsCallback(PromisedQueryPtr query) : query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::userFullInfo::ID); + auto user_full_info = move_object_as(result); + td::vector> commands; + if (user_full_info->bot_info_ != nullptr) { + commands = std::move(user_full_info->bot_info_->commands_); + } + answer_query(td::json_array(commands, [](auto &command) { return JsonBotCommand(command.get()); }), + std::move(query_)); + } + + private: + PromisedQueryPtr query_; +}; + +class Client::TdOnGetChatFullInfoCallback : public TdQueryCallback { + public: + TdOnGetChatFullInfoCallback(Client *client, int64 chat_id, PromisedQueryPtr query) + : client_(client), chat_id_(chat_id), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + // we don't need the result, everything is already received through updates + + client_->send_request(make_object(chat_id_), + std::make_unique(client_, chat_id_, std::move(query_))); + } + + private: + Client *client_; + int64 chat_id_; + PromisedQueryPtr query_; +}; + +class Client::TdOnGetGroupMembersCallback : public TdQueryCallback { + public: + TdOnGetGroupMembersCallback(const Client *client, bool administrators_only, PromisedQueryPtr query) + : client_(client), administrators_only_(administrators_only), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::basicGroupFullInfo::ID); + auto group_full_info = move_object_as(result); + answer_query(JsonChatMembers(group_full_info->members_, Client::ChatType::Group, administrators_only_, client_), + std::move(query_)); + } + + private: + const Client *client_; + bool administrators_only_; + PromisedQueryPtr query_; +}; + +class Client::TdOnGetSupergroupMembersCallback : public TdQueryCallback { + public: + TdOnGetSupergroupMembersCallback(const Client *client, Client::ChatType chat_type, PromisedQueryPtr query) + : client_(client), chat_type_(chat_type), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::chatMembers::ID); + auto chat_members = move_object_as(result); + answer_query(JsonChatMembers(chat_members->members_, chat_type_, false, client_), std::move(query_)); + } + + private: + const Client *client_; + Client::ChatType chat_type_; + PromisedQueryPtr query_; +}; + +class Client::TdOnGetSupergroupMembersCountCallback : public TdQueryCallback { + public: + explicit TdOnGetSupergroupMembersCountCallback(PromisedQueryPtr query) : query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::supergroupFullInfo::ID); + auto supergroup_full_info = move_object_as(result); + if (supergroup_full_info->member_count_ == 0) { + return fail_query(400, "Bad Request: need administrator rights", std::move(query_)); + } + return answer_query(td::VirtuallyJsonableInt(supergroup_full_info->member_count_), std::move(query_)); + } + + private: + PromisedQueryPtr query_; +}; + +class Client::TdOnGenerateChatInviteLinkCallback : public TdQueryCallback { + public: + explicit TdOnGenerateChatInviteLinkCallback(PromisedQueryPtr query) : query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::chatInviteLink::ID); + auto invite_link = move_object_as(result); + return answer_query(td::VirtuallyJsonableString(invite_link->invite_link_), std::move(query_)); + } + + private: + PromisedQueryPtr query_; +}; + +class Client::TdOnGetGameHighScoresCallback : public TdQueryCallback { + public: + TdOnGetGameHighScoresCallback(const Client *client, PromisedQueryPtr query) + : client_(client), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::gameHighScores::ID); + auto game_high_scores = move_object_as(result); + answer_query(td::json_array(game_high_scores->scores_, + [client = client_](auto &score) { return JsonGameHighScore(score.get(), client); }), + std::move(query_)); + } + + private: + const Client *client_; + PromisedQueryPtr query_; +}; + +class Client::TdOnReturnFileCallback : public TdQueryCallback { + public: + TdOnReturnFileCallback(const Client *client, PromisedQueryPtr query) : client_(client), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::file::ID); + auto file = move_object_as(result); + answer_query(JsonFile(file.get(), client_), std::move(query_)); + } + + private: + const Client *client_; + PromisedQueryPtr query_; +}; + +class Client::TdOnReturnStickerSetCallback : public TdQueryCallback { + public: + TdOnReturnStickerSetCallback(Client *client, bool return_sticker_set, PromisedQueryPtr query) + : client_(client), return_sticker_set_(return_sticker_set), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::stickerSet::ID); + auto sticker_set = move_object_as(result); + client_->on_get_sticker_set_name(sticker_set->id_, sticker_set->name_); + if (return_sticker_set_) { + answer_query(JsonStickerSet(sticker_set.get(), client_), std::move(query_)); + } else { + answer_query(td::JsonTrue(), std::move(query_)); + } + } + + private: + Client *client_; + bool return_sticker_set_; + PromisedQueryPtr query_; +}; + +class Client::TdOnSendCustomRequestCallback : public TdQueryCallback { + public: + explicit TdOnSendCustomRequestCallback(PromisedQueryPtr query) : query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + if (result->get_id() == td_api::error::ID) { + return fail_query_with_error(std::move(query_), move_object_as(result)); + } + + CHECK(result->get_id() == td_api::customRequestResult::ID); + auto res = move_object_as(result); + answer_query(JsonCustomJson(res->result_), std::move(query_)); + } + + private: + PromisedQueryPtr query_; +}; + +void Client::close() { + need_close_ = true; + if (td_client_.empty()) { + set_timeout_in(0); + } else if (!closing_) { + do_send_request(make_object(), std::make_unique()); + } +} + +void Client::log_out() { + if (!td_client_.empty() && !logging_out_ && !closing_) { + do_send_request(make_object(), std::make_unique()); + } +} + +std::size_t Client::get_pending_update_count() const { + return parameters_->shared_data_->tqueue_->get_size(tqueue_id_); +} + +ServerBotInfo Client::get_bot_info() const { + ServerBotInfo res; + res.id_ = bot_token_id_; + res.token_ = bot_token_; + auto user_info = get_user_info(my_id_); + if (user_info != nullptr) { + res.username_ = user_info->username; + } else if (!was_authorized_) { + res.username_ = ""; + } else { + res.username_ = ""; + } + res.webhook_ = webhook_url_; + res.has_webhook_certificate_ = has_webhook_certificate_; + auto &tqueue = parameters_->shared_data_->tqueue_; + res.head_update_id_ = tqueue->get_head(tqueue_id_).value(); + res.tail_update_id_ = tqueue->get_tail(tqueue_id_).value(); + res.pending_update_count_ = tqueue->get_size(tqueue_id_); + res.webhook_max_connections_ = webhook_max_connections_; + res.start_timestamp_ = start_timestamp_; + return res; +} + +void Client::start_up() { + start_timestamp_ = td::Time::now(); + next_bot_updates_warning_date_ = start_timestamp_ + 600; + schedule_next_delete_messages_lru(); + webhook_set_date_ = start_timestamp_; + + sticker_set_names_[GREAT_MINDS_SET_ID] = GREAT_MINDS_SET_NAME.str(); + + auto colon_pos = bot_token_.find_first_of(':'); + if (colon_pos == td::string::npos) { + LOG(WARNING) << "Wrong bot token " << bot_token_; + logging_out_ = true; + return finish_closing(); + } + bot_token_id_ = bot_token_.substr(0, colon_pos); + + auto base64_bot_token = bot_token_.substr(colon_pos + 1); + if (td::base64url_decode(base64_bot_token).is_error() || base64_bot_token.size() < 24) { + LOG(WARNING) << "Wrong bot token " << bot_token_; + logging_out_ = true; + return finish_closing(); + } + + bot_token_with_dc_ = bot_token_ + (is_test_dc_ ? ":T" : ""); + + auto context = std::make_shared(); + set_context(context); + set_tag(bot_token_id_); + + auto r_absolute_dir = td::realpath(td::string(".") + TD_DIR_SLASH, true); + CHECK(r_absolute_dir.is_ok()); + absolute_dir_ = r_absolute_dir.move_as_ok(); + auto suff = bot_token_with_dc_ + TD_DIR_SLASH; +#if TD_PORT_WINDOWS + for (auto &c : suff) { + if (c == ':') { + c = '~'; + } + } +#endif + dir_ = td::string(".") + TD_DIR_SLASH + suff; + if (absolute_dir_.back() != TD_DIR_SLASH) { + absolute_dir_ += TD_DIR_SLASH; + } + absolute_dir_ += suff; + + class TdCallback : public td::TdCallback { + public: + explicit TdCallback(td::ActorId client) : client_(std::move(client)) { + } + void on_result(td::uint64 id, object_ptr result) override { + send_closure_later(client_, &Client::on_result, id, std::move(result)); + } + void on_error(td::uint64 id, object_ptr result) override { + send_closure_later(client_, &Client::on_result, id, std::move(result)); + } + + private: + td::ActorId client_; + }; + td::ClientActor::Options options; + options.net_query_stats = parameters_->net_query_stats_; + td_client_ = td::create_actor("TdClientActor", td::make_unique(actor_id(this)), + std::move(options)); +} + +void Client::send(PromisedQueryPtr query) { + if (!query->is_internal()) { + send_closure( + stat_actor_, &BotStatActor::add_event, + ServerBotStat::Request{query->query_size(), query->file_count(), query->files_size(), query->files_max_size()}, + td::Time::now()); + } + cmd_queue_.emplace(std::move(query)); + loop(); +} + +void Client::raw_event(const td::Event::Raw &event) { + long_poll_wakeup(true); +} + +void Client::loop() { + if (logging_out_ || closing_ || was_authorized_) { + while (!cmd_queue_.empty()) { + auto query = std::move(cmd_queue_.front()); + cmd_queue_.pop(); + on_cmd(std::move(query)); + } + } +} + +void Client::on_get_reply_message(int64 chat_id, object_ptr reply_to_message) { + auto &queue = new_message_queues_[chat_id]; + CHECK(queue.has_active_request_); + queue.has_active_request_ = false; + + CHECK(!queue.queue_.empty()); + object_ptr &message = queue.queue_.front().message; + CHECK(chat_id == message->chat_id_); + int64 &reply_to_message_id = get_reply_to_message_id(message); + CHECK(reply_to_message_id > 0); + if (reply_to_message == nullptr) { + LOG(INFO) << "Can't find message " << reply_to_message_id << " in chat " << chat_id + << ". It is already deleted or inaccessible because of the chosen privacy mode"; + reply_to_message_id = 0; + } else { + CHECK(chat_id == reply_to_message->chat_id_); + CHECK(reply_to_message_id == reply_to_message->id_); + LOG(INFO) << "Receive reply to message " << reply_to_message_id << " in chat " << chat_id; + add_message(std::move(reply_to_message)); + } + + process_new_message_queue(chat_id); +} + +void Client::on_get_edited_message(object_ptr edited_message) { + if (edited_message == nullptr) { + LOG(INFO) << "Can't find just edited message. It is already deleted or inaccessible because of chosen privacy mode"; + } else { + add_new_message(std::move(edited_message), true); + } +} + +void Client::on_get_callback_query_message(object_ptr message, int32 user_id, int state) { + CHECK(user_id != 0); + auto &queue = new_callback_query_queues_[user_id]; + CHECK(queue.has_active_request_); + queue.has_active_request_ = false; + + CHECK(!queue.queue_.empty()); + int64 chat_id = queue.queue_.front()->chat_id_; + int64 message_id = queue.queue_.front()->message_id_; + if (message == nullptr) { + if (state == 0) { + LOG(INFO) << "Can't find callback query message " << message_id << " in chat " << chat_id + << ". It may be already deleted"; + } else { + CHECK(state == 1); + auto message_info = get_message_editable(chat_id, message_id); + if (message_info == nullptr) { + LOG(INFO) << "Can't find callback query message " << message_id << " in chat " << chat_id + << ". It may be already deleted, while searcing for its reply to message"; + process_new_callback_query_queue(user_id, state); + return; + } + LOG(INFO) << "Can't find callback query reply to message " << message_info->reply_to_message_id << " in chat " + << chat_id << ". It may be already deleted"; + message_info->is_reply_to_message_deleted = true; + } + } else { + LOG(INFO) << "Receive callback query " << (state == 1 ? "reply to " : "") << "message " << message_id << " in chat " + << chat_id; + add_message(std::move(message)); + } + process_new_callback_query_queue(user_id, state + 1); +} + +void Client::on_get_sticker_set(int64 set_id, int32 new_callback_query_user_id, int64 new_message_chat_id, + object_ptr sticker_set) { + if (new_callback_query_user_id != 0) { + auto &queue = new_callback_query_queues_[new_callback_query_user_id]; + CHECK(queue.has_active_request_); + queue.has_active_request_ = false; + + CHECK(!queue.queue_.empty()); + } + if (new_message_chat_id != 0) { + auto &queue = new_message_queues_[new_message_chat_id]; + CHECK(queue.has_active_request_); + queue.has_active_request_ = false; + + CHECK(!queue.queue_.empty()); + } + + CHECK(set_id != 0); + if (set_id != GREAT_MINDS_SET_ID) { + td::string &set_name = sticker_set_names_[set_id]; + if (sticker_set != nullptr) { + set_name = std::move(sticker_set->name_); + } + } + + if (new_callback_query_user_id != 0) { + process_new_callback_query_queue(new_callback_query_user_id, 2); + } + if (new_message_chat_id != 0) { + process_new_message_queue(new_message_chat_id); + } +} + +void Client::on_get_sticker_set_name(int64 set_id, const td::string &name) { + CHECK(set_id != 0); + if (set_id != GREAT_MINDS_SET_ID) { + sticker_set_names_[set_id] = name; + } +} + +template +void Client::check_user_read_access(const UserInfo *user_info, PromisedQueryPtr query, OnSuccess on_success) { + CHECK(user_info != nullptr); + if (!user_info->have_access) { + return fail_query(400, "Bad Request: have no access to the user", std::move(query)); + } + on_success(std::move(query)); +} + +template +void Client::check_user(int32 user_id, PromisedQueryPtr query, OnSuccess on_success) { + const UserInfo *user_info = get_user_info(user_id); + if (user_info != nullptr) { + return check_user_read_access(user_info, std::move(query), std::move(on_success)); + } + send_request(make_object(user_id), + std::make_unique>(this, std::move(query), std::move(on_success))); +} + +template +void Client::check_user_no_fail(int32 user_id, PromisedQueryPtr query, OnSuccess on_success) { + const UserInfo *user_info = get_user_info(user_id); + if (user_info != nullptr) { + on_success(std::move(query)); + return; + } + send_request(make_object(user_id), + std::make_unique>(std::move(query), std::move(on_success))); +} + +template +void Client::check_chat_access(int64 chat_id, AccessRights access_rights, const ChatInfo *chat_info, + PromisedQueryPtr query, OnSuccess on_success) const { + CHECK(chat_info != nullptr); + bool need_write_access = access_rights == AccessRights::Write; + bool need_edit_access = access_rights == AccessRights::Edit || need_write_access; + bool need_read_access = true; + switch (chat_info->type) { + case ChatInfo::Type::Private: { + auto user_info = get_user_info(chat_info->user_id); + CHECK(user_info != nullptr); + if (user_info->type == UserInfo::Type::Deleted && need_edit_access) { + return fail_query(403, "Forbidden: user is deactivated", std::move(query)); + } + if (user_info->type == UserInfo::Type::Unknown) { + return fail_query(400, "Bad Request: private chat not found", std::move(query)); + } + break; + } + case ChatInfo::Type::Group: { + auto group_info = get_group_info(chat_info->group_id); + CHECK(group_info != nullptr); + if (!group_info->is_active && need_write_access) { + if (group_info->upgraded_to_supergroup_id != 0) { + std::unordered_map> parameters; + auto updagraded_to_chat_id = get_supergroup_chat_id(group_info->upgraded_to_supergroup_id); + parameters.emplace("migrate_to_chat_id", std::make_unique(updagraded_to_chat_id)); + return fail_query(400, "Bad Request: group chat was upgraded to a supergroup chat", std::move(query), + std::move(parameters)); + } else { + LOG(WARNING) << "Group chat " << chat_info->group_id << " with " << group_info->member_count + << " members and title \"" << chat_info->title << "\" is deactivated"; + return fail_query(400, "Bad Request: group chat was deactivated", std::move(query)); + } + } + if (group_info->is_active && group_info->kicked && need_edit_access) { + return fail_query(403, "Forbidden: bot was kicked from the group chat", std::move(query)); + } + if (group_info->is_active && group_info->left && need_edit_access) { + return fail_query(403, "Forbidden: bot is not a member of the group chat", std::move(query)); + } + break; + } + case ChatInfo::Type::Supergroup: { + auto supergroup_info = get_supergroup_info(chat_info->supergroup_id); + CHECK(supergroup_info != nullptr); + bool is_public = !supergroup_info->username.empty() || supergroup_info->has_location; + if (supergroup_info->status->get_id() == td_api::chatMemberStatusBanned::ID) { + if (supergroup_info->is_supergroup) { + return fail_query(403, "Forbidden: bot was kicked from the supergroup chat", std::move(query)); + } else { + return fail_query(403, "Forbidden: bot was kicked from the channel chat", std::move(query)); + } + } + bool need_more_access_rights = is_public ? need_edit_access : need_read_access; + if (supergroup_info->status->get_id() == td_api::chatMemberStatusLeft::ID && need_more_access_rights) { + if (supergroup_info->is_supergroup) { + return fail_query(403, "Forbidden: bot is not a member of the supergroup chat", std::move(query)); + } else { + return fail_query(403, "Forbidden: bot is not a member of the channel chat", std::move(query)); + } + } + break; + } + case ChatInfo::Type::Unknown: + default: + UNREACHABLE(); + } + on_success(chat_id, std::move(query)); +} + +template +void Client::check_chat(Slice chat_id_str, AccessRights access_rights, PromisedQueryPtr query, OnSuccess on_success) { + if (chat_id_str.empty()) { + return fail_query(400, "Bad Request: chat_id is empty", std::move(query)); + } + + if (chat_id_str[0] == '@') { + return send_request(make_object(chat_id_str.str()), + std::make_unique>(this, true, access_rights, std::move(query), + std::move(on_success))); + } + + auto chat_id = td::to_integer(chat_id_str); + auto chat_info = get_chat(chat_id); + if (chat_info != nullptr) { + return check_chat_access(chat_id, access_rights, chat_info, std::move(query), std::move(on_success)); + } + send_request(make_object(chat_id), + std::make_unique>(this, false, access_rights, std::move(query), + std::move(on_success))); +} + +template +void Client::check_remote_file_id(td::string file_id, PromisedQueryPtr query, OnSuccess on_success) { + if (file_id.empty()) { + return fail_query(400, "Bad Request: file_id not specified", std::move(query)); + } + + send_request(make_object(std::move(file_id), nullptr), + std::make_unique>(std::move(query), std::move(on_success))); +} + +bool Client::is_chat_member(const object_ptr &status) { + switch (status->get_id()) { + case td_api::chatMemberStatusBanned::ID: + case td_api::chatMemberStatusLeft::ID: + return false; + case td_api::chatMemberStatusRestricted::ID: + return static_cast(status.get())->is_member_; + default: + // ignore Creator.is_member_ + return true; + } +} + +bool Client::have_message_access(int64 chat_id) const { + auto chat_info = get_chat(chat_id); + CHECK(chat_info != nullptr); + switch (chat_info->type) { + case ChatInfo::Type::Private: + case ChatInfo::Type::Group: + return true; + case ChatInfo::Type::Supergroup: { + auto supergroup_info = get_supergroup_info(chat_info->supergroup_id); + CHECK(supergroup_info != nullptr); + return is_chat_member(supergroup_info->status); + } + case ChatInfo::Type::Unknown: + default: + UNREACHABLE(); + return false; + } +} + +template +void Client::check_message(Slice chat_id_str, int64 message_id, bool allow_empty, AccessRights access_rights, + Slice message_type, PromisedQueryPtr query, OnSuccess on_success) { + check_chat(chat_id_str, access_rights, std::move(query), + [this, message_id, allow_empty, message_type, on_success = std::move(on_success)]( + int64 chat_id, PromisedQueryPtr query) mutable { + if (!have_message_access(chat_id)) { + return fail_query_with_error(std::move(query), 400, "MESSAGE_NOT_FOUND", + PSLICE() << message_type << " not found"); + } + + send_request(make_object(chat_id, message_id), + std::make_unique>( + this, chat_id, allow_empty, message_type, std::move(query), std::move(on_success))); + }); +} + +template +void Client::resolve_sticker_set(const td::string &sticker_set_name, PromisedQueryPtr query, OnSuccess on_success) { + if (sticker_set_name.empty()) { + return fail_query(400, "Bad Request: sticker_set_name is empty", std::move(query)); + } + + send_request(make_object(sticker_set_name), + std::make_unique>(std::move(query), std::move(on_success))); +} + +void Client::fix_reply_markup_bot_user_ids(object_ptr &reply_markup) const { + if (reply_markup == nullptr || reply_markup->get_id() != td_api::replyMarkupInlineKeyboard::ID) { + return; + } + auto inline_keyboard = static_cast(reply_markup.get()); + for (auto &row : inline_keyboard->rows_) { + for (auto &button : row) { + CHECK(button != nullptr); + CHECK(button->type_ != nullptr); + if (button->type_->get_id() != td_api::inlineKeyboardButtonTypeLoginUrl::ID) { + continue; + } + auto login_url_button = static_cast(button->type_.get()); + if (login_url_button->id_ % 1000 != 0) { + continue; + } + auto it = temp_to_real_bot_user_id_.find(std::abs(login_url_button->id_)); + CHECK(it != temp_to_real_bot_user_id_.end()); + auto bot_user_id = it->second; + CHECK(bot_user_id != 0); + if (login_url_button->id_ < 0) { + login_url_button->id_ = -bot_user_id; + } else { + login_url_button->id_ = bot_user_id; + } + } + } +} + +void Client::fix_inline_query_results_bot_user_ids( + td::vector> &results) const { + for (auto &result : results) { + td_api::downcast_call( + *result, [this](auto &result_type) { this->fix_reply_markup_bot_user_ids(result_type.reply_markup_); }); + } +} + +void Client::resolve_bot_usernames(PromisedQueryPtr query, td::Promise on_success) { + CHECK(!unresolved_bot_usernames_.empty()); + auto query_id = current_bot_resolve_query_id_++; + auto &pending_query = pending_bot_resolve_queries_[query_id]; + pending_query.pending_resolve_count = unresolved_bot_usernames_.size(); + pending_query.query = std::move(query); + pending_query.on_success = std::move(on_success); + for (auto &username : unresolved_bot_usernames_) { + auto &query_ids = awaiting_bot_resolve_queries_[username]; + query_ids.push_back(query_id); + if (query_ids.size() == 1) { + send_request(make_object(username), + std::make_unique(this, username)); + } + } + unresolved_bot_usernames_.clear(); +} + +template +void Client::resolve_reply_markup_bot_usernames(object_ptr reply_markup, PromisedQueryPtr query, + OnSuccess on_success) { + if (!unresolved_bot_usernames_.empty()) { + CHECK(reply_markup != nullptr); + CHECK(reply_markup->get_id() == td_api::replyMarkupInlineKeyboard::ID); + return resolve_bot_usernames( + std::move(query), + td::PromiseCreator::lambda([this, reply_markup = std::move(reply_markup), + on_success = std::move(on_success)](td::Result result) mutable { + if (result.is_ok()) { + fix_reply_markup_bot_user_ids(reply_markup); + on_success(std::move(reply_markup), result.move_as_ok()); + } + })); + } + on_success(std::move(reply_markup), std::move(query)); +} + +template +void Client::resolve_inline_query_results_bot_usernames(td::vector> results, + PromisedQueryPtr query, OnSuccess on_success) { + if (!unresolved_bot_usernames_.empty()) { + return resolve_bot_usernames( + std::move(query), + td::PromiseCreator::lambda([this, results = std::move(results), + on_success = std::move(on_success)](td::Result result) mutable { + if (result.is_ok()) { + fix_inline_query_results_bot_user_ids(results); + on_success(std::move(results), result.move_as_ok()); + } + })); + } + on_success(std::move(results), std::move(query)); +} + +void Client::on_resolve_bot_username(const td::string &username, int32 user_id) { + auto query_ids_it = awaiting_bot_resolve_queries_.find(username); + CHECK(query_ids_it != awaiting_bot_resolve_queries_.end()); + CHECK(!query_ids_it->second.empty()); + auto query_ids = std::move(query_ids_it->second); + awaiting_bot_resolve_queries_.erase(query_ids_it); + + if (user_id == 0) { + bot_user_ids_.erase(username); + } else { + auto &temp_bot_user_id = bot_user_ids_[username]; + temp_to_real_bot_user_id_[temp_bot_user_id] = user_id; + temp_bot_user_id = user_id; + } + + for (auto query_id : query_ids) { + auto it = pending_bot_resolve_queries_.find(query_id); + if (it == pending_bot_resolve_queries_.end()) { + // the query has already failed + continue; + } + CHECK(it->second.pending_resolve_count > 0); + it->second.pending_resolve_count--; + if (it->second.pending_resolve_count == 0 || user_id == 0) { + if (user_id == 0) { + fail_query(400, PSTRING() << "Bad Request: bot \"" << username << "\" not found", std::move(it->second.query)); + } else { + it->second.on_success.set_value(std::move(it->second.query)); + } + pending_bot_resolve_queries_.erase(it); + } + } +} + +template +void Client::get_chat_member(int64 chat_id, int32 user_id, PromisedQueryPtr query, OnSuccess on_success) { + check_user_no_fail( + user_id, std::move(query), + [this, chat_id, user_id, on_success = std::move(on_success)](PromisedQueryPtr query) mutable { + send_request(make_object(chat_id, user_id), + std::make_unique>(std::move(query), std::move(on_success))); + }); +} + +void Client::send_request(object_ptr &&f, std::unique_ptr handler) { + if (logging_out_) { + return handler->on_result(make_object(LOGGING_OUT_ERROR_CODE, LOGGING_OUT_ERROR_DESCRIPTION.str())); + } + if (closing_) { + return handler->on_result(make_object(CLOSING_ERROR_CODE, CLOSING_ERROR_DESCRIPTION.str())); + } + + do_send_request(std::move(f), std::move(handler)); +} + +void Client::do_send_request(object_ptr &&f, std::unique_ptr handler) { + CHECK(!td_client_.empty()); + auto id = handlers_.create(std::move(handler)); + send_closure(td_client_, &td::ClientActor::request, id, std::move(f)); +} + +td_api::object_ptr Client::execute(object_ptr &&f) { + return td::ClientActor::execute(std::move(f)); +} + +void Client::on_update_file(object_ptr file) { + auto file_id = file->id_; + if (!is_file_being_downloaded(file_id)) { + return; + } + if (!parameters_->local_mode_ && file->local_->downloaded_size_ > MAX_DOWNLOAD_FILE_SIZE) { + if (file->local_->is_downloading_active_) { + send_request(make_object(file_id, false), + std::make_unique()); + } + return on_file_download(file_id, Status::Error(400, "Bad Request: file is too big")); + } + if (file->local_->is_downloading_completed_) { + return on_file_download(file_id, std::move(file)); + } + if (!file->local_->is_downloading_active_ && download_started_file_ids_.count(file_id)) { + // also includes all 5xx and 429 errors + auto error = Status::Error(400, "Bad Request: wrong file_id or the file is temporarily unavailable"); + if (logging_out_) { + error = Status::Error(LOGGING_OUT_ERROR_CODE, LOGGING_OUT_ERROR_DESCRIPTION); + } + if (closing_) { + error = Status::Error(CLOSING_ERROR_CODE, CLOSING_ERROR_DESCRIPTION); + } + return on_file_download(file_id, std::move(error)); + } +} + +void Client::on_update_authorization_state() { + CHECK(authorization_state_ != nullptr); + switch (authorization_state_->get_id()) { + case td_api::authorizationStateWaitTdlibParameters::ID: { + send_request( + make_object("ignore_inline_thumbnails", make_object(true)), + std::make_unique()); + send_request(make_object("reuse_uploaded_photos_by_hash", + make_object(true)), + std::make_unique()); + send_request(make_object("disable_persistent_network_statistics", + make_object(true)), + std::make_unique()); + send_request(make_object("disable_time_adjustment_protection", + make_object(true)), + std::make_unique()); + + auto parameters = make_object(); + + parameters->use_test_dc_ = is_test_dc_; + parameters->database_directory_ = dir_; + //parameters->use_file_database_ = false; + //parameters->use_chat_info_database_ = false; + //parameters->use_secret_chats_ = false; + parameters->use_message_database_ = USE_MESSAGE_DATABASE; + parameters->api_id_ = parameters_->api_id_; + parameters->api_hash_ = parameters_->api_hash_; + parameters->system_language_code_ = "en"; + parameters->device_model_ = "server"; + parameters->application_version_ = "5.0"; + parameters->enable_storage_optimizer_ = true; + parameters->ignore_file_names_ = true; + + return send_request(make_object(std::move(parameters)), + std::make_unique(this)); + } + case td_api::authorizationStateWaitEncryptionKey::ID: + return send_request(make_object(), std::make_unique(this)); + case td_api::authorizationStateWaitPhoneNumber::ID: + return send_request(make_object(bot_token_), + std::make_unique(this)); + case td_api::authorizationStateReady::ID: { + auto user_info = get_user_info(my_id_); + if (my_id_ <= 0 || user_info == nullptr) { + return send_request(make_object(), std::make_unique(this)); + } + + if (!was_authorized_) { + LOG(WARNING) << "Logged in as @" << user_info->username; + was_authorized_ = true; + update_shared_unix_time_difference(); + if (!pending_updates_.empty()) { + LOG(INFO) << "Process " << pending_updates_.size() << " pending updates"; + for (auto &update : pending_updates_) { + on_update(std::move(update)); + } + td::reset_to_empty(pending_updates_); + } + } + return loop(); + } + case td_api::authorizationStateLoggingOut::ID: + if (!logging_out_) { + LOG(WARNING) << "Logging out"; + logging_out_ = true; + } + break; + case td_api::authorizationStateClosing::ID: + if (!closing_) { + LOG(WARNING) << "Closing"; + closing_ = true; + } + break; + case td_api::authorizationStateClosed::ID: + return on_closed(); + default: + return log_out(); // just in case + } +} + +bool Client::allow_update_before_authorization(const td_api::Object *update) const { + auto update_id = update->get_id(); + if (update_id == td_api::updateAuthorizationState::ID) { + return true; + } + if (update_id == td_api::updateOption::ID) { + const auto &name = static_cast(update)->name_; + return name == "my_id" || name == "unix_time"; + } + if (update_id == td_api::updateUser::ID) { + return static_cast(update)->user_->id_ == my_id_; + } + return false; +} + +void Client::update_shared_unix_time_difference() { + CHECK(was_authorized_); + LOG_IF(ERROR, local_unix_time_difference_ == 0) << "Unix time difference was not updated"; + auto data = parameters_->shared_data_.get(); + if (local_unix_time_difference_ > data->unix_time_difference_) { + data->unix_time_difference_ = local_unix_time_difference_; + } +} + +void Client::on_update(object_ptr result) { + if (!was_authorized_ && !allow_update_before_authorization(result.get())) { + pending_updates_.push_back(std::move(result)); + return; + } + switch (result->get_id()) { + case td_api::updateAuthorizationState::ID: { + auto update = move_object_as(result); + authorization_state_ = std::move(update->authorization_state_); + on_update_authorization_state(); + break; + } + case td_api::updateNewMessage::ID: { + auto update = move_object_as(result); + add_new_message(std::move(update->message_), false); + break; + } + case td_api::updateMessageSendSucceeded::ID: { + auto update = move_object_as(result); + on_message_send_succeeded(std::move(update->message_), update->old_message_id_); + break; + } + case td_api::updateMessageSendFailed::ID: { + auto update = move_object_as(result); + on_message_send_failed(update->message_->chat_id_, update->old_message_id_, update->message_->id_, + Status::Error(update->error_code_, update->error_message_)); + break; + } + case td_api::updateMessageContent::ID: { + auto update = move_object_as(result); + update_message_content(update->chat_id_, update->message_id_, std::move(update->new_content_)); + break; + } + case td_api::updateMessageEdited::ID: { + auto update = move_object_as(result); + auto chat_id = update->chat_id_; + auto message_id = update->message_id_; + on_update_message_edited(chat_id, message_id, update->edit_date_, std::move(update->reply_markup_)); + send_request(make_object(chat_id, message_id), + std::make_unique(this)); + break; + } + case td_api::updateDeleteMessages::ID: { + auto update = move_object_as(result); + for (auto message_id : update->message_ids_) { + delete_message(update->chat_id_, message_id, update->from_cache_); + } + break; + } + case td_api::updateFile::ID: { + auto update = move_object_as(result); + on_update_file(std::move(update->file_)); + break; + } + case td_api::updateFileGenerationStart::ID: { + auto update = move_object_as(result); + auto generation_id = update->generation_id_; + send_request( + make_object(generation_id, make_object(400, "Wrong file_id")), + std::make_unique()); + break; + } + case td_api::updateNewChat::ID: { + auto update = move_object_as(result); + auto chat = std::move(update->chat_); + auto chat_info = add_chat(chat->id_); + bool need_warning = false; + switch (chat->type_->get_id()) { + case td_api::chatTypePrivate::ID: { + auto info = move_object_as(chat->type_); + chat_info->type = ChatInfo::Type::Private; + auto user_id = info->user_id_; + chat_info->user_id = user_id; + need_warning = get_user_info(user_id) == nullptr; + break; + } + case td_api::chatTypeBasicGroup::ID: { + auto info = move_object_as(chat->type_); + chat_info->type = ChatInfo::Type::Group; + auto group_id = info->basic_group_id_; + chat_info->group_id = group_id; + need_warning = get_group_info(group_id) == nullptr; + break; + } + case td_api::chatTypeSupergroup::ID: { + auto info = move_object_as(chat->type_); + chat_info->type = ChatInfo::Type::Supergroup; + auto supergroup_id = info->supergroup_id_; + chat_info->supergroup_id = supergroup_id; + need_warning = get_supergroup_info(supergroup_id) == nullptr; + break; + } + case td_api::chatTypeSecret::ID: { + // unsupported + break; + } + default: + UNREACHABLE(); + } + if (need_warning) { + LOG(ERROR) << "Received updateNewChat about chat " << chat->id_ << ", but hadn't received corresponding info"; + } + + chat_info->title = std::move(chat->title_); + chat_info->photo = std::move(chat->photo_); + chat_info->permissions = std::move(chat->permissions_); + break; + } + case td_api::updateChatTitle::ID: { + auto update = move_object_as(result); + auto chat_info = add_chat(update->chat_id_); + CHECK(chat_info->type != ChatInfo::Type::Unknown); + chat_info->title = std::move(update->title_); + break; + } + case td_api::updateChatPhoto::ID: { + auto update = move_object_as(result); + auto chat_info = add_chat(update->chat_id_); + CHECK(chat_info->type != ChatInfo::Type::Unknown); + chat_info->photo = std::move(update->photo_); + break; + } + case td_api::updateChatPermissions::ID: { + auto update = move_object_as(result); + auto chat_info = add_chat(update->chat_id_); + CHECK(chat_info->type != ChatInfo::Type::Unknown); + chat_info->permissions = std::move(update->permissions_); + break; + } + case td_api::updateUser::ID: { + auto update = move_object_as(result); + add_user(users_, std::move(update->user_)); + break; + } + case td_api::updateUserFullInfo::ID: { + auto update = move_object_as(result); + auto user_id = update->user_id_; + set_user_bio(user_id, std::move(update->user_full_info_->bio_)); + break; + } + case td_api::updateBasicGroup::ID: { + auto update = move_object_as(result); + add_group(groups_, std::move(update->basic_group_)); + break; + } + case td_api::updateBasicGroupFullInfo::ID: { + auto update = move_object_as(result); + auto group_id = update->basic_group_id_; + set_group_description(group_id, std::move(update->basic_group_full_info_->description_)); + set_group_invite_link(group_id, std::move(update->basic_group_full_info_->invite_link_)); + break; + } + case td_api::updateSupergroup::ID: { + auto update = move_object_as(result); + add_supergroup(supergroups_, std::move(update->supergroup_)); + break; + } + case td_api::updateSupergroupFullInfo::ID: { + auto update = move_object_as(result); + auto supergroup_id = update->supergroup_id_; + set_supergroup_description(supergroup_id, std::move(update->supergroup_full_info_->description_)); + set_supergroup_invite_link(supergroup_id, std::move(update->supergroup_full_info_->invite_link_)); + set_supergroup_sticker_set_id(supergroup_id, update->supergroup_full_info_->sticker_set_id_); + set_supergroup_can_set_sticker_set(supergroup_id, update->supergroup_full_info_->can_set_sticker_set_); + set_supergroup_slow_mode_delay(supergroup_id, update->supergroup_full_info_->slow_mode_delay_); + set_supergroup_linked_chat_id(supergroup_id, update->supergroup_full_info_->linked_chat_id_); + set_supergroup_location(supergroup_id, std::move(update->supergroup_full_info_->location_)); + break; + } + case td_api::updateOption::ID: { + auto update = move_object_as(result); + auto name = update->name_; + if (name == "my_id") { + if (update->value_->get_id() == td_api::optionValueEmpty::ID) { + CHECK(logging_out_); + my_id_ = -1; + } else { + CHECK(update->value_->get_id() == td_api::optionValueInteger::ID); + my_id_ = static_cast(move_object_as(update->value_)->value_); + } + } + if (name == "group_anonymous_bot_user_id" && update->value_->get_id() == td_api::optionValueInteger::ID) { + group_anonymous_bot_user_id_ = + static_cast(move_object_as(update->value_)->value_); + } + if (name == "telegram_service_notifications_chat_id" && + update->value_->get_id() == td_api::optionValueInteger::ID) { + service_notifications_user_id_ = + static_cast(move_object_as(update->value_)->value_); + } + if (name == "authorization_date") { + if (update->value_->get_id() == td_api::optionValueEmpty::ID) { + authorization_date_ = -1; + } else { + CHECK(update->value_->get_id() == td_api::optionValueInteger::ID); + authorization_date_ = static_cast(move_object_as(update->value_)->value_); + } + } + if (name == "xallowed_update_types") { + if (update->value_->get_id() == td_api::optionValueEmpty::ID) { + allowed_update_types_ = DEFAULT_ALLOWED_UPDATE_TYPES; + } else { + CHECK(update->value_->get_id() == td_api::optionValueInteger::ID); + allowed_update_types_ = + static_cast(move_object_as(update->value_)->value_); + } + } + if (name == "unix_time" && update->value_->get_id() != td_api::optionValueEmpty::ID) { + CHECK(update->value_->get_id() == td_api::optionValueInteger::ID); + local_unix_time_difference_ = + static_cast(move_object_as(update->value_)->value_) - td::Time::now(); + if (was_authorized_) { + update_shared_unix_time_difference(); + } + } + break; + } + case td_api::updatePoll::ID: + add_update_poll(move_object_as(result)); + break; + case td_api::updatePollAnswer::ID: + add_update_poll_answer(move_object_as(result)); + break; + case td_api::updateNewInlineQuery::ID: { + auto update = move_object_as(result); + add_new_inline_query(update->id_, update->sender_user_id_, std::move(update->user_location_), update->query_, + update->offset_); + break; + } + case td_api::updateNewChosenInlineResult::ID: { + auto update = move_object_as(result); + add_new_chosen_inline_result(update->sender_user_id_, std::move(update->user_location_), update->query_, + update->result_id_, update->inline_message_id_); + break; + } + case td_api::updateNewCallbackQuery::ID: + add_new_callback_query(move_object_as(result)); + break; + case td_api::updateNewInlineCallbackQuery::ID: + add_new_inline_callback_query(move_object_as(result)); + break; + case td_api::updateNewShippingQuery::ID: + add_new_shipping_query(move_object_as(result)); + break; + case td_api::updateNewPreCheckoutQuery::ID: + add_new_pre_checkout_query(move_object_as(result)); + break; + case td_api::updateNewCustomEvent::ID: + add_new_custom_event(move_object_as(result)); + break; + case td_api::updateNewCustomQuery::ID: + add_new_custom_query(move_object_as(result)); + break; + default: + // we are not interested in this updates + break; + } +} + +void Client::on_result(td::uint64 id, object_ptr result) { + LOG(DEBUG) << "Receive from Td: " << id << " " << to_string(result); + if (id == 0) { + return on_update(std::move(result)); + } + + auto *handler_ptr = handlers_.get(id); + CHECK(handler_ptr != nullptr); + auto handler = std::move(*handler_ptr); + handler->on_result(std::move(result)); + handlers_.erase(id); +} + +void Client::on_closed() { + LOG(WARNING) << "Closed"; + CHECK(logging_out_ || closing_); + CHECK(!td_client_.empty()); + td_client_.reset(); + + int http_status_code = logging_out_ ? LOGGING_OUT_ERROR_CODE : CLOSING_ERROR_CODE; + Slice description = logging_out_ ? LOGGING_OUT_ERROR_DESCRIPTION : CLOSING_ERROR_DESCRIPTION; + if (webhook_set_query_) { + fail_query(http_status_code, description, std::move(webhook_set_query_)); + } + if (!webhook_url_.empty()) { + webhook_id_.reset(); + } + if (long_poll_query_) { + long_poll_wakeup(true); + CHECK(!long_poll_query_); + } + + while (!cmd_queue_.empty()) { + auto query = std::move(cmd_queue_.front()); + cmd_queue_.pop(); + fail_query(http_status_code, description, std::move(query)); + } + + while (!yet_unsent_messages_.empty()) { + auto it = yet_unsent_messages_.begin(); + auto chat_id = it->first.chat_id; + auto message_id = it->first.message_id; + if (!USE_MESSAGE_DATABASE) { + LOG(ERROR) << "Doesn't receive updateMessageSendFailed for message " << message_id << " in chat " << chat_id; + } + on_message_send_failed(chat_id, message_id, 0, Status::Error(http_status_code, description)); + } + + while (!pending_bot_resolve_queries_.empty()) { + auto it = pending_bot_resolve_queries_.begin(); + fail_query(http_status_code, description, std::move(it->second.query)); + pending_bot_resolve_queries_.erase(it); + } + + while (!file_download_listeners_.empty()) { + auto it = file_download_listeners_.begin(); + auto file_id = it->first; + LOG(ERROR) << "Doesn't receive updateFile for file " << file_id; + on_file_download(file_id, Status::Error(http_status_code, description)); + } + + if (logging_out_) { + parameters_->shared_data_->webhook_db_->erase(bot_token_with_dc_); + + class RmWorker : public td::Actor { + public: + RmWorker(td::string dir, td::ActorId parent) : dir_(std::move(dir)), parent_(std::move(parent)) { + } + + private: + td::string dir_; + td::ActorId parent_; + + void start_up() override { + CHECK(!dir_.empty()); + CHECK(dir_[0] == '.'); + td::rmrf(dir_).ignore(); + stop(); + } + void tear_down() override { + send_closure(parent_, &Client::finish_closing); + } + }; + // NB: the same scheduler as for database in Td + auto current_scheduler_id = td::Scheduler::instance()->sched_id(); + auto scheduler_count = td::Scheduler::instance()->sched_count(); + auto scheduler_id = td::min(current_scheduler_id + 1, scheduler_count - 1); + td::create_actor_on_scheduler("RmWorker", scheduler_id, dir_, actor_id(this)).release(); + return; + } + + finish_closing(); +} + +void Client::finish_closing() { + if (clear_tqueue_ && logging_out_) { + clear_tqueue(); + } + + set_timeout_in(need_close_ ? 0 : 600); +} + +void Client::timeout_expired() { + LOG(WARNING) << "Stop client"; + stop(); +} + +void Client::clear_tqueue() { + CHECK(webhook_id_.empty()); + auto &tqueue = parameters_->shared_data_->tqueue_; + auto size = tqueue->get_size(tqueue_id_); + if (size > 0) { + LOG(INFO) << "Removing " << size << " tqueue events"; + td::MutableSpan span; + auto r_size = tqueue->get(tqueue_id_, tqueue->get_tail(tqueue_id_), true, 0, span); + CHECK(r_size.is_ok()); + CHECK(r_size.ok() == 0); + CHECK(tqueue->get_size(tqueue_id_) == 0); + } +} + +bool Client::to_bool(td::MutableSlice value) { + td::to_lower_inplace(value); + value = td::trim(value); + return value == "true" || value == "yes" || value == "1"; +} + +td::Result> Client::get_keyboard_button(JsonValue &button) { + if (button.type() == JsonValue::Type::Object) { + auto &object = button.get_object(); + + TRY_RESULT(text, get_json_object_string_field(object, "text", false)); + + TRY_RESULT(request_phone_number, get_json_object_bool_field(object, "request_phone_number")); + TRY_RESULT(request_contact, get_json_object_bool_field(object, "request_contact")); + if (request_phone_number || request_contact) { + return make_object(text, make_object()); + } + + TRY_RESULT(request_location, get_json_object_bool_field(object, "request_location")); + if (request_location) { + return make_object(text, make_object()); + } + + if (has_json_object_field(object, "request_poll")) { + bool force_regular = false; + bool force_quiz = false; + TRY_RESULT(request_poll, get_json_object_field(object, "request_poll", JsonValue::Type::Object, false)); + auto &request_poll_object = request_poll.get_object(); + if (has_json_object_field(request_poll_object, "type")) { + TRY_RESULT(type, get_json_object_string_field(request_poll_object, "type")); + if (type == "quiz") { + force_quiz = true; + } else if (type == "regular") { + force_regular = true; + } + } + return make_object( + text, make_object(force_regular, force_quiz)); + } + + return make_object(text, nullptr); + } + if (button.type() == JsonValue::Type::String) { + return make_object(button.get_string().str(), nullptr); + } + + return Status::Error(400, "KeyboardButton must be a String or an Object"); +} + +td::Result> Client::get_inline_keyboard_button(JsonValue &button) { + if (button.type() != JsonValue::Type::Object) { + return Status::Error(400, "InlineKeyboardButton must be an Object"); + } + + auto &object = button.get_object(); + + TRY_RESULT(text, get_json_object_string_field(object, "text", false)); + { + TRY_RESULT(url, get_json_object_string_field(object, "url")); + if (!url.empty()) { + return make_object(text, make_object(url)); + } + } + + { + TRY_RESULT(callback_data, get_json_object_string_field(object, "callback_data")); + if (!callback_data.empty()) { + return make_object( + text, make_object(callback_data)); + } + } + + if (has_json_object_field(object, "callback_game")) { + return make_object(text, make_object()); + } + + if (has_json_object_field(object, "pay")) { + return make_object(text, make_object()); + } + + if (has_json_object_field(object, "switch_inline_query")) { + TRY_RESULT(switch_inline_query, get_json_object_string_field(object, "switch_inline_query", false)); + return make_object( + text, make_object(switch_inline_query, false)); + } + + if (has_json_object_field(object, "switch_inline_query_current_chat")) { + TRY_RESULT(switch_inline_query, get_json_object_string_field(object, "switch_inline_query_current_chat", false)); + return make_object( + text, make_object(switch_inline_query, true)); + } + + if (has_json_object_field(object, "login_url")) { + TRY_RESULT(login_url, get_json_object_field(object, "login_url", JsonValue::Type::Object, false)); + CHECK(login_url.type() == JsonValue::Type::Object); + auto &login_url_object = login_url.get_object(); + TRY_RESULT(url, get_json_object_string_field(login_url_object, "url", false)); + TRY_RESULT(bot_username, get_json_object_string_field(login_url_object, "bot_username")); + TRY_RESULT(request_write_access, get_json_object_bool_field(login_url_object, "request_write_access")); + TRY_RESULT(forward_text, get_json_object_string_field(login_url_object, "forward_text")); + + int32 bot_user_id = 0; + if (bot_username.empty()) { + bot_user_id = my_id_; + } else { + if (bot_username[0] == '@') { + bot_username = bot_username.substr(1); + } + if (bot_username.empty()) { + return Status::Error(400, "LoginUrl bot username is invalid"); + } + for (auto c : bot_username) { + if (c != '_' && !td::is_alnum(c)) { + return Status::Error(400, "LoginUrl bot username is invalid"); + } + } + if (cur_temp_bot_user_id_ >= 100000) { + return Status::Error(400, "Too much different LoginUrl bot usernames"); + } + auto &user_id = bot_user_ids_[bot_username]; + if (user_id == 0) { + user_id = cur_temp_bot_user_id_++; + user_id *= 1000; + } + if (user_id % 1000 == 0) { + unresolved_bot_usernames_.insert(bot_username); + } + bot_user_id = user_id; + } + if (!request_write_access) { + bot_user_id *= -1; + } + return make_object( + text, make_object(url, bot_user_id, forward_text)); + } + + return Status::Error(400, "Text buttons are unallowed in the inline keyboard"); +} + +td::Result> Client::get_reply_markup(const Query *query) { + auto reply_markup = query->arg("reply_markup"); + if (reply_markup.empty()) { + return nullptr; + } + + LOG(INFO) << "Parsing JSON object: " << reply_markup; + auto r_value = json_decode(reply_markup); + if (r_value.is_error()) { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + return Status::Error(400, "Can't parse reply keyboard markup JSON object"); + } + + return get_reply_markup(r_value.move_as_ok()); +} + +td::Result> Client::get_reply_markup(JsonValue &&value) { + td::vector>> rows; + td::vector>> inline_rows; + bool resize = false; + bool one_time = false; + bool remove = false; + bool is_personal = false; + bool force_reply = false; + + if (value.type() != JsonValue::Type::Object) { + return Status::Error(400, "Object expected as reply markup"); + } + for (auto &field_value : value.get_object()) { + if (field_value.first == "keyboard") { + auto keyboard = std::move(field_value.second); + if (keyboard.type() != JsonValue::Type::Array) { + return Status::Error(400, "Field \"keyboard\" of the ReplyKeyboardMarkup must be an Array"); + } + for (auto &row : keyboard.get_array()) { + td::vector> new_row; + if (row.type() != JsonValue::Type::Array) { + return Status::Error(400, "Field \"keyboard\" of the ReplyKeyboardMarkup must be an Array of Arrays"); + } + for (auto &button : row.get_array()) { + auto r_button = get_keyboard_button(button); + if (r_button.is_error()) { + return Status::Error(400, PSLICE() << "Can't parse keyboard button: " << r_button.error().message()); + } + new_row.push_back(r_button.move_as_ok()); + } + + rows.push_back(std::move(new_row)); + } + } else if (field_value.first == "inline_keyboard") { + auto inline_keyboard = std::move(field_value.second); + if (inline_keyboard.type() != JsonValue::Type::Array) { + return Status::Error(400, "Field \"inline_keyboard\" of the InlineKeyboardMarkup must be an Array"); + } + for (auto &inline_row : inline_keyboard.get_array()) { + td::vector> new_inline_row; + if (inline_row.type() != JsonValue::Type::Array) { + return Status::Error(400, "Field \"inline_keyboard\" of the InlineKeyboardMarkup must be an Array of Arrays"); + } + for (auto &button : inline_row.get_array()) { + auto r_button = get_inline_keyboard_button(button); + if (r_button.is_error()) { + return Status::Error(400, PSLICE() << "Can't parse inline keyboard button: " << r_button.error().message()); + } + new_inline_row.push_back(r_button.move_as_ok()); + } + + inline_rows.push_back(std::move(new_inline_row)); + } + } else if (field_value.first == "resize_keyboard") { + if (field_value.second.type() != JsonValue::Type::Boolean) { + return Status::Error(400, "Field \"resize_keyboard\" of the ReplyKeyboardMarkup must be of the type Boolean"); + } + resize = field_value.second.get_boolean(); + } else if (field_value.first == "one_time_keyboard") { + if (field_value.second.type() != JsonValue::Type::Boolean) { + return Status::Error(400, "Field \"one_time_keyboard\" of the ReplyKeyboardMarkup must be of the type Boolean"); + } + one_time = field_value.second.get_boolean(); + } else if (field_value.first == "hide_keyboard" || field_value.first == "remove_keyboard") { + if (field_value.second.type() != JsonValue::Type::Boolean) { + return Status::Error(400, "Field \"remove_keyboard\" of the ReplyKeyboardRemove must be of the type Boolean"); + } + remove = field_value.second.get_boolean(); + } else if (field_value.first == "personal_keyboard" || field_value.first == "selective") { + if (field_value.second.type() != JsonValue::Type::Boolean) { + return Status::Error(400, "Field \"selective\" of the reply markup must be of the type Boolean"); + } + is_personal = field_value.second.get_boolean(); + } else if (field_value.first == "force_reply_keyboard" || field_value.first == "force_reply") { + if (field_value.second.type() != JsonValue::Type::Boolean) { + return Status::Error(400, "Field \"force_reply\" of the reply markup must be of the type Boolean"); + } + force_reply = field_value.second.get_boolean(); + } + } + + object_ptr result; + if (!rows.empty()) { + result = make_object(std::move(rows), resize, one_time, is_personal); + } else if (!inline_rows.empty()) { + result = make_object(std::move(inline_rows)); + } else if (remove) { + result = make_object(is_personal); + } else if (force_reply) { + result = make_object(is_personal); + } + if (result == nullptr || result->get_id() != td_api::replyMarkupInlineKeyboard::ID) { + unresolved_bot_usernames_.clear(); + } + + return std::move(result); +} + +td::Result> Client::get_labeled_price_part(JsonValue &value) { + if (value.type() != JsonValue::Type::Object) { + return Status::Error(400, "LabeledPrice must be an Object"); + } + + auto &object = value.get_object(); + + TRY_RESULT(label, get_json_object_string_field(object, "label", false)); + if (label.empty()) { + return Status::Error(400, "LabeledPrice label must be non-empty"); + } + + TRY_RESULT(amount, get_json_object_field(object, "amount", JsonValue::Type::Null, false)); + Slice number; + if (amount.type() == JsonValue::Type::Number) { + number = amount.get_number(); + } else if (amount.type() == JsonValue::Type::String) { + number = amount.get_string(); + } else { + return Status::Error(400, "Field \"amount\" must be of type Number or String"); + } + auto parsed_amount = td::to_integer_safe(number); + if (parsed_amount.is_error()) { + return Status::Error(400, "Can't parse \"amount\" as Number"); + } + return make_object(label, parsed_amount.ok()); +} + +td::Result>> Client::get_labeled_price_parts(JsonValue &value) { + if (value.type() != JsonValue::Type::Array) { + return Status::Error(400, "Expected an Array of labeled prices"); + } + + td::vector> prices; + for (auto &price : value.get_array()) { + auto r_labeled_price = get_labeled_price_part(price); + if (r_labeled_price.is_error()) { + return Status::Error(400, PSLICE() << "Can't parse labeled price: " << r_labeled_price.error().message()); + } + prices.push_back(r_labeled_price.move_as_ok()); + } + if (prices.empty()) { + return Status::Error(400, "There must be at least one price"); + } + + return std::move(prices); +} + +td::Result> Client::get_shipping_option(JsonValue &option) { + if (option.type() != JsonValue::Type::Object) { + return Status::Error(400, "ShippingOption must be an Object"); + } + + auto &object = option.get_object(); + + TRY_RESULT(id, get_json_object_string_field(object, "id", false)); + if (id.empty()) { + return Status::Error(400, "ShippingOption identifier must be non-empty"); + } + + TRY_RESULT(title, get_json_object_string_field(object, "title", false)); + if (title.empty()) { + return Status::Error(400, "ShippingOption title must be non-empty"); + } + + TRY_RESULT(prices_json, get_json_object_field(object, "prices", JsonValue::Type::Array, false)); + + auto r_prices = get_labeled_price_parts(prices_json); + if (r_prices.is_error()) { + return Status::Error(400, PSLICE() << "Can't parse shipping option prices: " << r_prices.error().message()); + } + + return make_object(id, title, r_prices.move_as_ok()); +} + +td::Result>> Client::get_shipping_options(const Query *query) { + TRY_RESULT(shipping_options, get_required_string_arg(query, "shipping_options")); + + LOG(INFO) << "Parsing JSON object: " << shipping_options; + auto r_value = json_decode(shipping_options); + if (r_value.is_error()) { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + return Status::Error(400, "Can't parse shipping options JSON object"); + } + + return get_shipping_options(r_value.move_as_ok()); +} + +td::Result>> Client::get_shipping_options(JsonValue &&value) { + if (value.type() != JsonValue::Type::Array) { + return Status::Error(400, "Expected an Array of shipping options"); + } + + td::vector> options; + for (auto &option : value.get_array()) { + auto r_shipping_option = get_shipping_option(option); + if (r_shipping_option.is_error()) { + return Status::Error(400, PSLICE() << "Can't parse shipping option: " << r_shipping_option.error().message()); + } + options.push_back(r_shipping_option.move_as_ok()); + } + if (options.empty()) { + return Status::Error(400, "There must be at least one shipping option"); + } + + return std::move(options); +} + +td_api::object_ptr Client::get_chat_action(const Query *query) { + auto action = query->arg("action"); + td::to_lower_inplace(action); + if (action == "cancel") { + return make_object(); + } + if (action == "typing") { + return make_object(); + } + if (action == "record_video") { + return make_object(); + } + if (action == "upload_video") { + return make_object(); + } + if (action == "record_audio" || action == "record_voice") { + return make_object(); + } + if (action == "upload_audio" || action == "upload_voice") { + return make_object(); + } + if (action == "upload_photo") { + return make_object(); + } + if (action == "upload_document") { + return make_object(); + } + if (action == "pick_up_location" || action == "find_location") { + return make_object(); + } + if (action == "record_video_note") { + return make_object(); + } + if (action == "upload_video_note") { + return make_object(); + } + return nullptr; +} + +td_api::object_ptr Client::get_input_file(const Query *query, Slice field_name, + bool force_file) const { + return get_input_file(query, field_name, query->arg(field_name), force_file); +} + +td::string Client::get_local_file_path(Slice file_uri) { + if (td::begins_with(file_uri, "/")) { + file_uri.remove_prefix(td::begins_with(file_uri, "/localhost") ? 10 : 1); + } +#if TD_PORT_WINDOWS + if (td::begins_with(file_uri, "/")) { + file_uri.remove_prefix(1); + } +#endif + td::string result(file_uri.size(), '\0'); + auto result_len = url_decode(file_uri, result, false); + result.resize(result_len); + return result; +} + +td_api::object_ptr Client::get_input_file(const Query *query, Slice field_name, Slice file_id, + bool force_file) const { + if (!file_id.empty()) { + if (parameters_->local_mode_) { + Slice file_protocol{"file:/"}; + if (td::begins_with(file_id, file_protocol)) { + return make_object(get_local_file_path(file_id.substr(file_protocol.size()))); + } + } + Slice attach_protocol{"attach://"}; + if (td::begins_with(file_id, attach_protocol)) { + field_name = file_id.substr(attach_protocol.size()); + } else { + if (!force_file) { + return make_object(file_id.str()); + } + } + } + auto file = query->file(field_name); + if (file != nullptr) { + return make_object(file->temp_file_name); + } + + return nullptr; +} + +td_api::object_ptr Client::get_input_thumbnail(const Query *query, Slice field_name) const { + auto input_file = get_input_file(query, field_name, true); + if (input_file == nullptr) { + return nullptr; + } + return make_object(std::move(input_file), 0, 0); +} + +td::Result> Client::get_input_message_content( + JsonValue &input_message_content, bool is_input_message_content_required) { + CHECK(input_message_content.type() == JsonValue::Type::Object); + auto &object = input_message_content.get_object(); + + TRY_RESULT(message_text, get_json_object_string_field(object, "message_text")); + + if (!message_text.empty()) { + TRY_RESULT(disable_web_page_preview, get_json_object_bool_field(object, "disable_web_page_preview")); + TRY_RESULT(parse_mode, get_json_object_string_field(object, "parse_mode")); + auto entities = get_json_object_field_force(object, "entities"); + TRY_RESULT(input_message_text, get_input_message_text(std::move(message_text), disable_web_page_preview, + std::move(parse_mode), std::move(entities))); + return std::move(input_message_text); + } + + if (has_json_object_field(object, "latitude") && has_json_object_field(object, "longitude")) { + TRY_RESULT(latitude, get_json_object_double_field(object, "latitude", false)); + TRY_RESULT(longitude, get_json_object_double_field(object, "longitude", false)); + TRY_RESULT(horizontal_accuracy, get_json_object_double_field(object, "horizontal_accuracy")); + TRY_RESULT(live_period, get_json_object_int_field(object, "live_period")); + TRY_RESULT(heading, get_json_object_int_field(object, "heading")); + TRY_RESULT(proximity_alert_radius, get_json_object_int_field(object, "proximity_alert_radius")); + auto location = make_object(latitude, longitude, horizontal_accuracy); + + if (has_json_object_field(object, "title") && has_json_object_field(object, "address")) { + TRY_RESULT(title, get_json_object_string_field(object, "title", false)); + TRY_RESULT(address, get_json_object_string_field(object, "address", false)); + td::string provider; + td::string venue_id; + td::string venue_type; + + TRY_RESULT(google_place_id, get_json_object_string_field(object, "google_place_id")); + TRY_RESULT(google_place_type, get_json_object_string_field(object, "google_place_type")); + if (!google_place_id.empty() || !google_place_type.empty()) { + provider = "gplaces"; + venue_id = std::move(google_place_id); + venue_type = std::move(google_place_type); + } + TRY_RESULT(foursquare_id, get_json_object_string_field(object, "foursquare_id")); + TRY_RESULT(foursquare_type, get_json_object_string_field(object, "foursquare_type")); + if (!foursquare_id.empty() || !foursquare_type.empty()) { + provider = "foursquare"; + venue_id = std::move(foursquare_id); + venue_type = std::move(foursquare_type); + } + + return make_object( + make_object(std::move(location), title, address, provider, venue_id, venue_type)); + } + + return make_object(std::move(location), live_period, heading, proximity_alert_radius); + } + + if (has_json_object_field(object, "phone_number")) { + TRY_RESULT(phone_number, get_json_object_string_field(object, "phone_number", false)); + TRY_RESULT(first_name, get_json_object_string_field(object, "first_name", false)); + TRY_RESULT(last_name, get_json_object_string_field(object, "last_name")); + TRY_RESULT(vcard, get_json_object_string_field(object, "vcard")); + + return make_object( + make_object(phone_number, first_name, last_name, vcard, 0)); + } + + if (is_input_message_content_required) { + return Status::Error(400, "Input message content is not specified"); + } + return nullptr; +} + +td_api::object_ptr Client::get_message_send_options(bool disable_notification) { + return make_object(disable_notification, false, nullptr); +} + +td::Result>> Client::get_inline_query_results( + const Query *query) { + auto results_encoded = query->arg("results"); + if (results_encoded.empty()) { + return td::vector>(); + } + + LOG(INFO) << "Parsing JSON object: " << results_encoded; + auto r_values = json_decode(results_encoded); + if (r_values.is_error()) { + return Status::Error(400, + PSLICE() << "Can't parse JSON encoded inline query results: " << r_values.error().message()); + } + + return get_inline_query_results(r_values.move_as_ok()); +} + +td::Result>> Client::get_inline_query_results( + td::JsonValue &&values) { + if (values.type() == JsonValue::Type::Null) { + return td::vector>(); + } + if (values.type() != JsonValue::Type::Array) { + return Status::Error(400, "Expected an Array of inline query results"); + } + + td::vector> inline_query_results; + for (auto &value : values.get_array()) { + auto r_inline_query_result = get_inline_query_result(std::move(value)); + if (r_inline_query_result.is_error()) { + return Status::Error(400, + PSLICE() << "Can't parse inline query result: " << r_inline_query_result.error().message()); + } + inline_query_results.push_back(r_inline_query_result.move_as_ok()); + } + + return std::move(inline_query_results); +} + +td::Result> Client::get_inline_query_result(td::JsonValue &&value) { + if (value.type() != JsonValue::Type::Object) { + return Status::Error(400, "Inline query result must be an object"); + } + + auto &object = value.get_object(); + + TRY_RESULT(type, get_json_object_string_field(object, "type", false)); + td::to_lower_inplace(type); + + TRY_RESULT(id, get_json_object_string_field(object, "id", false)); + + bool is_input_message_content_required = (type == "article"); + object_ptr input_message_content; + + TRY_RESULT(input_message_content_obj, + get_json_object_field(object, "input_message_content", JsonValue::Type::Object)); + if (input_message_content_obj.type() == JsonValue::Type::Null) { + TRY_RESULT(message_text, get_json_object_string_field(object, "message_text", !is_input_message_content_required)); + TRY_RESULT(disable_web_page_preview, get_json_object_bool_field(object, "disable_web_page_preview")); + TRY_RESULT(parse_mode, get_json_object_string_field(object, "parse_mode")); + auto entities = get_json_object_field_force(object, "entities"); + + if (is_input_message_content_required || !message_text.empty()) { + TRY_RESULT(input_message_text, get_input_message_text(std::move(message_text), disable_web_page_preview, + std::move(parse_mode), std::move(entities))); + input_message_content = std::move(input_message_text); + } + } else { + TRY_RESULT(input_message_content_result, + get_input_message_content(input_message_content_obj, is_input_message_content_required)); + input_message_content = std::move(input_message_content_result); + } + TRY_RESULT(input_caption, get_json_object_string_field(object, "caption")); + TRY_RESULT(parse_mode, get_json_object_string_field(object, "parse_mode")); + auto entities = get_json_object_field_force(object, "caption_entities"); + TRY_RESULT(caption, get_formatted_text(std::move(input_caption), std::move(parse_mode), std::move(entities))); + + TRY_RESULT(reply_markup_object, get_json_object_field(object, "reply_markup", JsonValue::Type::Object)); + object_ptr reply_markup; + if (reply_markup_object.type() != JsonValue::Type::Null) { + TRY_RESULT_ASSIGN(reply_markup, get_reply_markup(std::move(reply_markup_object))); + } + + object_ptr result; + if (type == "article") { + TRY_RESULT(url, get_json_object_string_field(object, "url")); + TRY_RESULT(hide_url, get_json_object_bool_field(object, "hide_url")); + TRY_RESULT(title, get_json_object_string_field(object, "title", false)); + TRY_RESULT(description, get_json_object_string_field(object, "description")); + TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url")); + TRY_RESULT(thumbnail_width, get_json_object_int_field(object, "thumb_width")); + TRY_RESULT(thumbnail_height, get_json_object_int_field(object, "thumb_height")); + + CHECK(input_message_content != nullptr); + return make_object( + id, url, hide_url, title, description, thumbnail_url, thumbnail_width, thumbnail_height, + std::move(reply_markup), std::move(input_message_content)); + } + if (type == "audio") { + TRY_RESULT(audio_url, get_json_object_string_field(object, "audio_url")); + TRY_RESULT(audio_duration, get_json_object_int_field(object, "audio_duration")); + TRY_RESULT(title, get_json_object_string_field(object, "title", audio_url.empty())); + TRY_RESULT(performer, get_json_object_string_field(object, "performer")); + if (audio_url.empty()) { + TRY_RESULT_ASSIGN(audio_url, get_json_object_string_field(object, "audio_file_id", false)); + } + + if (input_message_content == nullptr) { + input_message_content = make_object(nullptr, nullptr, audio_duration, title, performer, + std::move(caption)); + } + return make_object(id, title, performer, audio_url, audio_duration, + std::move(reply_markup), std::move(input_message_content)); + } + if (type == "contact") { + TRY_RESULT(phone_number, get_json_object_string_field(object, "phone_number", false)); + TRY_RESULT(first_name, get_json_object_string_field(object, "first_name", false)); + TRY_RESULT(last_name, get_json_object_string_field(object, "last_name")); + TRY_RESULT(vcard, get_json_object_string_field(object, "vcard")); + TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url")); + TRY_RESULT(thumbnail_width, get_json_object_int_field(object, "thumb_width")); + TRY_RESULT(thumbnail_height, get_json_object_int_field(object, "thumb_height")); + + if (input_message_content == nullptr) { + input_message_content = make_object( + make_object(phone_number, first_name, last_name, vcard, 0)); + } + return make_object( + id, make_object(phone_number, first_name, last_name, vcard, 0), thumbnail_url, thumbnail_width, + thumbnail_height, std::move(reply_markup), std::move(input_message_content)); + } + if (type == "document") { + TRY_RESULT(title, get_json_object_string_field(object, "title", false)); + TRY_RESULT(description, get_json_object_string_field(object, "description")); + TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url")); + TRY_RESULT(document_url, get_json_object_string_field(object, "document_url")); + TRY_RESULT(mime_type, get_json_object_string_field(object, "mime_type", document_url.empty())); + TRY_RESULT(thumbnail_width, get_json_object_int_field(object, "thumb_width")); + TRY_RESULT(thumbnail_height, get_json_object_int_field(object, "thumb_height")); + if (document_url.empty()) { + TRY_RESULT_ASSIGN(document_url, get_json_object_string_field(object, "document_file_id", false)); + } + + if (input_message_content == nullptr) { + input_message_content = make_object(nullptr, nullptr, false, std::move(caption)); + } + return make_object( + id, title, description, document_url, mime_type, thumbnail_url, thumbnail_width, thumbnail_height, + std::move(reply_markup), std::move(input_message_content)); + } + if (type == "game") { + TRY_RESULT(game_short_name, get_json_object_string_field(object, "game_short_name", false)); + return make_object(id, game_short_name, std::move(reply_markup)); + } + if (type == "gif") { + TRY_RESULT(title, get_json_object_string_field(object, "title")); + TRY_RESULT(gif_url, get_json_object_string_field(object, "gif_url")); + TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url", gif_url.empty())); + TRY_RESULT(thumbnail_mime_type, get_json_object_string_field(object, "thumb_mime_type", true)); + TRY_RESULT(gif_duration, get_json_object_int_field(object, "gif_duration")); + TRY_RESULT(gif_width, get_json_object_int_field(object, "gif_width")); + TRY_RESULT(gif_height, get_json_object_int_field(object, "gif_height")); + if (gif_url.empty()) { + TRY_RESULT_ASSIGN(gif_url, get_json_object_string_field(object, "gif_file_id", false)); + } + + if (input_message_content == nullptr) { + input_message_content = make_object( + nullptr, nullptr, td::vector(), gif_duration, gif_width, gif_height, std::move(caption)); + } + return make_object( + id, title, thumbnail_url, thumbnail_mime_type, gif_url, "image/gif", gif_duration, gif_width, gif_height, + std::move(reply_markup), std::move(input_message_content)); + } + if (type == "location") { + TRY_RESULT(latitude, get_json_object_double_field(object, "latitude", false)); + TRY_RESULT(longitude, get_json_object_double_field(object, "longitude", false)); + TRY_RESULT(horizontal_accuracy, get_json_object_double_field(object, "horizontal_accuracy")); + TRY_RESULT(live_period, get_json_object_int_field(object, "live_period")); + TRY_RESULT(heading, get_json_object_int_field(object, "heading")); + TRY_RESULT(proximity_alert_radius, get_json_object_int_field(object, "proximity_alert_radius")); + TRY_RESULT(title, get_json_object_string_field(object, "title", false)); + TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url")); + TRY_RESULT(thumbnail_width, get_json_object_int_field(object, "thumb_width")); + TRY_RESULT(thumbnail_height, get_json_object_int_field(object, "thumb_height")); + + if (input_message_content == nullptr) { + auto location = make_object(latitude, longitude, horizontal_accuracy); + input_message_content = + make_object(std::move(location), live_period, heading, proximity_alert_radius); + } + return make_object( + id, make_object(latitude, longitude, horizontal_accuracy), live_period, title, thumbnail_url, + thumbnail_width, thumbnail_height, std::move(reply_markup), std::move(input_message_content)); + } + if (type == "mpeg4_gif") { + TRY_RESULT(title, get_json_object_string_field(object, "title")); + TRY_RESULT(mpeg4_url, get_json_object_string_field(object, "mpeg4_url")); + TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url", mpeg4_url.empty())); + TRY_RESULT(thumbnail_mime_type, get_json_object_string_field(object, "thumb_mime_type", true)); + TRY_RESULT(mpeg4_duration, get_json_object_int_field(object, "mpeg4_duration")); + TRY_RESULT(mpeg4_width, get_json_object_int_field(object, "mpeg4_width")); + TRY_RESULT(mpeg4_height, get_json_object_int_field(object, "mpeg4_height")); + if (mpeg4_url.empty()) { + TRY_RESULT_ASSIGN(mpeg4_url, get_json_object_string_field(object, "mpeg4_file_id", false)); + } + + if (input_message_content == nullptr) { + input_message_content = make_object( + nullptr, nullptr, td::vector(), mpeg4_duration, mpeg4_width, mpeg4_height, std::move(caption)); + } + return make_object( + id, title, thumbnail_url, thumbnail_mime_type, mpeg4_url, "video/mp4", mpeg4_duration, mpeg4_width, + mpeg4_height, std::move(reply_markup), std::move(input_message_content)); + } + if (type == "photo") { + TRY_RESULT(title, get_json_object_string_field(object, "title")); + TRY_RESULT(description, get_json_object_string_field(object, "description")); + TRY_RESULT(photo_url, get_json_object_string_field(object, "photo_url")); + TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url", photo_url.empty())); + TRY_RESULT(photo_width, get_json_object_int_field(object, "photo_width")); + TRY_RESULT(photo_height, get_json_object_int_field(object, "photo_height")); + if (photo_url.empty()) { + TRY_RESULT_ASSIGN(photo_url, get_json_object_string_field(object, "photo_file_id", false)); + } + + if (input_message_content == nullptr) { + input_message_content = + make_object(nullptr, nullptr, td::vector(), 0, 0, std::move(caption), 0); + } + return make_object(id, title, description, thumbnail_url, photo_url, + photo_width, photo_height, std::move(reply_markup), + std::move(input_message_content)); + } + if (type == "sticker") { + TRY_RESULT(sticker_file_id, get_json_object_string_field(object, "sticker_file_id", false)); + + if (input_message_content == nullptr) { + input_message_content = make_object(nullptr, nullptr, 0, 0); + } + return make_object(id, "", sticker_file_id, 0, 0, std::move(reply_markup), + std::move(input_message_content)); + } + if (type == "venue") { + TRY_RESULT(latitude, get_json_object_double_field(object, "latitude", false)); + TRY_RESULT(longitude, get_json_object_double_field(object, "longitude", false)); + TRY_RESULT(horizontal_accuracy, get_json_object_double_field(object, "horizontal_accuracy")); + TRY_RESULT(title, get_json_object_string_field(object, "title", false)); + TRY_RESULT(address, get_json_object_string_field(object, "address", false)); + TRY_RESULT(foursquare_id, get_json_object_string_field(object, "foursquare_id")); + TRY_RESULT(foursquare_type, get_json_object_string_field(object, "foursquare_type")); + TRY_RESULT(google_place_id, get_json_object_string_field(object, "google_place_id")); + TRY_RESULT(google_place_type, get_json_object_string_field(object, "google_place_type")); + TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url")); + TRY_RESULT(thumbnail_width, get_json_object_int_field(object, "thumb_width")); + TRY_RESULT(thumbnail_height, get_json_object_int_field(object, "thumb_height")); + + td::string provider; + td::string venue_id; + td::string venue_type; + if (!google_place_id.empty() || !google_place_type.empty()) { + provider = "gplaces"; + venue_id = std::move(google_place_id); + venue_type = std::move(google_place_type); + } + if (!foursquare_id.empty() || !foursquare_type.empty()) { + provider = "foursquare"; + venue_id = std::move(foursquare_id); + venue_type = std::move(foursquare_type); + } + + if (input_message_content == nullptr) { + input_message_content = make_object( + make_object(make_object(latitude, longitude, horizontal_accuracy), title, + address, provider, venue_id, venue_type)); + } + return make_object( + id, + make_object(make_object(latitude, longitude, horizontal_accuracy), title, + address, provider, venue_id, venue_type), + thumbnail_url, thumbnail_width, thumbnail_height, std::move(reply_markup), std::move(input_message_content)); + } + if (type == "video") { + TRY_RESULT(title, get_json_object_string_field(object, "title", false)); + TRY_RESULT(description, get_json_object_string_field(object, "description")); + TRY_RESULT(video_url, get_json_object_string_field(object, "video_url")); + TRY_RESULT(thumbnail_url, get_json_object_string_field(object, "thumb_url", video_url.empty())); + TRY_RESULT(mime_type, get_json_object_string_field(object, "mime_type", video_url.empty())); + TRY_RESULT(video_width, get_json_object_int_field(object, "video_width")); + TRY_RESULT(video_height, get_json_object_int_field(object, "video_height")); + TRY_RESULT(video_duration, get_json_object_int_field(object, "video_duration")); + if (video_url.empty()) { + TRY_RESULT_ASSIGN(video_url, get_json_object_string_field(object, "video_file_id", false)); + } + + if (input_message_content == nullptr) { + input_message_content = + make_object(nullptr, nullptr, td::vector(), video_duration, video_width, + video_height, false, std::move(caption), 0); + } + return make_object(id, title, description, thumbnail_url, video_url, mime_type, + video_width, video_height, video_duration, + std::move(reply_markup), std::move(input_message_content)); + } + if (type == "voice") { + TRY_RESULT(title, get_json_object_string_field(object, "title", false)); + TRY_RESULT(voice_note_url, get_json_object_string_field(object, "voice_url")); + TRY_RESULT(voice_note_duration, get_json_object_int_field(object, "voice_duration")); + if (voice_note_url.empty()) { + TRY_RESULT_ASSIGN(voice_note_url, get_json_object_string_field(object, "voice_file_id", false)); + } + + if (input_message_content == nullptr) { + input_message_content = make_object(nullptr, voice_note_duration, + "" /* waveform */, std::move(caption)); + } + return make_object( + id, title, voice_note_url, voice_note_duration, std::move(reply_markup), std::move(input_message_content)); + } + + return Status::Error(400, PSLICE() << "type \"" << type << "\" is unsupported for the inline query result"); +} + +td::Result> Client::get_bot_command(JsonValue &&value) { + if (value.type() != JsonValue::Type::Object) { + return Status::Error(400, "expected an Object"); + } + + auto &object = value.get_object(); + + TRY_RESULT(command, get_json_object_string_field(object, "command", false)); + TRY_RESULT(description, get_json_object_string_field(object, "description", false)); + + return make_object(command, description); +} + +td::Result>> Client::get_bot_commands(const Query *query) { + auto commands = query->arg("commands"); + if (commands.empty()) { + return td::vector>(); + } + LOG(INFO) << "Parsing JSON object: " << commands; + auto r_value = json_decode(commands); + if (r_value.is_error()) { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + return Status::Error(400, "Can't parse commands JSON object"); + } + + auto value = r_value.move_as_ok(); + if (value.type() != JsonValue::Type::Array) { + return Status::Error(400, "Expected an Array of BotCommand"); + } + + td::vector> bot_commands; + for (auto &command : value.get_array()) { + auto r_bot_command = get_bot_command(std::move(command)); + if (r_bot_command.is_error()) { + return Status::Error(400, PSLICE() << "Can't parse BotCommand: " << r_bot_command.error().message()); + } + bot_commands.push_back(r_bot_command.move_as_ok()); + } + return std::move(bot_commands); +} + +td::Result> Client::get_mask_position(JsonValue &&value) { + if (value.type() != JsonValue::Type::Object) { + return Status::Error(400, "MaskPosition must be an Object"); + } + + auto &object = value.get_object(); + + TRY_RESULT(point_str, get_json_object_string_field(object, "point", false)); + point_str = td::trim(td::to_lower(point_str)); + int32 point; + for (point = 0; point < MASK_POINTS_SIZE; point++) { + if (MASK_POINTS[point] == point_str) { + break; + } + } + if (point == MASK_POINTS_SIZE) { + return Status::Error(400, "Wrong point specified in MaskPosition"); + } + + TRY_RESULT(x_shift, get_json_object_double_field(object, "x_shift", false)); + TRY_RESULT(y_shift, get_json_object_double_field(object, "y_shift", false)); + TRY_RESULT(scale, get_json_object_double_field(object, "scale", false)); + + return make_object(mask_index_to_point(point), x_shift, y_shift, scale); +} + +td::int32 Client::mask_point_to_index(const object_ptr &mask_point) { + CHECK(mask_point != nullptr); + switch (mask_point->get_id()) { + case td_api::maskPointForehead::ID: + return 0; + case td_api::maskPointEyes::ID: + return 1; + case td_api::maskPointMouth::ID: + return 2; + case td_api::maskPointChin::ID: + return 3; + default: + UNREACHABLE(); + return -1; + } +} + +td_api::object_ptr Client::mask_index_to_point(int32 index) { + switch (index) { + case 0: + return make_object(); + case 1: + return make_object(); + case 2: + return make_object(); + case 3: + return make_object(); + default: + UNREACHABLE(); + return nullptr; + } +} + +td::Result> Client::get_mask_position(const Query *query, Slice field_name) { + auto mask_position = query->arg(field_name); + if (mask_position.empty()) { + return nullptr; + } + + LOG(INFO) << "Parsing JSON object: " << mask_position; + auto r_value = json_decode(mask_position); + if (r_value.is_error()) { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + return Status::Error(400, "Can't parse mask position JSON object"); + } + + auto r_mask_position = get_mask_position(r_value.move_as_ok()); + if (r_mask_position.is_error()) { + return Status::Error(400, PSLICE() << "Can't parse mask position: " << r_mask_position.error().message()); + } + return r_mask_position.move_as_ok(); +} + +td::Result>> Client::get_input_stickers(const Query *query) const { + auto emojis = query->arg("emojis"); + + td::vector> stickers; + auto sticker = get_input_file(query, "png_sticker"); + if (sticker != nullptr) { + TRY_RESULT(mask_position, get_mask_position(query, "mask_position")); + stickers.push_back( + make_object(std::move(sticker), emojis.str(), std::move(mask_position))); + } else { + sticker = get_input_file(query, "tgs_sticker", true); + if (sticker == nullptr) { + if (query->arg("tgs_sticker").empty()) { + return Status::Error(400, "Bad Request: there is no sticker file in the request"); + } + return Status::Error(400, "Bad Request: TGS sticker must be uploaded as an InputFile"); + } + + stickers.push_back(make_object(std::move(sticker), emojis.str())); + } + CHECK(stickers.size() == 1); + + return std::move(stickers); +} + +td::Result Client::get_passport_element_hash(Slice encoded_hash) { + if (!td::is_base64(encoded_hash)) { + return Status::Error(400, "hash isn't a valid base64-encoded string"); + } + return td::base64_decode(encoded_hash).move_as_ok(); +} + +td::Result> Client::get_passport_element_error_source( + td::JsonObject &object) { + TRY_RESULT(source, get_json_object_string_field(object, "source")); + + if (source.empty() || source == "unspecified") { + TRY_RESULT(element_hash, get_json_object_string_field(object, "element_hash", false)); + TRY_RESULT(hash, get_passport_element_hash(element_hash)); + + return make_object(hash); + } + if (source == "data") { + TRY_RESULT(data_hash, get_json_object_string_field(object, "data_hash", false)); + TRY_RESULT(hash, get_passport_element_hash(data_hash)); + + TRY_RESULT(field_name, get_json_object_string_field(object, "field_name", false)); + return make_object(field_name, hash); + } + if (source == "file" || source == "selfie" || source == "translation_file" || source == "front_side" || + source == "reverse_side") { + TRY_RESULT(file_hash, get_json_object_string_field(object, "file_hash", false)); + TRY_RESULT(hash, get_passport_element_hash(file_hash)); + + if (source == "front_side") { + return make_object(hash); + } + if (source == "reverse_side") { + return make_object(hash); + } + if (source == "selfie") { + return make_object(hash); + } + if (source == "translation_file") { + return make_object(hash); + } + if (source == "file") { + return make_object(hash); + } + UNREACHABLE(); + } + if (source == "files" || source == "translation_files") { + td::vector input_hashes; + TRY_RESULT(file_hashes, get_json_object_field(object, "file_hashes", JsonValue::Type::Array, false)); + for (auto &input_hash : file_hashes.get_array()) { + if (input_hash.type() != JsonValue::Type::String) { + return Status::Error(400, "hash must be a string"); + } + TRY_RESULT(hash, get_passport_element_hash(input_hash.get_string())); + input_hashes.push_back(std::move(hash)); + } + if (source == "files") { + return make_object(std::move(input_hashes)); + } + if (source == "translation_files") { + return make_object(std::move(input_hashes)); + } + UNREACHABLE(); + } + return Status::Error(400, "wrong source specified"); +} + +td::Result> Client::get_passport_element_error( + JsonValue &&value) { + if (value.type() != JsonValue::Type::Object) { + return Status::Error(400, "expected an Object"); + } + + auto &object = value.get_object(); + + TRY_RESULT(input_type, get_json_object_string_field(object, "type", false)); + auto type = get_passport_element_type(input_type); + if (type == nullptr) { + return Status::Error(400, "wrong Telegram Passport element type specified"); + } + TRY_RESULT(message, get_json_object_string_field(object, "message", false)); + TRY_RESULT(source, get_passport_element_error_source(object)); + + return make_object(std::move(type), message, std::move(source)); +} + +td::Result>> Client::get_passport_element_errors( + const Query *query) { + auto input_errors = query->arg("errors"); + LOG(INFO) << "Parsing JSON object: " << input_errors; + auto r_value = json_decode(input_errors); + if (r_value.is_error()) { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + return Status::Error(400, "Can't parse errors JSON object"); + } + + auto value = r_value.move_as_ok(); + if (value.type() != JsonValue::Type::Array) { + return Status::Error(400, "Expected an Array of PassportElementError"); + } + + td::vector> errors; + for (auto &input_error : value.get_array()) { + auto r_error = get_passport_element_error(std::move(input_error)); + if (r_error.is_error()) { + return Status::Error(400, PSLICE() << "Can't parse PassportElementError: " << r_error.error().message()); + } + errors.push_back(r_error.move_as_ok()); + } + return std::move(errors); +} + +td::Result> Client::get_caption(const Query *query) { + JsonValue entities; + auto r_value = json_decode(query->arg("caption_entities")); + if (r_value.is_ok()) { + entities = r_value.move_as_ok(); + } else { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + } + + return get_formatted_text(query->arg("caption").str(), query->arg("parse_mode").str(), std::move(entities)); +} + +td::Result> Client::get_text_entity_type(td::JsonObject &object) { + TRY_RESULT(type, get_json_object_string_field(object, "type", false)); + if (type.empty()) { + return Status::Error("Type is not specified"); + } + + if (type == "bold") { + return make_object(); + } + if (type == "italic") { + return make_object(); + } + if (type == "underline") { + return make_object(); + } + if (type == "strikethrough") { + return make_object(); + } + if (type == "code") { + return make_object(); + } + if (type == "pre") { + TRY_RESULT(language, get_json_object_string_field(object, "language")); + if (language.empty()) { + return make_object(); + } + return make_object(language); + } + if (type == "text_link") { + TRY_RESULT(url, get_json_object_string_field(object, "url", false)); + return make_object(url); + } + if (type == "text_mention") { + TRY_RESULT(user, get_json_object_field(object, "user", JsonValue::Type::Object, false)); + CHECK(user.type() == JsonValue::Type::Object); + TRY_RESULT(user_id, get_json_object_int_field(user.get_object(), "id", false)); + return make_object(user_id); + } + if (type == "mention" || type == "hashtag" || type == "cashtag" || type == "bot_command" || type == "url" || + type == "email" || type == "phone_number" || type == "bank_card_number") { + return nullptr; + } + + return Status::Error("Unsupported type specified"); +} + +td::Result> Client::get_text_entity(JsonValue &&value) { + if (value.type() != JsonValue::Type::Object) { + return Status::Error(400, "expected an Object"); + } + + auto &object = value.get_object(); + TRY_RESULT(offset, get_json_object_int_field(object, "offset", false)); + TRY_RESULT(length, get_json_object_int_field(object, "length", false)); + TRY_RESULT(type, get_text_entity_type(object)); + + if (type == nullptr) { + return nullptr; + } + + return make_object(offset, length, std::move(type)); +} + +td::Result> Client::get_formatted_text(td::string text, td::string parse_mode, + JsonValue &&input_entities) { + td::to_lower_inplace(parse_mode); + if (!text.empty() && !parse_mode.empty() && parse_mode != "none") { + object_ptr text_parse_mode; + if (parse_mode == "markdown") { + text_parse_mode = make_object(1); + } else if (parse_mode == "markdownv2") { + text_parse_mode = make_object(2); + } else if (parse_mode == "html") { + text_parse_mode = make_object(); + } else { + return Status::Error(400, "Unsupported parse_mode"); + } + + auto parsed_text = execute(make_object(text, std::move(text_parse_mode))); + if (parsed_text->get_id() == td_api::error::ID) { + auto error = move_object_as(parsed_text); + return Status::Error(error->code_, error->message_); + } + + CHECK(parsed_text->get_id() == td_api::formattedText::ID); + return move_object_as(parsed_text); + } + + td::vector> entities; + if (input_entities.type() == JsonValue::Type::Array) { + for (auto &input_entity : input_entities.get_array()) { + auto r_entity = get_text_entity(std::move(input_entity)); + if (r_entity.is_error()) { + return Status::Error(400, PSLICE() << "Can't parse MessageEntity: " << r_entity.error().message()); + } + if (r_entity.ok() == nullptr) { + continue; + } + entities.push_back(r_entity.move_as_ok()); + } + } + + return make_object(text, std::move(entities)); +} + +td::Result> Client::get_input_message_text(const Query *query) { + JsonValue entities; + auto r_value = json_decode(query->arg("entities")); + if (r_value.is_ok()) { + entities = r_value.move_as_ok(); + } else { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + } + + return get_input_message_text(query->arg("text").str(), to_bool(query->arg("disable_web_page_preview")), + query->arg("parse_mode").str(), std::move(entities)); +} + +td::Result> Client::get_input_message_text(td::string text, + bool disable_web_page_preview, + td::string parse_mode, + JsonValue &&input_entities) { + if (text.empty()) { + return Status::Error(400, "Message text is empty"); + } + + TRY_RESULT(formatted_text, get_formatted_text(std::move(text), std::move(parse_mode), std::move(input_entities))); + + return make_object(std::move(formatted_text), disable_web_page_preview, false); +} + +td::Result> Client::get_location(const Query *query) { + auto latitude = trim(query->arg("latitude")); + if (latitude.empty()) { + return Status::Error(400, "Bad Request: latitude is empty"); + } + auto longitude = trim(query->arg("longitude")); + if (longitude.empty()) { + return Status::Error(400, "Bad Request: longitude is empty"); + } + auto horizontal_accuracy = trim(query->arg("horizontal_accuracy")); + + return make_object(td::to_double(latitude), td::to_double(longitude), + td::to_double(horizontal_accuracy)); +} + +td::Result> Client::get_chat_permissions(const Query *query, + bool &allow_legacy) { + auto can_send_messages = false; + auto can_send_media_messages = false; + auto can_send_polls = false; + auto can_send_other_messages = false; + auto can_add_web_page_previews = false; + auto can_change_info = false; + auto can_invite_users = false; + auto can_pin_messages = false; + + if (query->has_arg("permissions")) { + allow_legacy = false; + + auto r_value = json_decode(query->arg("permissions")); + if (r_value.is_error()) { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + return Status::Error(400, "Can't parse permissions JSON object"); + } + + auto value = r_value.move_as_ok(); + if (value.type() != JsonValue::Type::Object) { + return Status::Error(400, "Object expected as permissions"); + } + auto &object = value.get_object(); + + auto status = [&] { + TRY_RESULT_ASSIGN(can_send_messages, get_json_object_bool_field(object, "can_send_messages")); + TRY_RESULT_ASSIGN(can_send_media_messages, get_json_object_bool_field(object, "can_send_media_messages")); + TRY_RESULT_ASSIGN(can_send_polls, get_json_object_bool_field(object, "can_send_polls")); + TRY_RESULT_ASSIGN(can_send_other_messages, get_json_object_bool_field(object, "can_send_other_messages")); + TRY_RESULT_ASSIGN(can_add_web_page_previews, get_json_object_bool_field(object, "can_add_web_page_previews")); + TRY_RESULT_ASSIGN(can_change_info, get_json_object_bool_field(object, "can_change_info")); + TRY_RESULT_ASSIGN(can_invite_users, get_json_object_bool_field(object, "can_invite_users")); + TRY_RESULT_ASSIGN(can_pin_messages, get_json_object_bool_field(object, "can_pin_messages")); + return Status::OK(); + }(); + + if (status.is_error()) { + return Status::Error(400, PSLICE() << "Can't parse chat permissions: " << status.error().message()); + } + } else if (allow_legacy) { + allow_legacy = false; + + can_send_messages = to_bool(query->arg("can_send_messages")); + can_send_media_messages = to_bool(query->arg("can_send_media_messages")); + can_send_other_messages = to_bool(query->arg("can_send_other_messages")); + can_add_web_page_previews = to_bool(query->arg("can_add_web_page_previews")); + + if (can_send_messages && can_send_media_messages && can_send_other_messages && can_add_web_page_previews) { + // legacy unrestrict + can_send_polls = true; + can_change_info = true; + can_invite_users = true; + can_pin_messages = true; + } else if (query->has_arg("can_send_messages") || query->has_arg("can_send_media_messages") || + query->has_arg("can_send_other_messages") || query->has_arg("can_add_web_page_previews")) { + allow_legacy = true; + } + } + + if (can_send_other_messages || can_add_web_page_previews) { + can_send_media_messages = true; + } + return make_object(can_send_messages, can_send_media_messages, can_send_polls, + can_send_other_messages, can_add_web_page_previews, can_change_info, + can_invite_users, can_pin_messages); +} + +td::Result> Client::get_input_media(const Query *query, + JsonValue &&input_media, + bool for_album) const { + if (input_media.type() != JsonValue::Type::Object) { + return Status::Error(400, "expected an Object"); + } + + auto &object = input_media.get_object(); + + TRY_RESULT(input_caption, get_json_object_string_field(object, "caption")); + TRY_RESULT(parse_mode, get_json_object_string_field(object, "parse_mode")); + auto entities = get_json_object_field_force(object, "caption_entities"); + TRY_RESULT(caption, get_formatted_text(std::move(input_caption), std::move(parse_mode), std::move(entities))); + // TRY_RESULT(ttl, get_json_object_int_field(object, "ttl")); + int32 ttl = 0; + TRY_RESULT(media, get_json_object_string_field(object, "media", true)); + + auto input_file = get_input_file(query, Slice(), media, false); + if (input_file == nullptr) { + return Status::Error(400, "media not found"); + } + + TRY_RESULT(thumbnail, get_json_object_string_field(object, "thumb")); + object_ptr input_thumbnail; + auto thumbanil_input_file = get_input_file(query, thumbnail.empty() ? Slice("thumb") : Slice(), thumbnail, true); + if (thumbanil_input_file != nullptr) { + input_thumbnail = make_object(std::move(thumbanil_input_file), 0, 0); + } + + TRY_RESULT(type, get_json_object_string_field(object, "type", false)); + if (type == "photo") { + return make_object(std::move(input_file), std::move(input_thumbnail), + td::vector(), 0, 0, std::move(caption), ttl); + } + if (type == "video") { + TRY_RESULT(width, get_json_object_int_field(object, "width")); + TRY_RESULT(height, get_json_object_int_field(object, "height")); + TRY_RESULT(duration, get_json_object_int_field(object, "duration")); + TRY_RESULT(supports_streaming, get_json_object_bool_field(object, "supports_streaming")); + width = td::clamp(width, 0, MAX_LENGTH); + height = td::clamp(height, 0, MAX_LENGTH); + duration = td::clamp(duration, 0, MAX_DURATION); + + return make_object(std::move(input_file), std::move(input_thumbnail), + td::vector(), duration, width, height, supports_streaming, + std::move(caption), ttl); + } + if (for_album && type == "animation") { + return Status::Error(400, PSLICE() << "type \"" << type << "\" can't be used in sendMediaGroup"); + } + if (type == "animation") { + TRY_RESULT(width, get_json_object_int_field(object, "width")); + TRY_RESULT(height, get_json_object_int_field(object, "height")); + TRY_RESULT(duration, get_json_object_int_field(object, "duration")); + width = td::clamp(width, 0, MAX_LENGTH); + height = td::clamp(height, 0, MAX_LENGTH); + duration = td::clamp(duration, 0, MAX_DURATION); + return make_object(std::move(input_file), std::move(input_thumbnail), + td::vector(), duration, width, height, std::move(caption)); + } + if (type == "audio") { + TRY_RESULT(duration, get_json_object_int_field(object, "duration")); + TRY_RESULT(title, get_json_object_string_field(object, "title")); + TRY_RESULT(performer, get_json_object_string_field(object, "performer")); + duration = td::clamp(duration, 0, MAX_DURATION); + return make_object(std::move(input_file), std::move(input_thumbnail), duration, title, + performer, std::move(caption)); + } + if (type == "document") { + TRY_RESULT(disable_content_type_detection, get_json_object_bool_field(object, "disable_content_type_detection")); + return make_object(std::move(input_file), std::move(input_thumbnail), + disable_content_type_detection || for_album, std::move(caption)); + } + + return Status::Error(400, PSLICE() << "type \"" << type << "\" is unsupported"); +} + +td::Result> Client::get_input_media(const Query *query, + Slice field_name, + bool for_album) const { + TRY_RESULT(media, get_required_string_arg(query, field_name)); + + LOG(INFO) << "Parsing JSON object: " << media; + auto r_value = json_decode(media); + if (r_value.is_error()) { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + return Status::Error(400, "Can't parse input media JSON object"); + } + + auto r_input_message_content = get_input_media(query, r_value.move_as_ok(), for_album); + if (r_input_message_content.is_error()) { + return Status::Error(400, PSLICE() << "Can't parse InputMedia: " << r_input_message_content.error().message()); + } + return r_input_message_content.move_as_ok(); +} + +td::Result>> Client::get_input_message_contents( + const Query *query, Slice field_name) const { + TRY_RESULT(media, get_required_string_arg(query, field_name)); + + LOG(INFO) << "Parsing JSON object: " << media; + auto r_value = json_decode(media); + if (r_value.is_error()) { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + return Status::Error(400, "Can't parse media JSON object"); + } + + return get_input_message_contents(query, r_value.move_as_ok()); +} + +td::Result>> Client::get_input_message_contents( + const Query *query, JsonValue &&value) const { + if (value.type() != JsonValue::Type::Array) { + return Status::Error(400, "Expected an Array of InputMedia"); + } + + td::vector> contents; + for (auto &input_media : value.get_array()) { + TRY_RESULT(input_message_content, get_input_media(query, std::move(input_media), true)); + contents.push_back(std::move(input_message_content)); + } + return std::move(contents); +} + +td::Result> Client::get_poll_options(const Query *query) { + auto input_options = query->arg("options"); + LOG(INFO) << "Parsing JSON object: " << input_options; + auto r_value = json_decode(input_options); + if (r_value.is_error()) { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + return Status::Error(400, "Can't parse options JSON object"); + } + + auto value = r_value.move_as_ok(); + if (value.type() != JsonValue::Type::Array) { + return Status::Error(400, "Expected an Array of String as options"); + } + + td::vector options; + for (auto &input_option : value.get_array()) { + if (input_option.type() != JsonValue::Type::String) { + return Status::Error(400, "Expected an option to be of type String"); + } + options.push_back(input_option.get_string().str()); + } + return std::move(options); +} + +td::int32 Client::get_integer_arg(const Query *query, Slice field_name, int32 default_value, int32 min_value, + int32 max_value) { + auto s_arg = query->arg(field_name); + if (s_arg.empty()) { + return default_value; + } + + return td::clamp(td::to_integer(s_arg), min_value, max_value); +} + +td::Result Client::get_required_string_arg(const Query *query, Slice field_name) { + auto s_arg = query->arg(field_name); + if (s_arg.empty()) { + return Status::Error(400, PSLICE() << "Parameter \"" << field_name << "\" is required"); + } + return s_arg; +} + +td::int64 Client::get_message_id(const Query *query, Slice field_name) { + auto s_arg = query->arg(field_name); + if (s_arg.empty()) { + return 0; + } + + int arg = td::to_integer(s_arg); + if (arg < 0) { + return 0; + } + + return as_tdlib_message_id(arg); +} + +td::Result Client::get_inline_message_id(const Query *query, Slice field_name) { + auto s_arg = query->arg(field_name); + if (s_arg.empty()) { + return Status::Error(400, "Message identifier is not specified"); + } + return s_arg; +} + +td::Result Client::get_user_id(const Query *query, Slice field_name) { + int32 user_id = get_integer_arg(query, field_name, 0, 0); + if (user_id == 0) { + return Status::Error(400, PSLICE() << "Invalid " << field_name << " specified"); + } + return user_id; +} + +td::int64 Client::extract_yet_unsent_message_query_id(int64 chat_id, int64 message_id, + bool *is_reply_to_message_deleted) { + auto yet_unsent_message_it = yet_unsent_messages_.find({chat_id, message_id}); + CHECK(yet_unsent_message_it != yet_unsent_messages_.end()); + + auto reply_to_message_id = yet_unsent_message_it->second.reply_to_message_id; + if (is_reply_to_message_deleted != nullptr && yet_unsent_message_it->second.is_reply_to_message_deleted) { + *is_reply_to_message_deleted = true; + } + auto query_id = yet_unsent_message_it->second.send_message_query_id; + + yet_unsent_messages_.erase(yet_unsent_message_it); + + if (reply_to_message_id > 0) { + auto it = yet_unsent_reply_message_ids_.find({chat_id, reply_to_message_id}); + CHECK(it != yet_unsent_reply_message_ids_.end()); + auto erased_count = it->second.erase(message_id); + CHECK(erased_count > 0); + if (it->second.empty()) { + yet_unsent_reply_message_ids_.erase(it); + } + } + + return query_id; +} + +void Client::on_message_send_succeeded(object_ptr &&message, int64 old_message_id) { + auto full_message_id = add_message(std::move(message), true); + + int64 chat_id = full_message_id.chat_id; + int64 new_message_id = full_message_id.message_id; + CHECK(new_message_id > 0); + + auto message_info = get_message(chat_id, new_message_id); + CHECK(message_info != nullptr); + message_info->is_content_changed = false; + + auto query_id = + extract_yet_unsent_message_query_id(chat_id, old_message_id, &message_info->is_reply_to_message_deleted); + auto &query = pending_send_message_queries_[query_id]; + if (query.is_multisend) { + query.messages.push_back(td::json_encode(JsonMessage(message_info, true, "sent message", this))); + query.awaited_messages--; + + if (query.awaited_messages == 0) { + if (query.error == nullptr) { + answer_query(JsonMessages(query.messages), std::move(query.query)); + } else { + fail_query_with_error(std::move(query.query), std::move(query.error)); + } + pending_send_message_queries_.erase(query_id); + } + } else { + CHECK(query.awaited_messages == 1); + if (query.query->method() == "copymessage") { + answer_query(JsonMessageId(new_message_id), std::move(query.query)); + } else { + answer_query(JsonMessage(message_info, true, "sent message", this), std::move(query.query)); + } + pending_send_message_queries_.erase(query_id); + } +} + +void Client::on_message_send_failed(int64 chat_id, int64 old_message_id, int64 new_message_id, Status result) { + auto error = make_object(result.code(), result.message().str()); + + auto query_id = extract_yet_unsent_message_query_id(chat_id, old_message_id, nullptr); + auto &query = pending_send_message_queries_[query_id]; + if (query.is_multisend) { + if (query.error == nullptr) { + query.error = std::move(error); + } + query.awaited_messages--; + + if (query.awaited_messages == 0) { + fail_query_with_error(std::move(query.query), std::move(query.error)); + pending_send_message_queries_.erase(query_id); + } + } else { + CHECK(query.awaited_messages == 1); + fail_query_with_error(std::move(query.query), std::move(error)); + pending_send_message_queries_.erase(query_id); + } + + if (new_message_id != 0 && !logging_out_ && !closing_) { + send_request(make_object(chat_id, td::vector{new_message_id}, false), + std::make_unique(this, chat_id, new_message_id)); + } +} + +void Client::on_cmd(PromisedQueryPtr query) { + LOG(DEBUG) << "Process query " << *query; + if (!td_client_.empty()) { + if (query->method() == "close") { + auto retry_after = static_cast(10 * 60 - (td::Time::now() - start_timestamp_)); + if (retry_after > 0 && start_timestamp_ > parameters_->start_timestamp_ + 10 * 60) { + return query->set_retry_after_error(retry_after); + } + need_close_ = true; + return do_send_request(make_object(), std::make_unique(std::move(query))); + } + if (query->method() == "logout") { + clear_tqueue_ = true; + return do_send_request(make_object(), std::make_unique(std::move(query))); + } + } + + if (logging_out_) { + return fail_query(LOGGING_OUT_ERROR_CODE, LOGGING_OUT_ERROR_DESCRIPTION, std::move(query)); + } + if (closing_) { + return fail_query(CLOSING_ERROR_CODE, LOGGING_OUT_ERROR_DESCRIPTION, std::move(query)); + } + CHECK(was_authorized_); + + unresolved_bot_usernames_.clear(); + + auto method_it = methods_.find(query->method().str()); + if (method_it == methods_.end()) { + return fail_query(404, "Not Found: method not found", std::move(query)); + } + + auto result = (this->*(method_it->second))(query); + if (result.is_error()) { + fail_query_with_error(std::move(query), result.code(), result.message()); + } +} + +td::Status Client::process_get_me_query(PromisedQueryPtr &query) { + answer_query(JsonUser(my_id_, this, true), std::move(query)); + return Status::OK(); +} + +td::Status Client::process_get_my_commands_query(PromisedQueryPtr &query) { + send_request(make_object(my_id_), + std::make_unique(std::move(query))); + return Status::OK(); +} + +td::Status Client::process_set_my_commands_query(PromisedQueryPtr &query) { + TRY_RESULT(bot_commands, get_bot_commands(query.get())); + send_request(make_object(std::move(bot_commands)), + std::make_unique(std::move(query))); + return Status::OK(); +} + +td::Status Client::process_get_user_profile_photos_query(PromisedQueryPtr &query) { + TRY_RESULT(user_id, get_user_id(query.get())); + int32 offset = get_integer_arg(query.get(), "offset", 0, 0); + int32 limit = get_integer_arg(query.get(), "limit", 100, 1, 100); + + check_user(user_id, std::move(query), [this, user_id, offset, limit](PromisedQueryPtr query) { + send_request(make_object(user_id, offset, limit), + std::make_unique(this, std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_send_message_query(PromisedQueryPtr &query) { + TRY_RESULT(input_message_text, get_input_message_text(query.get())); + do_send_message(std::move(input_message_text), std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_animation_query(PromisedQueryPtr &query) { + auto animation = get_input_file(query.get(), "animation"); + if (animation == nullptr) { + return Status::Error(400, "There is no animation in the request"); + } + auto thumbnail = get_input_thumbnail(query.get(), "thumb"); + int32 duration = get_integer_arg(query.get(), "duration", 0, 0, MAX_DURATION); + int32 width = get_integer_arg(query.get(), "width", 0, 0, MAX_LENGTH); + int32 height = get_integer_arg(query.get(), "height", 0, 0, MAX_LENGTH); + TRY_RESULT(caption, get_caption(query.get())); + do_send_message( + make_object(std::move(animation), std::move(thumbnail), td::vector(), + duration, width, height, std::move(caption)), + std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_audio_query(PromisedQueryPtr &query) { + auto audio = get_input_file(query.get(), "audio"); + if (audio == nullptr) { + return Status::Error(400, "There is no audio in the request"); + } + auto thumbnail = get_input_thumbnail(query.get(), "thumb"); + int32 duration = get_integer_arg(query.get(), "duration", 0, 0, MAX_DURATION); + auto title = query->arg("title").str(); + auto performer = query->arg("performer").str(); + TRY_RESULT(caption, get_caption(query.get())); + do_send_message(make_object(std::move(audio), std::move(thumbnail), duration, title, + performer, std::move(caption)), + std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_dice_query(PromisedQueryPtr &query) { + auto emoji = query->arg("emoji"); + do_send_message(make_object(emoji.str(), false), std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_document_query(PromisedQueryPtr &query) { + auto document = get_input_file(query.get(), "document"); + if (document == nullptr) { + return Status::Error(400, "There is no document in the request"); + } + auto thumbnail = get_input_thumbnail(query.get(), "thumb"); + TRY_RESULT(caption, get_caption(query.get())); + bool disable_content_type_detection = to_bool(query->arg("disable_content_type_detection")); + do_send_message(make_object(std::move(document), std::move(thumbnail), + disable_content_type_detection, std::move(caption)), + std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_photo_query(PromisedQueryPtr &query) { + auto photo = get_input_file(query.get(), "photo"); + if (photo == nullptr) { + return Status::Error(400, "There is no photo in the request"); + } + TRY_RESULT(caption, get_caption(query.get())); + auto ttl = 0; + do_send_message(make_object(std::move(photo), nullptr, td::vector(), 0, 0, + std::move(caption), ttl), + std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_sticker_query(PromisedQueryPtr &query) { + auto sticker = get_input_file(query.get(), "sticker"); + if (sticker == nullptr) { + return Status::Error(400, "There is no sticker in the request"); + } + do_send_message(make_object(std::move(sticker), nullptr, 0, 0), std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_video_query(PromisedQueryPtr &query) { + auto video = get_input_file(query.get(), "video"); + if (video == nullptr) { + return Status::Error(400, "There is no video in the request"); + } + auto thumbnail = get_input_thumbnail(query.get(), "thumb"); + int32 duration = get_integer_arg(query.get(), "duration", 0, 0, MAX_DURATION); + int32 width = get_integer_arg(query.get(), "width", 0, 0, MAX_LENGTH); + int32 height = get_integer_arg(query.get(), "height", 0, 0, MAX_LENGTH); + bool supports_streaming = to_bool(query->arg("supports_streaming")); + TRY_RESULT(caption, get_caption(query.get())); + auto ttl = 0; + do_send_message( + make_object(std::move(video), std::move(thumbnail), td::vector(), duration, + width, height, supports_streaming, std::move(caption), ttl), + std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_video_note_query(PromisedQueryPtr &query) { + auto video_note = get_input_file(query.get(), "video_note"); + if (video_note == nullptr) { + return Status::Error(400, "There is no video note in the request"); + } + auto thumbnail = get_input_thumbnail(query.get(), "thumb"); + int32 duration = get_integer_arg(query.get(), "duration", 0, 0, MAX_DURATION); + int32 length = get_integer_arg(query.get(), "length", 0, 0, MAX_LENGTH); + do_send_message( + make_object(std::move(video_note), std::move(thumbnail), duration, length), + std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_voice_query(PromisedQueryPtr &query) { + auto voice_note = get_input_file(query.get(), "voice"); + if (voice_note == nullptr) { + return Status::Error(400, "There is no voice in the request"); + } + int32 duration = get_integer_arg(query.get(), "duration", 0, 0, MAX_DURATION); + TRY_RESULT(caption, get_caption(query.get())); + do_send_message(make_object(std::move(voice_note), duration, "", std::move(caption)), + std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_game_query(PromisedQueryPtr &query) { + TRY_RESULT(game_short_name, get_required_string_arg(query.get(), "game_short_name")); + do_send_message(make_object(my_id_, game_short_name.str()), std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_invoice_query(PromisedQueryPtr &query) { + TRY_RESULT(title, get_required_string_arg(query.get(), "title")); + TRY_RESULT(description, get_required_string_arg(query.get(), "description")); + TRY_RESULT(payload, get_required_string_arg(query.get(), "payload")); + if (!td::check_utf8(payload.str())) { + return Status::Error(400, "The payload must be encoded in UTF-8"); + } + TRY_RESULT(provider_token, get_required_string_arg(query.get(), "provider_token")); + auto provider_data = query->arg("provider_data"); + TRY_RESULT(start_parameter, get_required_string_arg(query.get(), "start_parameter")); + TRY_RESULT(currency, get_required_string_arg(query.get(), "currency")); + + TRY_RESULT(labeled_price_parts, get_required_string_arg(query.get(), "prices")); + auto r_value = json_decode(labeled_price_parts); + if (r_value.is_error()) { + return Status::Error(400, "Can't parse prices JSON object"); + } + + TRY_RESULT(prices, get_labeled_price_parts(r_value.ok_ref())); + + auto photo_url = query->arg("photo_url"); + int32 photo_size = get_integer_arg(query.get(), "photo_size", 0, 0, 1000000000); + int32 photo_width = get_integer_arg(query.get(), "photo_width", 0, 0, MAX_LENGTH); + int32 photo_height = get_integer_arg(query.get(), "photo_height", 0, 0, MAX_LENGTH); + + auto need_name = to_bool(query->arg("need_name")); + auto need_phone_number = to_bool(query->arg("need_phone_number")); + auto need_email_address = to_bool(query->arg("need_email")); + auto need_shipping_address = to_bool(query->arg("need_shipping_address")); + auto send_phone_number_to_provider = to_bool(query->arg("send_phone_number_to_provider")); + auto send_email_address_to_provider = to_bool(query->arg("send_email_to_provider")); + auto is_flexible = to_bool(query->arg("is_flexible")); + + do_send_message( + make_object( + make_object(currency.str(), std::move(prices), false, need_name, need_phone_number, + need_email_address, need_shipping_address, send_phone_number_to_provider, + send_email_address_to_provider, is_flexible), + title.str(), description.str(), photo_url.str(), photo_size, photo_width, photo_height, payload.str(), + provider_token.str(), provider_data.str(), start_parameter.str()), + std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_location_query(PromisedQueryPtr &query) { + TRY_RESULT(location, get_location(query.get())); + int32 live_period = get_integer_arg(query.get(), "live_period", 0); + int32 heading = get_integer_arg(query.get(), "heading", 0); + int32 proximity_alert_radius = get_integer_arg(query.get(), "proximity_alert_radius", 0); + + do_send_message( + make_object(std::move(location), live_period, heading, proximity_alert_radius), + std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_venue_query(PromisedQueryPtr &query) { + TRY_RESULT(location, get_location(query.get())); + + auto title = query->arg("title"); + auto address = query->arg("address"); + td::string provider; + td::string venue_id; + td::string venue_type; + + auto google_place_id = query->arg("google_place_id"); + auto google_place_type = query->arg("google_place_type"); + if (!google_place_id.empty() || !google_place_type.empty()) { + provider = "gplaces"; + venue_id = google_place_id.str(); + venue_type = google_place_type.str(); + } + auto foursquare_id = query->arg("foursquare_id"); + auto foursquare_type = query->arg("foursquare_type"); + if (!foursquare_id.empty() || !foursquare_type.empty()) { + provider = "foursquare"; + venue_id = foursquare_id.str(); + venue_type = foursquare_type.str(); + } + + do_send_message(make_object(make_object( + std::move(location), title.str(), address.str(), provider, venue_id, venue_type)), + std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_contact_query(PromisedQueryPtr &query) { + TRY_RESULT(phone_number, get_required_string_arg(query.get(), "phone_number")); + TRY_RESULT(first_name, get_required_string_arg(query.get(), "first_name")); + auto last_name = query->arg("last_name"); + auto vcard = query->arg("vcard"); + do_send_message(make_object(make_object( + phone_number.str(), first_name.str(), last_name.str(), vcard.str(), 0)), + std::move(query)); + return Status::OK(); +} + +td::Status Client::process_send_poll_query(PromisedQueryPtr &query) { + auto question = query->arg("question"); + TRY_RESULT(options, get_poll_options(query.get())); + bool is_anonymous = true; + if (query->has_arg("is_anonymous")) { + is_anonymous = to_bool(query->arg("is_anonymous")); + } + + object_ptr poll_type; + auto type = query->arg("type"); + if (type == "quiz") { + JsonValue entities; + auto r_value = json_decode(query->arg("explanation_entities")); + if (r_value.is_ok()) { + entities = r_value.move_as_ok(); + } else { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + } + TRY_RESULT(explanation, get_formatted_text(query->arg("explanation").str(), + query->arg("explanation_parse_mode").str(), std::move(entities))); + + poll_type = make_object(get_integer_arg(query.get(), "correct_option_id", -1), + std::move(explanation)); + } else if (type.empty() || type == "regular") { + poll_type = make_object(to_bool(query->arg("allows_multiple_answers"))); + } else { + return Status::Error(400, "Unsupported poll type specified"); + } + int32 open_period = get_integer_arg(query.get(), "open_period", 0, 0, 10 * 60); + int32 close_date = get_integer_arg(query.get(), "close_date", 0); + auto is_closed = to_bool(query->arg("is_closed")); + do_send_message(make_object(question.str(), std::move(options), is_anonymous, + std::move(poll_type), open_period, close_date, is_closed), + std::move(query)); + return Status::OK(); +} + +td::Status Client::process_stop_poll_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto message_id = get_message_id(query.get()); + TRY_RESULT(reply_markup, get_reply_markup(query.get())); + + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, chat_id = chat_id.str(), message_id](object_ptr reply_markup, + PromisedQueryPtr query) { + check_message(chat_id, message_id, false, AccessRights::Edit, "message with poll to stop", std::move(query), + [this, reply_markup = std::move(reply_markup)](int64 chat_id, int64 message_id, + PromisedQueryPtr query) mutable { + send_request( + make_object(chat_id, message_id, std::move(reply_markup)), + std::make_unique(this, chat_id, message_id, std::move(query))); + }); + }); + return Status::OK(); +} + +td::Status Client::process_copy_message_query(PromisedQueryPtr &query) { + TRY_RESULT(from_chat_id, get_required_string_arg(query.get(), "from_chat_id")); + auto message_id = get_message_id(query.get()); + bool replace_caption = query->has_arg("caption"); + td_api::object_ptr caption; + if (replace_caption) { + TRY_RESULT_ASSIGN(caption, get_caption(query.get())); + } + auto options = make_object(true, replace_caption, std::move(caption)); + + check_message( + from_chat_id, message_id, false, AccessRights::Read, "message to copy", std::move(query), + [this, options = std::move(options)](int64 from_chat_id, int64 message_id, PromisedQueryPtr query) mutable { + do_send_message(make_object(from_chat_id, message_id, false, std::move(options)), + std::move(query)); + }); + return Status::OK(); +} + +td::Status Client::process_forward_message_query(PromisedQueryPtr &query) { + TRY_RESULT(from_chat_id, get_required_string_arg(query.get(), "from_chat_id")); + auto message_id = get_message_id(query.get()); + + check_message(from_chat_id, message_id, false, AccessRights::Read, "message to forward", std::move(query), + [this](int64 from_chat_id, int64 message_id, PromisedQueryPtr query) { + do_send_message(make_object(from_chat_id, message_id, false, nullptr), + std::move(query)); + }); + return Status::OK(); +} + +td::Status Client::process_send_media_group_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto reply_to_message_id = get_message_id(query.get(), "reply_to_message_id"); + auto allow_sending_without_reply = to_bool(query->arg("allow_sending_without_reply")); + auto disable_notification = to_bool(query->arg("disable_notification")); + // TRY_RESULT(reply_markup, get_reply_markup(query.get())); + auto reply_markup = nullptr; + TRY_RESULT(input_message_contents, get_input_message_contents(query.get(), "media")); + + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, chat_id = chat_id.str(), reply_to_message_id, allow_sending_without_reply, disable_notification, + input_message_contents = std::move(input_message_contents)](object_ptr reply_markup, + PromisedQueryPtr query) mutable { + auto on_success = [this, disable_notification, input_message_contents = std::move(input_message_contents), + reply_markup = std::move(reply_markup)](int64 chat_id, int64 reply_to_message_id, + PromisedQueryPtr query) mutable { + send_request(make_object(chat_id, 0, reply_to_message_id, + get_message_send_options(disable_notification), + std::move(input_message_contents)), + std::make_unique(this, std::move(query))); + }; + check_message(chat_id, reply_to_message_id, reply_to_message_id <= 0 || allow_sending_without_reply, + AccessRights::Write, "reply message", std::move(query), std::move(on_success)); + }); + return Status::OK(); +} + +td::Status Client::process_send_chat_action_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + object_ptr action = get_chat_action(query.get()); + if (action == nullptr) { + return Status::Error(400, "Wrong parameter action in request"); + } + + check_chat(chat_id, AccessRights::Write, std::move(query), + [this, action = std::move(action)](int64 chat_id, PromisedQueryPtr query) mutable { + send_request(make_object(chat_id, 0, std::move(action)), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_edit_message_text_query(PromisedQueryPtr &query) { + TRY_RESULT(input_message_text, get_input_message_text(query.get())); + auto chat_id = query->arg("chat_id"); + auto message_id = get_message_id(query.get()); + TRY_RESULT(reply_markup, get_reply_markup(query.get())); + + if (chat_id.empty() && message_id == 0) { + TRY_RESULT(inline_message_id, get_inline_message_id(query.get())); + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, inline_message_id = inline_message_id.str(), input_message_text = std::move(input_message_text)]( + object_ptr reply_markup, PromisedQueryPtr query) mutable { + send_request(make_object(inline_message_id, std::move(reply_markup), + std::move(input_message_text)), + std::make_unique(std::move(query))); + }); + } else { + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, chat_id = chat_id.str(), message_id, input_message_text = std::move(input_message_text)]( + object_ptr reply_markup, PromisedQueryPtr query) mutable { + check_message( + chat_id, message_id, false, AccessRights::Edit, "message to edit", std::move(query), + [this, input_message_text = std::move(input_message_text), reply_markup = std::move(reply_markup)]( + int64 chat_id, int64 message_id, PromisedQueryPtr query) mutable { + send_request(make_object(chat_id, message_id, std::move(reply_markup), + std::move(input_message_text)), + std::make_unique(this, std::move(query))); + }); + }); + } + return Status::OK(); +} + +td::Status Client::process_edit_message_live_location_query(PromisedQueryPtr &query) { + object_ptr location = nullptr; + int32 heading = get_integer_arg(query.get(), "heading", 0); + int32 proximity_alert_radius = get_integer_arg(query.get(), "proximity_alert_radius", 0); + if (query->method() == "editmessagelivelocation") { + TRY_RESULT_ASSIGN(location, get_location(query.get())); + } + auto chat_id = query->arg("chat_id"); + auto message_id = get_message_id(query.get()); + TRY_RESULT(reply_markup, get_reply_markup(query.get())); + + if (chat_id.empty() && message_id == 0) { + TRY_RESULT(inline_message_id, get_inline_message_id(query.get())); + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, inline_message_id = inline_message_id.str(), location = std::move(location), heading, + proximity_alert_radius](object_ptr reply_markup, PromisedQueryPtr query) mutable { + send_request( + make_object(inline_message_id, std::move(reply_markup), + std::move(location), heading, proximity_alert_radius), + std::make_unique(std::move(query))); + }); + } else { + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, chat_id = chat_id.str(), message_id, location = std::move(location), heading, proximity_alert_radius]( + object_ptr reply_markup, PromisedQueryPtr query) mutable { + check_message(chat_id, message_id, false, AccessRights::Edit, "message to edit", std::move(query), + [this, location = std::move(location), heading, proximity_alert_radius, + reply_markup = std::move(reply_markup)](int64 chat_id, int64 message_id, + PromisedQueryPtr query) mutable { + send_request(make_object( + chat_id, message_id, std::move(reply_markup), std::move(location), heading, + proximity_alert_radius), + std::make_unique(this, std::move(query))); + }); + }); + } + return Status::OK(); +} + +td::Status Client::process_edit_message_media_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto message_id = get_message_id(query.get()); + TRY_RESULT(reply_markup, get_reply_markup(query.get())); + TRY_RESULT(input_media, get_input_media(query.get(), "media", false)); + + if (chat_id.empty() && message_id == 0) { + TRY_RESULT(inline_message_id, get_inline_message_id(query.get())); + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, inline_message_id = inline_message_id.str(), input_message_content = std::move(input_media)]( + object_ptr reply_markup, PromisedQueryPtr query) mutable { + send_request(make_object(inline_message_id, std::move(reply_markup), + std::move(input_message_content)), + std::make_unique(std::move(query))); + }); + } else { + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, chat_id = chat_id.str(), message_id, input_message_content = std::move(input_media)]( + object_ptr reply_markup, PromisedQueryPtr query) mutable { + check_message( + chat_id, message_id, false, AccessRights::Edit, "message to edit", std::move(query), + [this, reply_markup = std::move(reply_markup), input_message_content = std::move(input_message_content)]( + int64 chat_id, int64 message_id, PromisedQueryPtr query) mutable { + send_request(make_object(chat_id, message_id, std::move(reply_markup), + std::move(input_message_content)), + std::make_unique(this, std::move(query))); + }); + }); + } + return Status::OK(); +} + +td::Status Client::process_edit_message_caption_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto message_id = get_message_id(query.get()); + TRY_RESULT(reply_markup, get_reply_markup(query.get())); + TRY_RESULT(caption, get_caption(query.get())); + + if (chat_id.empty() && message_id == 0) { + TRY_RESULT(inline_message_id, get_inline_message_id(query.get())); + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, inline_message_id = inline_message_id.str(), caption = std::move(caption)]( + object_ptr reply_markup, PromisedQueryPtr query) mutable { + send_request(make_object(inline_message_id, std::move(reply_markup), + std::move(caption)), + std::make_unique(std::move(query))); + }); + } else { + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, chat_id = chat_id.str(), message_id, caption = std::move(caption)]( + object_ptr reply_markup, PromisedQueryPtr query) mutable { + check_message(chat_id, message_id, false, AccessRights::Edit, "message to edit", std::move(query), + [this, reply_markup = std::move(reply_markup), caption = std::move(caption)]( + int64 chat_id, int64 message_id, PromisedQueryPtr query) mutable { + send_request(make_object( + chat_id, message_id, std::move(reply_markup), std::move(caption)), + std::make_unique(this, std::move(query))); + }); + }); + } + return Status::OK(); +} + +td::Status Client::process_edit_message_reply_markup_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto message_id = get_message_id(query.get()); + TRY_RESULT(reply_markup, get_reply_markup(query.get())); + + if (chat_id.empty() && message_id == 0) { + TRY_RESULT(inline_message_id, get_inline_message_id(query.get())); + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, inline_message_id = inline_message_id.str()](object_ptr reply_markup, + PromisedQueryPtr query) { + send_request(make_object(inline_message_id, std::move(reply_markup)), + std::make_unique(std::move(query))); + }); + } else { + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, chat_id = chat_id.str(), message_id](object_ptr reply_markup, + PromisedQueryPtr query) { + check_message(chat_id, message_id, false, AccessRights::Edit, "message to edit", std::move(query), + [this, reply_markup = std::move(reply_markup)](int64 chat_id, int64 message_id, + PromisedQueryPtr query) mutable { + send_request( + make_object(chat_id, message_id, std::move(reply_markup)), + std::make_unique(this, std::move(query))); + }); + }); + } + return Status::OK(); +} + +td::Status Client::process_delete_message_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto message_id = get_message_id(query.get()); + + if (chat_id.empty()) { + return Status::Error(400, "Chat identifier is not specified"); + } + + if (message_id == 0) { + return Status::Error(400, "Message identifier is not specified"); + } + + check_message(chat_id, message_id, false, AccessRights::Write, "message to delete", std::move(query), + [this](int64 chat_id, int64 message_id, PromisedQueryPtr query) { + delete_message(chat_id, message_id, false); + send_request(make_object(chat_id, td::vector{message_id}, true), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_set_game_score_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto message_id = get_message_id(query.get()); + TRY_RESULT(user_id, get_user_id(query.get())); + auto score = td::to_integer(query->arg("score")); + auto force = to_bool(query->arg("force")); + bool edit_message = true; + if (query->has_arg("disable_edit_message")) { + edit_message = !to_bool(query->arg("disable_edit_message")); + } else if (query->has_arg("edit_message")) { + edit_message = to_bool(query->arg("edit_message")); + } + + if (chat_id.empty() && message_id == 0) { + TRY_RESULT(inline_message_id, get_inline_message_id(query.get())); + check_user_no_fail( + user_id, std::move(query), + [this, inline_message_id = inline_message_id.str(), edit_message, user_id, score, + force](PromisedQueryPtr query) { + send_request(make_object(inline_message_id, edit_message, user_id, score, force), + std::make_unique(std::move(query))); + }); + } else { + check_message(chat_id, message_id, false, AccessRights::Edit, "message to set game score", std::move(query), + [this, user_id, score, force, edit_message](int64 chat_id, int64 message_id, PromisedQueryPtr query) { + check_user_no_fail( + user_id, std::move(query), + [this, chat_id, message_id, user_id, score, force, edit_message](PromisedQueryPtr query) { + send_request(make_object(chat_id, message_id, edit_message, user_id, + score, force), + std::make_unique(this, std::move(query))); + }); + }); + } + return Status::OK(); +} + +td::Status Client::process_get_game_high_scores_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto message_id = get_message_id(query.get()); + TRY_RESULT(user_id, get_user_id(query.get())); + + if (chat_id.empty() && message_id == 0) { + TRY_RESULT(inline_message_id, get_inline_message_id(query.get())); + check_user_no_fail(user_id, std::move(query), + [this, inline_message_id = inline_message_id.str(), user_id](PromisedQueryPtr query) { + send_request(make_object(inline_message_id, user_id), + std::make_unique(this, std::move(query))); + }); + } else { + check_message(chat_id, message_id, false, AccessRights::Read, "message to get game high scores", std::move(query), + [this, user_id](int64 chat_id, int64 message_id, PromisedQueryPtr query) { + check_user_no_fail( + user_id, std::move(query), [this, chat_id, message_id, user_id](PromisedQueryPtr query) { + send_request(make_object(chat_id, message_id, user_id), + std::make_unique(this, std::move(query))); + }); + }); + } + return Status::OK(); +} + +td::Status Client::process_answer_inline_query_query(PromisedQueryPtr &query) { + auto inline_query_id = td::to_integer(query->arg("inline_query_id")); + auto is_personal = to_bool(query->arg("is_personal")); + int32 cache_time = get_integer_arg(query.get(), "cache_time", 300, 0, 24 * 60 * 60); + auto next_offset = query->arg("next_offset"); + auto switch_pm_text = query->arg("switch_pm_text"); + auto switch_pm_parameter = query->arg("switch_pm_parameter"); + + TRY_RESULT(results, get_inline_query_results(query.get())); + + resolve_inline_query_results_bot_usernames( + std::move(results), std::move(query), + [this, inline_query_id, is_personal, cache_time, next_offset = next_offset.str(), + switch_pm_text = switch_pm_text.str(), switch_pm_parameter = switch_pm_parameter.str()]( + td::vector> results, PromisedQueryPtr query) { + send_request( + make_object(inline_query_id, is_personal, std::move(results), cache_time, + next_offset, switch_pm_text, switch_pm_parameter), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_answer_callback_query_query(PromisedQueryPtr &query) { + auto callback_query_id = td::to_integer(query->arg("callback_query_id")); + td::string text = query->arg("text").str(); + bool show_alert = to_bool(query->arg("show_alert")); + td::string url = query->arg("url").str(); + int32 cache_time = get_integer_arg(query.get(), "cache_time", 0, 0, 24 * 30 * 60 * 60); + + send_request(make_object(callback_query_id, text, show_alert, url, cache_time), + std::make_unique(std::move(query))); + return Status::OK(); +} + +td::Status Client::process_answer_shipping_query_query(PromisedQueryPtr &query) { + auto shipping_query_id = td::to_integer(query->arg("shipping_query_id")); + auto ok = to_bool(query->arg("ok")); + td::vector> shipping_options; + td::MutableSlice error_message; + if (ok) { + TRY_RESULT_ASSIGN(shipping_options, get_shipping_options(query.get())); + } else { + TRY_RESULT_ASSIGN(error_message, get_required_string_arg(query.get(), "error_message")); + } + send_request( + make_object(shipping_query_id, std::move(shipping_options), error_message.str()), + std::make_unique(std::move(query))); + return Status::OK(); +} + +td::Status Client::process_answer_pre_checkout_query_query(PromisedQueryPtr &query) { + auto pre_checkout_query_id = td::to_integer(query->arg("pre_checkout_query_id")); + auto ok = to_bool(query->arg("ok")); + td::MutableSlice error_message; + if (!ok) { + TRY_RESULT_ASSIGN(error_message, get_required_string_arg(query.get(), "error_message")); + } + + send_request(make_object(pre_checkout_query_id, error_message.str()), + std::make_unique(std::move(query))); + return Status::OK(); +} + +td::Status Client::process_export_chat_invite_link_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + + check_chat(chat_id, AccessRights::Write, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) { + send_request(make_object(chat_id), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_get_chat_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + + check_chat(chat_id, AccessRights::Read, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) { + auto chat_info = get_chat(chat_id); + CHECK(chat_info != nullptr); + switch (chat_info->type) { + case ChatInfo::Type::Private: + return send_request(make_object(chat_info->user_id), + std::make_unique(this, chat_id, std::move(query))); + case ChatInfo::Type::Group: + return send_request(make_object(chat_info->group_id), + std::make_unique(this, chat_id, std::move(query))); + case ChatInfo::Type::Supergroup: + return send_request(make_object(chat_info->supergroup_id), + std::make_unique(this, chat_id, std::move(query))); + case ChatInfo::Type::Unknown: + default: + UNREACHABLE(); + } + }); + return Status::OK(); +} + +td::Status Client::process_set_chat_photo_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto photo = get_input_file(query.get(), "photo", true); + if (photo == nullptr) { + if (query->arg("photo").empty()) { + return Status::Error(400, "There is no photo in the request"); + } + return Status::Error(400, "Photo must be uploaded as an InputFile"); + } + + check_chat(chat_id, AccessRights::Write, std::move(query), + [this, photo = std::move(photo)](int64 chat_id, PromisedQueryPtr query) mutable { + send_request(make_object( + chat_id, make_object(std::move(photo))), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_delete_chat_photo_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + + check_chat(chat_id, AccessRights::Write, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) { + send_request(make_object(chat_id, nullptr), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_set_chat_title_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto title = query->arg("title"); + + check_chat(chat_id, AccessRights::Write, std::move(query), + [this, title = title.str()](int64 chat_id, PromisedQueryPtr query) { + send_request(make_object(chat_id, title), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_set_chat_permissions_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + bool allow_legacy = false; + TRY_RESULT(permissions, get_chat_permissions(query.get(), allow_legacy)); + CHECK(!allow_legacy); + + check_chat(chat_id, AccessRights::Write, std::move(query), + [this, permissions = std::move(permissions)](int64 chat_id, PromisedQueryPtr query) mutable { + send_request(make_object(chat_id, std::move(permissions)), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_set_chat_description_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto description = query->arg("description"); + + check_chat(chat_id, AccessRights::Write, std::move(query), + [this, description = description.str()](int64 chat_id, PromisedQueryPtr query) { + send_request(make_object(chat_id, description), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_pin_chat_message_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto message_id = get_message_id(query.get()); + auto disable_notification = to_bool(query->arg("disable_notification")); + + check_message(chat_id, message_id, false, AccessRights::Write, "message to pin", std::move(query), + [this, disable_notification](int64 chat_id, int64 message_id, PromisedQueryPtr query) { + send_request(make_object(chat_id, message_id, disable_notification, false), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_unpin_chat_message_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto message_id = get_message_id(query.get()); + + if (message_id == 0) { + check_chat(chat_id, AccessRights::Write, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) { + send_request(make_object(chat_id), + std::make_unique(this, chat_id, std::move(query))); + }); + } else { + check_message(chat_id, message_id, false, AccessRights::Write, "message to unpin", std::move(query), + [this](int64 chat_id, int64 message_id, PromisedQueryPtr query) { + send_request(make_object(chat_id, message_id), + std::make_unique(std::move(query))); + }); + } + return Status::OK(); +} + +td::Status Client::process_unpin_all_chat_messages_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + + check_chat(chat_id, AccessRights::Write, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) { + send_request(make_object(chat_id), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_set_chat_sticker_set_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + auto sticker_set_name = query->arg("sticker_set_name"); + + check_chat(chat_id, AccessRights::Write, std::move(query), + [this, sticker_set_name = sticker_set_name.str()](int64 chat_id, PromisedQueryPtr query) { + if (get_chat_type(chat_id) != ChatType::Supergroup) { + return fail_query(400, "Bad Request: method is available only for supergroups", std::move(query)); + } + + resolve_sticker_set( + sticker_set_name, std::move(query), [this, chat_id](int64 sticker_set_id, PromisedQueryPtr query) { + auto chat_info = get_chat(chat_id); + CHECK(chat_info != nullptr); + CHECK(chat_info->type == ChatInfo::Type::Supergroup); + send_request( + make_object(chat_info->supergroup_id, sticker_set_id), + std::make_unique(std::move(query))); + }); + }); + return Status::OK(); +} + +td::Status Client::process_delete_chat_sticker_set_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + + check_chat(chat_id, AccessRights::Write, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) { + if (get_chat_type(chat_id) != ChatType::Supergroup) { + return fail_query(400, "Bad Request: method is available only for supergroups", std::move(query)); + } + + auto chat_info = get_chat(chat_id); + CHECK(chat_info != nullptr); + CHECK(chat_info->type == ChatInfo::Type::Supergroup); + send_request(make_object(chat_info->supergroup_id, 0), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_get_chat_member_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + TRY_RESULT(user_id, get_user_id(query.get())); + + check_chat(chat_id, AccessRights::Read, std::move(query), [this, user_id](int64 chat_id, PromisedQueryPtr query) { + get_chat_member(chat_id, user_id, std::move(query), + [this, chat_type = get_chat_type(chat_id)](td_api::object_ptr &&chat_member, + PromisedQueryPtr query) { + answer_query(JsonChatMember(chat_member.get(), chat_type, this), std::move(query)); + }); + }); + return Status::OK(); +} + +td::Status Client::process_get_chat_administrators_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + + check_chat(chat_id, AccessRights::Read, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) { + auto chat_info = get_chat(chat_id); + CHECK(chat_info != nullptr); + switch (chat_info->type) { + case ChatInfo::Type::Private: + return fail_query(400, "Bad Request: there are no administrators in the private chat", std::move(query)); + case ChatInfo::Type::Group: { + auto group_info = get_group_info(chat_info->group_id); + CHECK(group_info != nullptr); + return send_request(make_object(chat_info->group_id), + std::make_unique(this, true, std::move(query))); + } + case ChatInfo::Type::Supergroup: + return send_request( + make_object( + chat_info->supergroup_id, make_object(), 0, 100), + std::make_unique(this, get_chat_type(chat_id), std::move(query))); + case ChatInfo::Type::Unknown: + default: + UNREACHABLE(); + } + }); + return Status::OK(); +} + +td::Status Client::process_get_chat_member_count_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + + check_chat(chat_id, AccessRights::Read, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) { + auto chat_info = get_chat(chat_id); + CHECK(chat_info != nullptr); + switch (chat_info->type) { + case ChatInfo::Type::Private: + return answer_query(td::VirtuallyJsonableInt(1 + (chat_info->user_id != my_id_)), std::move(query)); + case ChatInfo::Type::Group: { + auto group_info = get_group_info(chat_info->group_id); + CHECK(group_info != nullptr); + if (group_info->member_count == 0) { + return fail_query(403, "Forbidden: bot is not a member of the group chat", std::move(query)); + } + return answer_query(td::VirtuallyJsonableInt(group_info->member_count), std::move(query)); + } + case ChatInfo::Type::Supergroup: + return send_request(make_object(chat_info->supergroup_id), + std::make_unique(std::move(query))); + case ChatInfo::Type::Unknown: + default: + UNREACHABLE(); + } + }); + return Status::OK(); +} + +td::Status Client::process_leave_chat_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + + check_chat(chat_id, AccessRights::Read, std::move(query), [this](int64 chat_id, PromisedQueryPtr query) { + send_request(make_object(chat_id), std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_promote_chat_member_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + TRY_RESULT(user_id, get_user_id(query.get())); + auto can_change_info = to_bool(query->arg("can_change_info")); + auto can_post_messages = to_bool(query->arg("can_post_messages")); + auto can_edit_messages = to_bool(query->arg("can_edit_messages")); + auto can_delete_messages = to_bool(query->arg("can_delete_messages")); + auto can_invite_users = to_bool(query->arg("can_invite_users")); + auto can_restrict_members = to_bool(query->arg("can_restrict_members")); + auto can_pin_messages = to_bool(query->arg("can_pin_messages")); + auto can_promote_members = to_bool(query->arg("can_promote_members")); + auto is_anonymous = to_bool(query->arg("is_anonymous")); + auto status = make_object( + td::string(), true, can_change_info, can_post_messages, can_edit_messages, can_delete_messages, can_invite_users, + can_restrict_members, can_pin_messages, can_promote_members, is_anonymous); + check_chat(chat_id, AccessRights::Write, std::move(query), + [this, user_id, status = std::move(status)](int64 chat_id, PromisedQueryPtr query) mutable { + auto chat_info = get_chat(chat_id); + CHECK(chat_info != nullptr); + if (chat_info->type != ChatInfo::Type::Supergroup) { + return fail_query(400, "Bad Request: method is available for supergroup and channel chats only", + std::move(query)); + } + + get_chat_member( + chat_id, user_id, std::move(query), + [this, chat_id, user_id, status = std::move(status)]( + td_api::object_ptr &&chat_member, PromisedQueryPtr query) mutable { + if (chat_member->status_->get_id() == td_api::chatMemberStatusAdministrator::ID) { + auto administrator = + static_cast(chat_member->status_.get()); + status->custom_title_ = std::move(administrator->custom_title_); + } + + send_request(make_object(chat_id, user_id, std::move(status)), + std::make_unique(std::move(query))); + }); + }); + return Status::OK(); +} + +td::Status Client::process_set_chat_administrator_custom_title_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + TRY_RESULT(user_id, get_user_id(query.get())); + + check_chat(chat_id, AccessRights::Write, std::move(query), [this, user_id](int64 chat_id, PromisedQueryPtr query) { + if (get_chat_type(chat_id) != ChatType::Supergroup) { + return fail_query(400, "Bad Request: method is available only for supergroups", std::move(query)); + } + + get_chat_member( + chat_id, user_id, std::move(query), + [this, chat_id, user_id](td_api::object_ptr &&chat_member, PromisedQueryPtr query) { + if (chat_member->status_->get_id() == td_api::chatMemberStatusCreator::ID) { + return fail_query(400, "Bad Request: only creator can edit their custom title", std::move(query)); + } + if (chat_member->status_->get_id() != td_api::chatMemberStatusAdministrator::ID) { + return fail_query(400, "Bad Request: user is not an administrator", std::move(query)); + } + auto administrator = td_api::move_object_as(chat_member->status_); + if (!administrator->can_be_edited_) { + return fail_query(400, "Bad Request: not enough rights to change custom title of the user", + std::move(query)); + } + administrator->custom_title_ = query->arg("custom_title").str(); + + send_request(make_object(chat_id, user_id, std::move(administrator)), + std::make_unique(std::move(query))); + }); + }); + return Status::OK(); +} + +td::Status Client::process_ban_chat_member_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + TRY_RESULT(user_id, get_user_id(query.get())); + int32 until_date = get_integer_arg(query.get(), "until_date", 0); + + check_chat(chat_id, AccessRights::Write, std::move(query), + [this, user_id, until_date](int64 chat_id, PromisedQueryPtr query) { + check_user_no_fail( + user_id, std::move(query), [this, chat_id, user_id, until_date](PromisedQueryPtr query) { + send_request(make_object( + chat_id, user_id, make_object(until_date)), + std::make_unique(std::move(query))); + }); + }); + return Status::OK(); +} + +td::Status Client::process_restrict_chat_member_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + TRY_RESULT(user_id, get_user_id(query.get())); + int32 until_date = get_integer_arg(query.get(), "until_date", 0); + bool allow_legacy = true; + TRY_RESULT(permissions, get_chat_permissions(query.get(), allow_legacy)); + + check_chat(chat_id, AccessRights::Write, std::move(query), + [this, user_id, until_date, is_legacy = allow_legacy, permissions = std::move(permissions)]( + int64 chat_id, PromisedQueryPtr query) mutable { + if (get_chat_type(chat_id) != ChatType::Supergroup) { + return fail_query(400, "Bad Request: method is available only for supergroups", std::move(query)); + } + + get_chat_member( + chat_id, user_id, std::move(query), + [this, chat_id, user_id, until_date, is_legacy, permissions = std::move(permissions)]( + td_api::object_ptr &&chat_member, PromisedQueryPtr query) mutable { + if (is_legacy && chat_member->status_->get_id() == td_api::chatMemberStatusRestricted::ID) { + auto restricted = + static_cast(chat_member->status_.get()); + auto *old_permissions = restricted->permissions_.get(); + permissions->can_send_polls_ = old_permissions->can_send_polls_; + permissions->can_change_info_ = old_permissions->can_change_info_; + permissions->can_invite_users_ = old_permissions->can_invite_users_; + permissions->can_pin_messages_ = old_permissions->can_pin_messages_; + } + + send_request(make_object( + chat_id, user_id, + make_object( + is_chat_member(chat_member->status_), until_date, std::move(permissions))), + std::make_unique(std::move(query))); + }); + }); + return Status::OK(); +} + +td::Status Client::process_unban_chat_member_query(PromisedQueryPtr &query) { + auto chat_id = query->arg("chat_id"); + TRY_RESULT(user_id, get_user_id(query.get())); + auto only_if_banned = to_bool(query->arg("only_if_banned")); + + check_chat(chat_id, AccessRights::Write, std::move(query), + [this, user_id, only_if_banned](int64 chat_id, PromisedQueryPtr query) { + auto chat_info = get_chat(chat_id); + CHECK(chat_info != nullptr); + if (chat_info->type != ChatInfo::Type::Supergroup) { + return fail_query(400, "Bad Request: method is available for supergroup and channel chats only", + std::move(query)); + } + + if (only_if_banned) { + get_chat_member(chat_id, user_id, std::move(query), + [this, chat_id, user_id](td_api::object_ptr &&chat_member, + PromisedQueryPtr query) { + if (chat_member->status_->get_id() != td_api::chatMemberStatusBanned::ID) { + return answer_query(td::JsonTrue(), std::move(query)); + } + + send_request(make_object( + chat_id, user_id, make_object()), + std::make_unique(std::move(query))); + }); + } else { + check_user_no_fail(user_id, std::move(query), [this, chat_id, user_id](PromisedQueryPtr query) { + send_request(make_object(chat_id, user_id, + make_object()), + std::make_unique(std::move(query))); + }); + } + }); + return Status::OK(); +} + +td::Status Client::process_get_sticker_set_query(PromisedQueryPtr &query) { + auto name = query->arg("name"); + if (td::trim(to_lower(name)) == to_lower(GREAT_MINDS_SET_NAME)) { + send_request(make_object(GREAT_MINDS_SET_ID), + std::make_unique(this, true, std::move(query))); + } else { + send_request(make_object(name.str()), + std::make_unique(this, true, std::move(query))); + } + return Status::OK(); +} + +td::Status Client::process_upload_sticker_file_query(PromisedQueryPtr &query) { + TRY_RESULT(user_id, get_user_id(query.get())); + auto png_sticker = get_input_file(query.get(), "png_sticker"); + + check_user(user_id, std::move(query), + [this, user_id, png_sticker = std::move(png_sticker)](PromisedQueryPtr query) mutable { + send_request(make_object(user_id, std::move(png_sticker)), + std::make_unique(this, std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_create_new_sticker_set_query(PromisedQueryPtr &query) { + TRY_RESULT(user_id, get_user_id(query.get())); + auto name = query->arg("name"); + auto title = query->arg("title"); + auto is_masks = to_bool(query->arg("contains_masks")); + TRY_RESULT(stickers, get_input_stickers(query.get())); + + check_user(user_id, std::move(query), + [this, user_id, title, name, is_masks, stickers = std::move(stickers)](PromisedQueryPtr query) mutable { + send_request(make_object(user_id, title.str(), name.str(), is_masks, + std::move(stickers)), + std::make_unique(this, false, std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_add_sticker_to_set_query(PromisedQueryPtr &query) { + TRY_RESULT(user_id, get_user_id(query.get())); + auto name = query->arg("name"); + TRY_RESULT(stickers, get_input_stickers(query.get())); + CHECK(!stickers.empty()); + + check_user(user_id, std::move(query), + [this, user_id, name, sticker = std::move(stickers[0])](PromisedQueryPtr query) mutable { + send_request(make_object(user_id, name.str(), std::move(sticker)), + std::make_unique(this, false, std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_set_sticker_set_thumb_query(PromisedQueryPtr &query) { + TRY_RESULT(user_id, get_user_id(query.get())); + auto name = query->arg("name"); + auto thumbnail = get_input_file(query.get(), "thumb"); + check_user(user_id, std::move(query), + [this, user_id, name, thumbnail = std::move(thumbnail)](PromisedQueryPtr query) mutable { + send_request(make_object(user_id, name.str(), std::move(thumbnail)), + std::make_unique(this, false, std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_set_sticker_position_in_set_query(PromisedQueryPtr &query) { + auto file_id = trim(query->arg("sticker")); + if (file_id.empty()) { + return Status::Error(400, "Sticker is not specified"); + } + int32 position = get_integer_arg(query.get(), "position", -1); + + send_request( + make_object(make_object(file_id.str()), position), + std::make_unique(std::move(query))); + return Status::OK(); +} + +td::Status Client::process_delete_sticker_from_set_query(PromisedQueryPtr &query) { + auto file_id = trim(query->arg("sticker")); + if (file_id.empty()) { + return Status::Error(400, "Sticker is not specified"); + } + + send_request(make_object(make_object(file_id.str())), + std::make_unique(std::move(query))); + return Status::OK(); +} + +td::Status Client::process_set_passport_data_errors_query(PromisedQueryPtr &query) { + TRY_RESULT(user_id, get_user_id(query.get())); + TRY_RESULT(passport_element_errors, get_passport_element_errors(query.get())); + + check_user(user_id, std::move(query), + [this, user_id, errors = std::move(passport_element_errors)](PromisedQueryPtr query) mutable { + send_request(make_object(user_id, std::move(errors)), + std::make_unique(std::move(query))); + }); + return Status::OK(); +} + +td::Status Client::process_send_custom_request_query(PromisedQueryPtr &query) { + TRY_RESULT(method, get_required_string_arg(query.get(), "method")); + auto parameters = query->arg("parameters"); + send_request(make_object(method.str(), parameters.str()), + std::make_unique(std::move(query))); + return Status::OK(); +} + +td::Status Client::process_answer_custom_query_query(PromisedQueryPtr &query) { + auto custom_query_id = td::to_integer(query->arg("custom_query_id")); + auto data = query->arg("data"); + send_request(make_object(custom_query_id, data.str()), + std::make_unique(std::move(query))); + return Status::OK(); +} + +td::Status Client::process_get_updates_query(PromisedQueryPtr &query) { + if (!webhook_url_.empty() || webhook_set_query_) { + fail_query_conflict( + "Conflict: can't use getUpdates method while webhook is active; use deleteWebhook to delete the webhook first", + std::move(query)); + return Status::OK(); + } + int32 offset = get_integer_arg(query.get(), "offset", 0); + int32 limit = get_integer_arg(query.get(), "limit", 100, 1, 100); + int32 timeout = get_integer_arg(query.get(), "timeout", 0, 0, LONG_POLL_MAX_TIMEOUT); + + update_allowed_update_types(query.get()); + + auto now = td::Time::now_cached(); + if (offset == previous_get_updates_offset_ && timeout < 3 && now < previous_get_updates_start_date_ + 3.0) { + timeout = 3; + } + previous_get_updates_offset_ = offset; + previous_get_updates_start_date_ = now; + do_get_updates(offset, limit, timeout, std::move(query)); + return Status::OK(); +} + +td::Status Client::process_set_webhook_query(PromisedQueryPtr &query) { + Slice new_url; + if (query->method() == "setwebhook") { + new_url = query->arg("url"); + } + + auto now = td::Time::now_cached(); + if (!new_url.empty()) { + if (now < next_allowed_set_webhook_date_) { + query->set_retry_after_error(1); + return Status::OK(); + } + next_allowed_set_webhook_date_ = now + 1; + } + + // do not send warning just after webhook was deleted or set + next_bot_updates_warning_date_ = td::max(next_bot_updates_warning_date_, now + BOT_UPDATES_WARNING_DELAY); + + int32 new_max_connections = new_url.empty() ? 0 : get_webhook_max_connections(query.get()); + Slice new_ip_address = new_url.empty() ? Slice() : query->arg("ip_address"); + bool new_fix_ip_address = new_url.empty() ? false : get_webhook_fix_ip_address(query.get()); + bool drop_pending_updates = to_bool(query->arg("drop_pending_updates")); + if (webhook_set_query_) { + // already updating webhook. Cancel previous request + fail_query_conflict("Conflict: terminated by other setWebhook", std::move(webhook_set_query_)); + } else if (webhook_url_ == new_url && !has_webhook_certificate_ && query->file("certificate") == nullptr && + query->arg("certificate").empty() && new_max_connections == webhook_max_connections_ && + new_fix_ip_address == webhook_fix_ip_address_ && + (!new_fix_ip_address || new_ip_address == webhook_ip_address_) && !drop_pending_updates) { + if (update_allowed_update_types(query.get())) { + save_webhook(); + } else if (now > next_webhook_is_not_modified_warning_date_) { + next_webhook_is_not_modified_warning_date_ = now + 300; + LOG(WARNING) << "Webhook is not modified: \"" << new_url << '"'; + } + answer_query(td::JsonTrue(), std::move(query), + new_url.empty() ? Slice("Webhook is already deleted") : Slice("Webhook is already set")); + return Status::OK(); + } + + if (now > next_set_webhook_logging_date_ || webhook_url_ != new_url) { + next_set_webhook_logging_date_ = now + 300; + LOG(WARNING) << "Set webhook to " << new_url << ", max_connections = " << new_max_connections + << ", IP address = " << new_ip_address; + } + + if (!new_url.empty()) { + abort_long_poll(true); + } + + webhook_generation_++; + // need to close old webhook first + if (!webhook_url_.empty()) { + if (!webhook_id_.empty()) { + send_closure_later(std::move(webhook_id_), &WebhookActor::close); + } + + // wait for webhook_close callback + webhook_query_type_ = WebhookQueryType::Cancel; + webhook_set_query_ = std::move(query); + return Status::OK(); + } + do_set_webhook(std::move(query), false); + return Status::OK(); +} + +td::Status Client::process_get_webhook_info_query(PromisedQueryPtr &query) { + answer_query(JsonWebhookInfo(this), std::move(query)); + return Status::OK(); +} + +td::Status Client::process_get_file_query(PromisedQueryPtr &query) { + td::string file_id = query->arg("file_id").str(); + check_remote_file_id(file_id, std::move(query), [this](object_ptr file, PromisedQueryPtr query) { + do_get_file(std::move(file), std::move(query)); + }); + return Status::OK(); +} + +void Client::do_get_file(object_ptr file, PromisedQueryPtr query) { + if (!parameters_->local_mode_ && + td::max(file->expected_size_, file->local_->downloaded_size_) > MAX_DOWNLOAD_FILE_SIZE) { // speculative check + return fail_query(400, "Bad Request: file is too big", std::move(query)); + } + + auto file_id = file->id_; + file_download_listeners_[file_id].push_back(std::move(query)); + if (file->local_->is_downloading_completed_) { + Slice relative_path = td::PathView::relative(file->local_->path_, absolute_dir_, true); + if (!relative_path.empty()) { + auto r_stat = td::stat(file->local_->path_); + if (r_stat.is_ok() && r_stat.ok().is_reg_ && r_stat.ok().size_ == file->size_) { + return on_file_download(file_id, std::move(file)); + } + } + } + + send_request(make_object(file_id, 1, 0, 0, false), + std::make_unique(this, file_id)); +} + +bool Client::is_file_being_downloaded(int32 file_id) const { + return file_download_listeners_.count(file_id) > 0; +} + +void Client::on_file_download(int32 file_id, td::Result> r_file) { + auto it = file_download_listeners_.find(file_id); + if (it == file_download_listeners_.end()) { + return; + } + auto queries = std::move(it->second); + file_download_listeners_.erase(it); + download_started_file_ids_.erase(file_id); + for (auto &query : queries) { + if (r_file.is_error()) { + const auto &error = r_file.error(); + fail_query_with_error(std::move(query), error.code(), error.public_message()); + } else { + answer_query(JsonFile(r_file.ok().get(), this), std::move(query)); + } + } +} + +void Client::webhook_verified(td::string cached_ip_address) { + if (get_link_token() != webhook_generation_) { + return; + } + bool need_save = webhook_set_query_ || cached_ip_address != webhook_ip_address_; + webhook_ip_address_ = cached_ip_address; + if (webhook_set_query_) { + LOG(WARNING) << "Webhook verified"; + answer_query(td::JsonTrue(), std::move(webhook_set_query_), "Webhook was set"); + } + if (need_save) { + save_webhook(); + } +} + +void Client::save_webhook() const { + td::string value; + if (has_webhook_certificate_) { + value += "cert/"; + } + value += PSTRING() << "#maxc" << webhook_max_connections_ << '/'; + if (!webhook_ip_address_.empty()) { + value += PSTRING() << "#ip" << webhook_ip_address_ << '/'; + } + if (webhook_fix_ip_address_) { + value += "#fix_ip/"; + } + if (allowed_update_types_ != DEFAULT_ALLOWED_UPDATE_TYPES) { + value += PSTRING() << "#allow" << allowed_update_types_ << '/'; + } + value += webhook_url_; + LOG(INFO) << "Save webhook " << value; + parameters_->shared_data_->webhook_db_->set(bot_token_with_dc_, value); +} + +void Client::webhook_success() { + next_bot_updates_warning_date_ = td::Time::now() + BOT_UPDATES_WARNING_DELAY; + if (was_bot_updates_warning_) { + send_request(make_object(0, ""), std::make_unique()); + was_bot_updates_warning_ = false; + } +} + +void Client::webhook_error(Status status) { + CHECK(status.is_error()); + last_webhook_error_date_ = get_unix_time(); + last_webhook_error_ = std::move(status); + + auto pending_update_count = get_pending_update_count(); + if (pending_update_count >= MIN_PENDING_UPDATES_WARNING && td::Time::now() > next_bot_updates_warning_date_) { + send_request(make_object(td::narrow_cast(pending_update_count), + "Webhook error. " + last_webhook_error_.message().str()), + std::make_unique()); + next_bot_updates_warning_date_ = td::Time::now_cached() + BOT_UPDATES_WARNING_DELAY; + was_bot_updates_warning_ = true; + } +} + +void Client::webhook_closed(Status status) { + LOG(WARNING) << "Webhook closed: " << status + << ", webhook_query_type = " << (webhook_query_type_ == WebhookQueryType::Verify ? "verify" : "change"); + webhook_id_.release(); + webhook_url_ = td::string(); + if (has_webhook_certificate_) { + td::unlink(get_webhook_certificate_path()).ignore(); + has_webhook_certificate_ = false; + } + webhook_max_connections_ = 0; + webhook_ip_address_ = td::string(); + webhook_fix_ip_address_ = false; + webhook_set_date_ = td::Time::now(); + last_webhook_error_date_ = 0; + last_webhook_error_ = Status::OK(); + parameters_->shared_data_->webhook_db_->erase(bot_token_with_dc_); + + if (webhook_set_query_) { + if (webhook_query_type_ == WebhookQueryType::Verify) { + fail_query(400, PSLICE() << "Bad Request: bad webhook: " << status.message(), std::move(webhook_set_query_)); + } else { + do_set_webhook(std::move(webhook_set_query_), true); + } + } +} + +void Client::hangup_shared() { + webhook_closed(Status::Error("Unknown")); +} + +td::string Client::get_webhook_certificate_path() const { + return dir_ + "cert.pem"; +} + +td::int32 Client::get_webhook_max_connections(const Query *query) const { + auto default_value = parameters_->default_max_webhook_connections_; + auto max_value = parameters_->local_mode_ ? 100000 : 100; + return get_integer_arg(query, "max_connections", default_value, 1, max_value); +} + +bool Client::get_webhook_fix_ip_address(const Query *query) { + if (query->is_internal()) { + return query->has_arg("fix_ip_address"); + } + return !query->arg("ip_address").empty(); +} + +void Client::do_set_webhook(PromisedQueryPtr query, bool was_deleted) { + CHECK(webhook_url_.empty()); + if (to_bool(query->arg("drop_pending_updates"))) { + clear_tqueue(); + } + Slice new_url; + if (query->method() == "setwebhook") { + new_url = query->arg("url"); + } + if (!new_url.empty()) { + auto url = td::parse_url(new_url, td::HttpUrl::Protocol::Https); + if (url.is_error()) { + return fail_query(400, "Bad Request: invalid webhook URL specified", std::move(query)); + } + auto *cert_file_ptr = query->file("certificate"); + has_webhook_certificate_ = false; + if (cert_file_ptr != nullptr) { + auto size = cert_file_ptr->size; + if (size > MAX_CERTIFICATE_FILE_SIZE) { + return fail_query(400, PSLICE() << "Bad Request: certificate size is too big (" << size << " bytes)", + std::move(query)); + } + auto from_path = cert_file_ptr->temp_file_name; + auto to_path = get_webhook_certificate_path(); + auto status = td::copy_file(from_path, to_path, size); + if (status.is_error()) { + return fail_query(500, "Internal Server Error: failed to save certificate", std::move(query)); + } + has_webhook_certificate_ = true; + } + + if (query->is_internal() && query->arg("certificate") == "previous") { + has_webhook_certificate_ = true; + } + webhook_url_ = new_url.str(); + webhook_set_date_ = td::Time::now(); + webhook_max_connections_ = get_webhook_max_connections(query.get()); + webhook_ip_address_ = query->arg("ip_address").str(); + webhook_fix_ip_address_ = get_webhook_fix_ip_address(query.get()); + last_webhook_error_date_ = 0; + last_webhook_error_ = Status::OK(); + + update_allowed_update_types(query.get()); + + LOG(WARNING) << "Create " << (has_webhook_certificate_ ? "" : "not ") << "self signed webhook: " << url.ok(); + auto webhook_actor_name = PSTRING() << "Webhook " << url.ok(); + webhook_id_ = td::create_actor( + webhook_actor_name, actor_shared(this, webhook_generation_), tqueue_id_, url.move_as_ok(), + has_webhook_certificate_ ? get_webhook_certificate_path() : "", webhook_max_connections_, query->is_internal(), + webhook_ip_address_, webhook_fix_ip_address_, parameters_); + // wait for webhook verified or webhook callback + webhook_query_type_ = WebhookQueryType::Verify; + webhook_set_query_ = std::move(query); + } else { + answer_query(td::JsonTrue(), std::move(query), + was_deleted ? Slice("Webhook was deleted") : Slice("Webhook is already deleted")); + } +} + +void Client::do_send_message(object_ptr input_message_content, PromisedQueryPtr query) { + auto chat_id = query->arg("chat_id"); + auto reply_to_message_id = get_message_id(query.get(), "reply_to_message_id"); + auto allow_sending_without_reply = to_bool(query->arg("allow_sending_without_reply")); + auto disable_notification = to_bool(query->arg("disable_notification")); + auto r_reply_markup = get_reply_markup(query.get()); + if (r_reply_markup.is_error()) { + return fail_query_with_error(std::move(query), 400, r_reply_markup.error().message()); + } + auto reply_markup = r_reply_markup.move_as_ok(); + + resolve_reply_markup_bot_usernames( + std::move(reply_markup), std::move(query), + [this, chat_id = chat_id.str(), reply_to_message_id, allow_sending_without_reply, disable_notification, + input_message_content = std::move(input_message_content)](object_ptr reply_markup, + PromisedQueryPtr query) mutable { + auto on_success = [this, disable_notification, input_message_content = std::move(input_message_content), + reply_markup = std::move(reply_markup)](int64 chat_id, int64 reply_to_message_id, + PromisedQueryPtr query) mutable { + send_request(make_object(chat_id, 0, reply_to_message_id, + get_message_send_options(disable_notification), + std::move(reply_markup), std::move(input_message_content)), + std::make_unique(this, std::move(query))); + }; + check_message(chat_id, reply_to_message_id, reply_to_message_id <= 0 || allow_sending_without_reply, + AccessRights::Write, "reply message", std::move(query), std::move(on_success)); + }); +} + +td::int64 Client::get_send_message_query_id(PromisedQueryPtr query, bool is_multisend) { + auto query_id = current_send_message_query_id_++; + auto &pending_query = pending_send_message_queries_[query_id]; + pending_query.query = std::move(query); + pending_query.is_multisend = is_multisend; + return query_id; +} + +void Client::on_sent_message(object_ptr &&message, int64 query_id) { + CHECK(message != nullptr); + int64 chat_id = message->chat_id_; + int64 message_id = message->id_; + int64 reply_to_message_id = message->reply_to_message_id_; + if (reply_to_message_id > 0) { + CHECK(message->reply_in_chat_id_ == chat_id); + bool is_inserted = yet_unsent_reply_message_ids_[{chat_id, reply_to_message_id}].insert(message_id).second; + CHECK(is_inserted); + } + + FullMessageId yet_unsent_message_id{chat_id, message_id}; + YetUnsentMessage yet_unsent_message; + yet_unsent_message.reply_to_message_id = reply_to_message_id; + yet_unsent_message.send_message_query_id = query_id; + auto emplace_result = yet_unsent_messages_.emplace(yet_unsent_message_id, yet_unsent_message); + CHECK(emplace_result.second); + pending_send_message_queries_[query_id].awaited_messages++; +} + +void Client::abort_long_poll(bool from_set_webhook) { + if (long_poll_query_) { + Slice message; + if (from_set_webhook) { + message = Slice("Conflict: terminated by setWebhook request"); + } else { + message = + Slice("Conflict: terminated by other getUpdates request; make sure that only one bot instance is running"); + } + fail_query_conflict(message, std::move(long_poll_query_)); + } +} + +void Client::fail_query_conflict(Slice message, PromisedQueryPtr &&query) { + auto now = td::Time::now_cached(); + if (now >= next_conflict_response_date_) { + fail_query(409, message, std::move(query)); + next_conflict_response_date_ = now + 3.0; + } else { + td::create_actor( + "FailQueryConflictSleepActor", 3.0, + td::PromiseCreator::lambda([message = message.str(), query = std::move(query)](td::Result<> result) mutable { + fail_query(409, message, std::move(query)); + })) + .release(); + } +} + +class Client::JsonUpdates : public Jsonable { + public: + explicit JsonUpdates(td::Span updates) : updates_(updates) { + } + void store(JsonValueScope *scope) const { + auto array = scope->enter_array(); + int left_len = 1 << 22; + for (auto &update : updates_) { + left_len -= 50 + td::narrow_cast(update.data.size()); + if (left_len <= 0) { + break; + } + array << JsonUpdate(update.id.value(), update.data); + } + } + + private: + td::Span updates_; +}; + +void Client::do_get_updates(int32 offset, int32 limit, int32 timeout, PromisedQueryPtr query) { + auto &tqueue = parameters_->shared_data_->tqueue_; + LOG(DEBUG) << "Get updates with offset = " << offset << ", limit = " << limit << " and timeout = " << timeout; + LOG(DEBUG) << "Queue head = " << tqueue->get_head(tqueue_id_) << ", queue tail = " << tqueue->get_tail(tqueue_id_); + + if (offset <= 0) { + auto head = tqueue->get_head(tqueue_id_); + if (!head.empty() && offset < 0) { + // negative offset is counted from the end + auto tail = tqueue->get_tail(tqueue_id_); + CHECK(!tail.empty()); + offset += tail.value(); + } + if (offset < head.value()) { + offset = head.value(); + } + } + + auto updates = mutable_span(parameters_->shared_data_->event_buffer_, SharedData::TQUEUE_EVENT_BUFFER_SIZE); + updates.truncate(limit); + td::TQueue::EventId from; + size_t total_size = 0; + if (offset <= 0) { + // queue is not created yet + updates = {}; + } else { + bool is_ok = false; + auto r_offset = td::TQueue::EventId::from_int32(offset); + auto now = get_unix_time(); + if (r_offset.is_ok()) { + from = r_offset.ok(); + auto r_total_size = tqueue->get(tqueue_id_, from, true, now, updates); + if (r_total_size.is_ok()) { + is_ok = true; + total_size = r_total_size.move_as_ok(); + } + } + if (!is_ok) { + from = tqueue->get_head(tqueue_id_); + auto r_total_size = tqueue->get(tqueue_id_, from, true, now, updates); + CHECK(r_total_size.is_ok()); + total_size = r_total_size.move_as_ok(); + } + } + CHECK(total_size >= updates.size()); + total_size -= updates.size(); + + bool need_warning = false; + if (total_size <= MIN_PENDING_UPDATES_WARNING / 2) { + if (last_pending_update_count_ > MIN_PENDING_UPDATES_WARNING) { + need_warning = true; + last_pending_update_count_ = MIN_PENDING_UPDATES_WARNING; + } + } else if (total_size >= last_pending_update_count_) { + need_warning = true; + while (total_size >= last_pending_update_count_) { + last_pending_update_count_ *= 2; + } + } + if (need_warning) { + LOG(WARNING) << "Found " << updates.size() << " updates out of " << total_size << " + " << updates.size() + << " after last getUpdates call at " << previous_get_updates_finish_date_; + } else { + LOG(DEBUG) << "Found " << updates.size() << " updates out of " << total_size << " from " << from; + } + + if (timeout != 0 && updates.size() == 0) { + abort_long_poll(false); + long_poll_offset_ = offset; + long_poll_limit_ = limit; + long_poll_query_ = std::move(query); + long_poll_was_wakeup_ = false; + long_poll_hard_timeout_ = td::Time::now_cached() + timeout; + long_poll_slot_.set_event(td::EventCreator::raw(actor_id(), static_cast(0))); + long_poll_slot_.set_timeout_at(long_poll_hard_timeout_); + return; + } + previous_get_updates_finish_date_ = td::Clocks::system(); // local time to output it to the log + next_bot_updates_warning_date_ = td::Time::now() + BOT_UPDATES_WARNING_DELAY; + if (total_size == updates.size() && was_bot_updates_warning_) { + send_request(make_object(0, ""), std::make_unique()); + was_bot_updates_warning_ = false; + } + answer_query(JsonUpdates(updates), std::move(query)); +} + +void Client::long_poll_wakeup(bool force_flag) { + if (!long_poll_query_) { + auto pending_update_count = get_pending_update_count(); + if (pending_update_count >= MIN_PENDING_UPDATES_WARNING && td::Time::now() > next_bot_updates_warning_date_) { + send_request(make_object(td::narrow_cast(pending_update_count), + "The getUpdates method is not called for too long"), + std::make_unique()); + next_bot_updates_warning_date_ = + td::Time::now_cached() + BOT_UPDATES_WARNING_DELAY; // do not send warnings too often + was_bot_updates_warning_ = true; + } + return; + } + if (force_flag) { + do_get_updates(long_poll_offset_, long_poll_limit_, 0, std::move(long_poll_query_)); + } else { + double now = td::Time::now(); + if (!long_poll_was_wakeup_) { + long_poll_hard_timeout_ = td::min(now + LONG_POLL_MAX_DELAY, long_poll_hard_timeout_); + long_poll_was_wakeup_ = true; + } + double timeout = td::min(now + LONG_POLL_WAIT_AFTER, long_poll_hard_timeout_); + long_poll_slot_.set_event(td::EventCreator::raw(actor_id(), static_cast(0))); + long_poll_slot_.set_timeout_at(timeout); + } +} + +void Client::add_user(std::unordered_map &users, object_ptr &&user) { + auto user_info = &users[user->id_]; + user_info->first_name = user->first_name_; + user_info->last_name = user->last_name_; + user_info->username = user->username_; + user_info->language_code = user->language_code_; + + user_info->have_access = user->have_access_; + + switch (user->type_->get_id()) { + case td_api::userTypeRegular::ID: + user_info->type = UserInfo::Type::Regular; + break; + case td_api::userTypeBot::ID: { + user_info->type = UserInfo::Type::Bot; + auto *bot = static_cast(user->type_.get()); + user_info->can_join_groups = bot->can_join_groups_; + user_info->can_read_all_group_messages = bot->can_read_all_group_messages_; + user_info->is_inline_bot = bot->is_inline_; + break; + } + case td_api::userTypeDeleted::ID: + user_info->type = UserInfo::Type::Deleted; + break; + case td_api::userTypeUnknown::ID: + user_info->type = UserInfo::Type::Unknown; + break; + default: + UNREACHABLE(); + break; + } +} + +const Client::UserInfo *Client::get_user_info(int32 user_id) const { + auto it = users_.find(user_id); + return it == users_.end() ? nullptr : &it->second; +} + +void Client::set_user_bio(int32 user_id, td::string &&bio) { + auto user_info = &users_[user_id]; + user_info->bio = std::move(bio); +} + +void Client::add_group(std::unordered_map &groups, object_ptr &&group) { + auto group_info = &groups[group->id_]; + group_info->member_count = group->member_count_; + group_info->left = group->status_->get_id() == td_api::chatMemberStatusLeft::ID; + group_info->kicked = group->status_->get_id() == td_api::chatMemberStatusBanned::ID; + group_info->is_active = group->is_active_; + group_info->upgraded_to_supergroup_id = group->upgraded_to_supergroup_id_; +} + +const Client::GroupInfo *Client::get_group_info(int32 group_id) const { + auto it = groups_.find(group_id); + return it == groups_.end() ? nullptr : &it->second; +} + +void Client::set_group_description(int32 group_id, td::string &&descripton) { + auto group_info = &groups_[group_id]; + group_info->description = std::move(descripton); +} + +void Client::set_group_invite_link(int32 group_id, td::string &&invite_link) { + auto group_info = &groups_[group_id]; + group_info->invite_link = std::move(invite_link); +} + +void Client::add_supergroup(std::unordered_map &supergroups, + object_ptr &&supergroup) { + auto supergroup_info = &supergroups[supergroup->id_]; + supergroup_info->username = std::move(supergroup->username_); + supergroup_info->date = supergroup->date_; + supergroup_info->status = std::move(supergroup->status_); + supergroup_info->is_supergroup = !supergroup->is_channel_; + supergroup_info->has_location = supergroup->has_location_; +} + +void Client::set_supergroup_description(int32 supergroup_id, td::string &&descripton) { + auto supergroup_info = &supergroups_[supergroup_id]; + supergroup_info->description = std::move(descripton); +} + +void Client::set_supergroup_invite_link(int32 supergroup_id, td::string &&invite_link) { + auto supergroup_info = &supergroups_[supergroup_id]; + supergroup_info->invite_link = std::move(invite_link); +} + +void Client::set_supergroup_sticker_set_id(int32 supergroup_id, int64 sticker_set_id) { + auto supergroup_info = &supergroups_[supergroup_id]; + supergroup_info->sticker_set_id = sticker_set_id; +} + +void Client::set_supergroup_can_set_sticker_set(int32 supergroup_id, bool can_set_sticker_set) { + auto supergroup_info = &supergroups_[supergroup_id]; + supergroup_info->can_set_sticker_set = can_set_sticker_set; +} + +void Client::set_supergroup_slow_mode_delay(int32 supergroup_id, int32 slow_mode_delay) { + auto supergroup_info = &supergroups_[supergroup_id]; + supergroup_info->slow_mode_delay = slow_mode_delay; +} + +void Client::set_supergroup_linked_chat_id(int32 supergroup_id, int64 linked_chat_id) { + auto supergroup_info = &supergroups_[supergroup_id]; + supergroup_info->linked_chat_id = linked_chat_id; +} + +void Client::set_supergroup_location(int32 supergroup_id, object_ptr location) { + auto supergroup_info = &supergroups_[supergroup_id]; + supergroup_info->location = std::move(location); +} + +const Client::SupergroupInfo *Client::get_supergroup_info(int32 supergroup_id) const { + auto it = supergroups_.find(supergroup_id); + return it == supergroups_.end() ? nullptr : &it->second; +} + +Client::ChatInfo *Client::add_chat(int64 chat_id) { + LOG(DEBUG) << "Update chat " << chat_id; + return &chats_[chat_id]; +} + +const Client::ChatInfo *Client::get_chat(int64 chat_id) const { + auto it = chats_.find(chat_id); + if (it == chats_.end()) { + return nullptr; + } + return &it->second; +} + +Client::ChatType Client::get_chat_type(int64 chat_id) const { + auto chat_info = get_chat(chat_id); + if (chat_info == nullptr) { + return ChatType::Unknown; + } + switch (chat_info->type) { + case ChatInfo::Type::Private: + return ChatType::Private; + case ChatInfo::Type::Group: + return ChatType::Group; + case ChatInfo::Type::Supergroup: { + auto supergroup_info = get_supergroup_info(chat_info->supergroup_id); + if (supergroup_info == nullptr) { + return ChatType::Unknown; + } + if (supergroup_info->is_supergroup) { + return ChatType::Supergroup; + } else { + return ChatType::Channel; + } + } + case ChatInfo::Type::Unknown: + return ChatType::Unknown; + default: + UNREACHABLE(); + return ChatType::Unknown; + } +} + +td::string Client::get_chat_description(int64 chat_id) const { + auto chat_info = get_chat(chat_id); + if (chat_info == nullptr) { + return PSTRING() << "unknown chat " << chat_id; + } + switch (chat_info->type) { + case ChatInfo::Type::Private: { + auto user_info = get_user_info(chat_info->user_id); + return PSTRING() << "private " << (user_info == nullptr || !user_info->have_access ? "un" : "") + << "accessible chat " << chat_id; + } + case ChatInfo::Type::Group: { + auto group_info = get_group_info(chat_info->group_id); + if (group_info == nullptr) { + return PSTRING() << "unknown group chat " << chat_id; + } + return PSTRING() << (group_info->is_active ? "" : "in") << "active group chat " << chat_id << ", chat status = " + << (group_info->kicked ? "kicked" : (group_info->left ? "left" : "member")); + } + case ChatInfo::Type::Supergroup: { + auto supergroup_info = get_supergroup_info(chat_info->supergroup_id); + if (supergroup_info == nullptr) { + return PSTRING() << "unknown supergroup chat " << chat_id; + } + return PSTRING() << (supergroup_info->is_supergroup ? "supergroup" : "channel") << " chat " << chat_id + << ", chat status = " << to_string(supergroup_info->status) + << ", username = " << supergroup_info->username; + } + case ChatInfo::Type::Unknown: + return PSTRING() << "unknown chat " << chat_id; + default: + UNREACHABLE(); + return ""; + } +} + +void Client::json_store_file(td::JsonObjectScope &object, const td_api::file *file, bool with_path) const { + if (file->id_ == 0) { + return; + } + + LOG_IF(ERROR, file->remote_->id_.empty()) << "File remote identifier is empty: " << td::oneline(to_string(*file)); + + object("file_id", file->remote_->id_); + object("file_unique_id", file->remote_->unique_id_); + if (file->size_) { + object("file_size", file->size_); + } + if (with_path && file->local_->is_downloading_completed_) { + if (parameters_->local_mode_) { + if (td::check_utf8(file->local_->path_)) { + object("file_path", file->local_->path_); + } else { + object("file_path", td::JsonRawString(file->local_->path_)); + } + } else { + Slice relative_path = td::PathView::relative(file->local_->path_, absolute_dir_, true); + if (!relative_path.empty() && file->local_->downloaded_size_ <= MAX_DOWNLOAD_FILE_SIZE) { + object("file_path", relative_path); + } + } + } +} + +void Client::json_store_thumbnail(td::JsonObjectScope &object, const td_api::thumbnail *thumbnail) const { + if (thumbnail == nullptr || thumbnail->format_->get_id() == td_api::thumbnailFormatMpeg4::ID) { + return; + } + + CHECK(thumbnail->file_->id_ > 0); + object("thumb", JsonThumbnail(thumbnail, this)); +} + +void Client::json_store_callback_query_payload(td::JsonObjectScope &object, + const td_api::CallbackQueryPayload *payload) { + CHECK(payload != nullptr); + switch (payload->get_id()) { + case td_api::callbackQueryPayloadData::ID: { + auto data = static_cast(payload); + if (!td::check_utf8(data->data_)) { + LOG(WARNING) << "Receive non-UTF-8 callback query data"; + object("data", td::JsonRawString(data->data_)); + } else { + object("data", data->data_); + } + break; + } + case td_api::callbackQueryPayloadGame::ID: + object("game_short_name", static_cast(payload)->game_short_name_); + break; + case td_api::callbackQueryPayloadDataWithPassword::ID: + UNREACHABLE(); + break; + default: + UNREACHABLE(); + } +} + +void Client::json_store_permissions(td::JsonObjectScope &object, const td_api::chatPermissions *permissions) { + object("can_send_messages", td::JsonBool(permissions->can_send_messages_)); + object("can_send_media_messages", td::JsonBool(permissions->can_send_media_messages_)); + object("can_send_polls", td::JsonBool(permissions->can_send_polls_)); + object("can_send_other_messages", td::JsonBool(permissions->can_send_other_messages_)); + object("can_add_web_page_previews", td::JsonBool(permissions->can_add_web_page_previews_)); + object("can_change_info", td::JsonBool(permissions->can_change_info_)); + object("can_invite_users", td::JsonBool(permissions->can_invite_users_)); + object("can_pin_messages", td::JsonBool(permissions->can_pin_messages_)); +} + +Client::Slice Client::get_update_type_name(UpdateType update_type) { + switch (update_type) { + case UpdateType::Message: + return Slice("message"); + case UpdateType::EditedMessage: + return Slice("edited_message"); + case UpdateType::ChannelPost: + return Slice("channel_post"); + case UpdateType::EditedChannelPost: + return Slice("edited_channel_post"); + case UpdateType::InlineQuery: + return Slice("inline_query"); + case UpdateType::ChosenInlineResult: + return Slice("chosen_inline_result"); + case UpdateType::CallbackQuery: + return Slice("callback_query"); + case UpdateType::CustomEvent: + return Slice("custom_event"); + case UpdateType::CustomQuery: + return Slice("custom_query"); + case UpdateType::ShippingQuery: + return Slice("shipping_query"); + case UpdateType::PreCheckoutQuery: + return Slice("pre_checkout_query"); + case UpdateType::Poll: + return Slice("poll"); + case UpdateType::PollAnswer: + return Slice("poll_answer"); + default: + UNREACHABLE(); + return Slice(); + } +} + +td::uint32 Client::get_allowed_update_types(td::MutableSlice allowed_updates, bool is_internal) { + if (allowed_updates.empty()) { + return 0; + } + + LOG(INFO) << "Parsing JSON object: " << allowed_updates; + auto r_value = json_decode(allowed_updates); + if (r_value.is_error()) { + LOG(INFO) << "Can't parse JSON object: " << r_value.error(); + return 0; + } + + td::uint32 result = 0; + auto value = r_value.move_as_ok(); + if (value.type() != JsonValue::Type::Array) { + if (value.type() == JsonValue::Type::Number && is_internal) { + auto r_number = td::to_integer_safe(value.get_number()); + if (r_number.is_ok() && r_number.ok() > 0) { + return r_number.ok(); + } + } + return 0; + } + for (auto &update_type_name : value.get_array()) { + if (update_type_name.type() != JsonValue::Type::String) { + return 0; + } + auto type_name = update_type_name.get_string(); + to_lower_inplace(type_name); + for (int32 i = 0; i < static_cast(UpdateType::Size); i++) { + if (get_update_type_name(static_cast(i)) == type_name) { + result |= (1 << i); + } + } + } + + if (result == 0) { + return DEFAULT_ALLOWED_UPDATE_TYPES; + } + return result; +} + +bool Client::update_allowed_update_types(const Query *query) { + auto allowed_update_types = get_allowed_update_types(query->arg("allowed_updates"), query->is_internal()); + if (allowed_update_types != 0 && allowed_update_types != allowed_update_types_) { + allowed_update_types_ = allowed_update_types; + object_ptr value; + if (allowed_update_types == DEFAULT_ALLOWED_UPDATE_TYPES) { + value = make_object(); + } else { + value = make_object(allowed_update_types); + } + send_request(make_object("xallowed_update_types", std::move(value)), + std::make_unique()); + return true; + } + return false; +} + +template +class UpdateJsonable : public td::VirtuallyJsonable { + public: + explicit UpdateJsonable(const T &update) : update(update) { + } + void store(JsonValueScope *scope) const override { + *scope << update; + } + + private: + const T &update; +}; + +template +void Client::add_update(UpdateType update_type, const T &update, int32 timeout, int64 webhook_queue_id) { + add_update_impl(update_type, UpdateJsonable(update), timeout, webhook_queue_id); +} + +void Client::add_update_impl(UpdateType update_type, const td::VirtuallyJsonable &update, int32 timeout, + int64 webhook_queue_id) { + if (((allowed_update_types_ >> static_cast(update_type)) & 1) == 0) { + return; + } + + send_closure(stat_actor_, &BotStatActor::add_event, ServerBotStat::Update{}, td::Time::now()); + + const size_t BUF_SIZE = 1 << 16; + auto buf = td::StackAllocator::alloc(BUF_SIZE); + td::JsonBuilder jb(td::StringBuilder(buf.as_slice(), true)); + jb.enter_value() << get_update_type_name(update_type); + jb.string_builder() << ":"; + jb.enter_value() << update; + if (jb.string_builder().is_error()) { + LOG(ERROR) << "JSON buffer overflow"; + return; + } + + auto update_slice = jb.string_builder().as_cslice(); + auto r_id = parameters_->shared_data_->tqueue_->push(tqueue_id_, update_slice.str(), get_unix_time() + timeout, + webhook_queue_id, td::TQueue::EventId()); + if (r_id.is_ok()) { + auto id = r_id.move_as_ok(); + LOG(DEBUG) << "Update " << id << " was added for " << timeout << " seconds: " << update_slice; + if (webhook_url_.empty()) { + long_poll_wakeup(false); + } else { + send_closure(webhook_id_, &WebhookActor::update); + } + } else { + LOG(DEBUG) << "Update failed to be added with error " << r_id.error() << " for " << timeout + << " seconds: " << update_slice; + } +} + +void Client::add_new_message(object_ptr &&message, bool is_edited) { + CHECK(message != nullptr); + + if (message->sending_state_ != nullptr) { + return; + } + + auto chat_id = message->chat_id_; + if (chat_id == 0) { + LOG(ERROR) << "Receive invalid chat in " << to_string(message); + return; + } + new_message_queues_[chat_id].queue_.emplace(std::move(message), is_edited); + process_new_message_queue(chat_id); +} + +void Client::add_update_poll(object_ptr &&update) { + CHECK(update != nullptr); + add_update(UpdateType::Poll, JsonPoll(update->poll_.get(), this), 86400, update->poll_->id_); +} + +void Client::add_update_poll_answer(object_ptr &&update) { + CHECK(update != nullptr); + add_update(UpdateType::PollAnswer, JsonPollAnswer(update.get(), this), 86400, update->poll_id_); +} + +void Client::add_new_inline_query(int64 inline_query_id, int32 sender_user_id, object_ptr location, + const td::string &query, const td::string &offset) { + add_update(UpdateType::InlineQuery, + JsonInlineQuery(inline_query_id, sender_user_id, location.get(), query, offset, this), 30, + sender_user_id + (static_cast(1) << 33)); +} + +void Client::add_new_chosen_inline_result(int32 sender_user_id, object_ptr location, + const td::string &query, const td::string &result_id, + const td::string &inline_message_id) { + add_update(UpdateType::ChosenInlineResult, + JsonChosenInlineResult(sender_user_id, location.get(), query, result_id, inline_message_id, this), 600, + sender_user_id + (static_cast(2) << 33)); +} + +void Client::add_new_callback_query(object_ptr &&query) { + CHECK(query != nullptr); + auto user_id = query->sender_user_id_; + if (user_id == 0) { + LOG(ERROR) << "Receive invalid sender in " << to_string(query); + return; + } + new_callback_query_queues_[user_id].queue_.push(std::move(query)); + process_new_callback_query_queue(user_id, 0); +} + +void Client::process_new_callback_query_queue(int32 user_id, int state) { + auto &queue = new_callback_query_queues_[user_id]; + if (queue.has_active_request_) { + CHECK(state == 0); + return; + } + if (logging_out_ || closing_) { + new_callback_query_queues_.erase(user_id); + return; + } + while (!queue.queue_.empty()) { + auto &query = queue.queue_.front(); + int64 chat_id = query->chat_id_; + int64 message_id = query->message_id_; + auto message_info = get_message(chat_id, message_id); + // callback message can be already deleted in the bot outbox + if (state == 0) { + if (message_info == nullptr) { + // get the message from the server + queue.has_active_request_ = true; + return send_request(make_object(chat_id, message_id, query->id_), + std::make_unique(this, user_id, state)); + } + state = 1; + } + if (state == 1) { + auto reply_to_message_id = + message_info == nullptr || message_info->is_reply_to_message_deleted ? 0 : message_info->reply_to_message_id; + if (reply_to_message_id > 0 && get_message(chat_id, reply_to_message_id) == nullptr) { + queue.has_active_request_ = true; + return send_request(make_object(chat_id, message_id), + std::make_unique(this, user_id, state)); + } + state = 2; + } + if (state == 2) { + auto message_sticker_set_id = message_info == nullptr ? 0 : get_sticker_set_id(message_info->content); + if (!have_sticker_set_name(message_sticker_set_id)) { + queue.has_active_request_ = true; + return send_request(make_object(message_sticker_set_id), + std::make_unique(this, message_sticker_set_id, user_id, 0)); + } + auto reply_to_message_id = + message_info == nullptr || message_info->is_reply_to_message_deleted ? 0 : message_info->reply_to_message_id; + if (reply_to_message_id > 0) { + auto reply_to_message_info = get_message(chat_id, reply_to_message_id); + auto reply_sticker_set_id = + reply_to_message_info == nullptr ? 0 : get_sticker_set_id(reply_to_message_info->content); + if (!have_sticker_set_name(reply_sticker_set_id)) { + queue.has_active_request_ = true; + return send_request(make_object(reply_sticker_set_id), + std::make_unique(this, reply_sticker_set_id, user_id, 0)); + } + } + } + CHECK(state == 2); + + CHECK(user_id == query->sender_user_id_); + add_update(UpdateType::CallbackQuery, + JsonCallbackQuery(query->id_, user_id, chat_id, message_id, message_info, query->chat_instance_, + query->payload_.get(), this), + 150, user_id + (static_cast(3) << 33)); + + queue.queue_.pop(); + } + new_callback_query_queues_.erase(user_id); +} + +void Client::add_new_inline_callback_query(object_ptr &&query) { + CHECK(query != nullptr); + add_update(UpdateType::CallbackQuery, + JsonInlineCallbackQuery(query->id_, query->sender_user_id_, query->inline_message_id_, + query->chat_instance_, query->payload_.get(), this), + 150, query->sender_user_id_ + (static_cast(3) << 33)); +} + +void Client::add_new_shipping_query(object_ptr &&query) { + CHECK(query != nullptr); + add_update(UpdateType::ShippingQuery, JsonShippingQuery(query.get(), this), 150, + query->sender_user_id_ + (static_cast(4) << 33)); +} + +void Client::add_new_pre_checkout_query(object_ptr &&query) { + CHECK(query != nullptr); + add_update(UpdateType::PreCheckoutQuery, JsonPreCheckoutQuery(query.get(), this), 150, + query->sender_user_id_ + (static_cast(4) << 33)); +} + +void Client::add_new_custom_event(object_ptr &&event) { + CHECK(event != nullptr); + add_update(UpdateType::CustomEvent, JsonCustomJson(event->event_), 600, 0); +} + +void Client::add_new_custom_query(object_ptr &&query) { + CHECK(query != nullptr); + int32 timeout = query->timeout_ <= 0 ? 86400 : query->timeout_; + add_update(UpdateType::CustomQuery, JsonCustomJson(query->data_), timeout, 0); +} + +td::int32 Client::choose_added_member_id(const td_api::messageChatAddMembers *message_add_members) const { + CHECK(message_add_members != nullptr); + for (auto &member_user_id : message_add_members->member_user_ids_) { + if (member_user_id == my_id_) { + return my_id_; + } + } + if (message_add_members->member_user_ids_.empty()) { + return 0; + } + return message_add_members->member_user_ids_[0]; +} + +bool Client::need_skip_update_message(int64 chat_id, const object_ptr &message, bool is_edited) const { + auto chat = get_chat(chat_id); + CHECK(chat != nullptr); + if (message->is_outgoing_) { + switch (message->content_->get_id()) { + case td_api::messageChatChangeTitle::ID: + case td_api::messageChatChangePhoto::ID: + case td_api::messageChatDeletePhoto::ID: + case td_api::messageChatDeleteMember::ID: + case td_api::messagePinMessage::ID: + case td_api::messageProximityAlertTriggered::ID: + // don't skip + break; + default: + return true; + } + } + + int32 message_date = message->edit_date_ == 0 ? message->date_ : message->edit_date_; + if (message_date <= get_unix_time() - 86400) { + // don't send messages received/edited more than 1 day ago + return true; + } + + if (chat->type == ChatInfo::Type::Supergroup) { + auto supergroup_info = get_supergroup_info(chat->supergroup_id); + if (supergroup_info->status->get_id() == td_api::chatMemberStatusLeft::ID || + supergroup_info->status->get_id() == td_api::chatMemberStatusBanned::ID) { + // if we have left the chat, send only update about leaving the supergroup + if (message->content_->get_id() == td_api::messageChatDeleteMember::ID) { + auto user_id = static_cast(message->content_.get())->user_id_; + return user_id != my_id_; + } + return true; + } + + if (supergroup_info->date > message->date_ || authorization_date_ > message->date_) { + // don't send messages received before join or getting authorization + return true; + } + } + + if (message->ttl_ > 0) { + return true; + } + + switch (message->content_->get_id()) { + case td_api::messagePhoto::ID: { + auto message_photo = static_cast(message->content_.get()); + if (message_photo->photo_ == nullptr) { + LOG(ERROR) << "Got empty messagePhoto"; + return true; + } + break; + } + case td_api::messageChatAddMembers::ID: { + auto message_add_members = static_cast(message->content_.get()); + if (message_add_members->member_user_ids_.empty()) { + LOG(ERROR) << "Got empty messageChatAddMembers"; + return true; + } + break; + } + case td_api::messageChatChangePhoto::ID: { + auto message_change_photo = static_cast(message->content_.get()); + if (message_change_photo->photo_ == nullptr) { + LOG(ERROR) << "Got empty messageChatChangePhoto"; + return true; + } + break; + } + case td_api::messageSupergroupChatCreate::ID: { + if (chat->type != ChatInfo::Type::Supergroup) { + LOG(ERROR) << "Receive messageSupergroupChatCreate in the non-supergroup chat " << chat_id; + return true; + } + break; + } + case td_api::messagePinMessage::ID: { + auto message_pin_message = static_cast(message->content_.get()); + auto pinned_message_id = message_pin_message->message_id_; + if (pinned_message_id <= 0) { + return true; + } + const MessageInfo *pinned_message = get_message(chat_id, pinned_message_id); + if (pinned_message == nullptr) { + LOG(WARNING) << "Pinned unknown, inaccessible or deleted message " << pinned_message_id; + return true; + } + break; + } + case td_api::messageProximityAlertTriggered::ID: { + auto proximity_alert_triggered = + static_cast(message->content_.get()); + return proximity_alert_triggered->traveler_->get_id() != td_api::messageSenderUser::ID || + proximity_alert_triggered->watcher_->get_id() != td_api::messageSenderUser::ID; + } + case td_api::messageGameScore::ID: + return true; + case td_api::messagePaymentSuccessful::ID: + return true; + case td_api::messagePassportDataSent::ID: + return true; + case td_api::messageCall::ID: + return true; + case td_api::messageUnsupported::ID: + return true; + case td_api::messageContactRegistered::ID: + return true; + case td_api::messageExpiredPhoto::ID: + return true; + case td_api::messageExpiredVideo::ID: + return true; + case td_api::messageCustomServiceAction::ID: + return true; + default: + break; + } + + if (is_edited) { + const MessageInfo *old_message = get_message(chat_id, message->id_); + if (old_message != nullptr && !old_message->is_content_changed) { + return true; + } + } + + return false; +} + +td::int64 &Client::get_reply_to_message_id(object_ptr &message) { + if (message->content_->get_id() == td_api::messagePinMessage::ID) { + CHECK(message->reply_to_message_id_ == 0); + return static_cast(message->content_.get())->message_id_; + } + if (message->reply_in_chat_id_ != message->chat_id_ && message->reply_to_message_id_ != 0) { + LOG(WARNING) << "Drop reply to message " << message->id_ << " in chat " << message->chat_id_ + << " from another chat " << message->reply_in_chat_id_; + message->reply_in_chat_id_ = 0; + message->reply_to_message_id_ = 0; + } + return message->reply_to_message_id_; +} + +void Client::set_message_reply_to_message_id(MessageInfo *message_info, int64 reply_to_message_id) { + if (message_info->reply_to_message_id == reply_to_message_id) { + return; + } + + if (message_info->reply_to_message_id > 0) { + LOG_IF(ERROR, reply_to_message_id > 0) + << "Message " << message_info->id << " in chat " << message_info->chat_id + << " has changed reply_to_message from " << message_info->reply_to_message_id << " to " << reply_to_message_id; + auto it = reply_message_ids_.find({message_info->chat_id, message_info->reply_to_message_id}); + if (it != reply_message_ids_.end()) { + it->second.erase(message_info->id); + if (it->second.empty()) { + reply_message_ids_.erase(it); + } + } + } + if (reply_to_message_id > 0) { + reply_message_ids_[{message_info->chat_id, reply_to_message_id}].insert(message_info->id); + } + + message_info->reply_to_message_id = reply_to_message_id; +} + +td::CSlice Client::get_callback_data(const object_ptr &type) { + CHECK(type != nullptr); + switch (type->get_id()) { + case td_api::inlineKeyboardButtonTypeCallback::ID: + return static_cast(type.get())->data_; + case td_api::inlineKeyboardButtonTypeCallbackWithPassword::ID: + return static_cast(type.get())->data_; + default: + UNREACHABLE(); + return td::CSlice(); + } +} + +bool Client::are_equal_inline_keyboard_buttons(const td_api::inlineKeyboardButton *lhs, + const td_api::inlineKeyboardButton *rhs) { + CHECK(lhs != nullptr); + CHECK(rhs != nullptr); + if (lhs->text_ != rhs->text_) { + return false; + } + if (lhs->type_->get_id() != rhs->type_->get_id()) { + return false; + } + switch (lhs->type_->get_id()) { + case td_api::inlineKeyboardButtonTypeUrl::ID: { + auto lhs_type = static_cast(lhs->type_.get()); + auto rhs_type = static_cast(rhs->type_.get()); + return lhs_type->url_ == rhs_type->url_; + } + case td_api::inlineKeyboardButtonTypeLoginUrl::ID: { + auto lhs_type = static_cast(lhs->type_.get()); + auto rhs_type = static_cast(rhs->type_.get()); + return lhs_type->url_ == rhs_type->url_; // don't compare id_ and forward_text_ + } + case td_api::inlineKeyboardButtonTypeCallback::ID: + case td_api::inlineKeyboardButtonTypeCallbackWithPassword::ID: + return get_callback_data(lhs->type_) == get_callback_data(rhs->type_); + case td_api::inlineKeyboardButtonTypeCallbackGame::ID: + return true; + case td_api::inlineKeyboardButtonTypeSwitchInline::ID: { + auto lhs_type = static_cast(lhs->type_.get()); + auto rhs_type = static_cast(rhs->type_.get()); + return lhs_type->query_ == rhs_type->query_ && lhs_type->in_current_chat_ == rhs_type->in_current_chat_; + } + case td_api::inlineKeyboardButtonTypeBuy::ID: + return true; + default: + UNREACHABLE(); + return false; + } +} + +bool Client::are_equal_inline_keyboards(const td_api::replyMarkupInlineKeyboard *lhs, + const td_api::replyMarkupInlineKeyboard *rhs) { + CHECK(lhs != nullptr); + CHECK(rhs != nullptr); + auto &old_rows = lhs->rows_; + auto &new_rows = rhs->rows_; + if (old_rows.size() != new_rows.size()) { + return false; + } + for (size_t i = 0; i < old_rows.size(); i++) { + if (old_rows[i].size() != new_rows[i].size()) { + return false; + } + for (size_t j = 0; j < old_rows[i].size(); j++) { + if (!are_equal_inline_keyboard_buttons(old_rows[i][j].get(), new_rows[i][j].get())) { + return false; + } + } + } + return true; +} + +void Client::set_message_reply_markup(MessageInfo *message_info, object_ptr &&reply_markup) { + if (reply_markup != nullptr && reply_markup->get_id() != td_api::replyMarkupInlineKeyboard::ID) { + reply_markup = nullptr; + } + if (reply_markup == nullptr && message_info->reply_markup == nullptr) { + return; + } + if (reply_markup != nullptr && message_info->reply_markup != nullptr) { + CHECK(message_info->reply_markup->get_id() == td_api::replyMarkupInlineKeyboard::ID); + if (are_equal_inline_keyboards( + static_cast(message_info->reply_markup.get()), + static_cast(reply_markup.get()))) { + return; + } + } + message_info->reply_markup = std::move(reply_markup); + message_info->is_content_changed = true; +} + +td::int64 Client::get_sticker_set_id(const object_ptr &content) { + if (content->get_id() != td_api::messageSticker::ID) { + return 0; + } + + return static_cast(content.get())->sticker_->set_id_; +} + +bool Client::have_sticker_set_name(int64 sticker_set_id) const { + return sticker_set_id == 0 || sticker_set_names_.count(sticker_set_id) > 0; +} + +Client::Slice Client::get_sticker_set_name(int64 sticker_set_id) const { + auto it = sticker_set_names_.find(sticker_set_id); + if (it == sticker_set_names_.end()) { + return Slice(); + } + return it->second; +} + +void Client::process_new_message_queue(int64 chat_id) { + auto &queue = new_message_queues_[chat_id]; + if (queue.has_active_request_) { + return; + } + if (logging_out_ || closing_) { + new_message_queues_.erase(chat_id); + return; + } + while (!queue.queue_.empty()) { + auto &message_ref = queue.queue_.front().message; + CHECK(chat_id == message_ref->chat_id_); + int64 message_id = message_ref->id_; + int64 reply_to_message_id = get_reply_to_message_id(message_ref); + if (reply_to_message_id > 0 && get_message(chat_id, reply_to_message_id) == nullptr) { + queue.has_active_request_ = true; + return send_request(make_object(chat_id, message_id), + std::make_unique(this, chat_id)); + } + auto message_sticker_set_id = get_sticker_set_id(message_ref->content_); + if (!have_sticker_set_name(message_sticker_set_id)) { + queue.has_active_request_ = true; + return send_request(make_object(message_sticker_set_id), + std::make_unique(this, message_sticker_set_id, 0, chat_id)); + } + if (reply_to_message_id > 0) { + auto reply_to_message_info = get_message(chat_id, reply_to_message_id); + CHECK(reply_to_message_info != nullptr); + auto reply_sticker_set_id = get_sticker_set_id(reply_to_message_info->content); + if (!have_sticker_set_name(reply_sticker_set_id)) { + queue.has_active_request_ = true; + return send_request(make_object(reply_sticker_set_id), + std::make_unique(this, reply_sticker_set_id, 0, chat_id)); + } + } + + auto message = std::move(message_ref); + auto is_edited = queue.queue_.front().is_edited; + queue.queue_.pop(); + if (need_skip_update_message(chat_id, message, is_edited)) { + add_message(std::move(message)); + continue; + } + + auto chat = get_chat(chat_id); + CHECK(chat != nullptr); + bool is_channel_post = + (chat->type == ChatInfo::Type::Supergroup && !get_supergroup_info(chat->supergroup_id)->is_supergroup); + + UpdateType update_type; + if (is_channel_post) { + update_type = is_edited ? UpdateType::EditedChannelPost : UpdateType::ChannelPost; + } else { + update_type = is_edited ? UpdateType::EditedMessage : UpdateType::Message; + } + + int32 message_date = message->edit_date_ == 0 ? message->date_ : message->edit_date_; + auto now = get_unix_time(); + auto update_delay_time = now - td::max(message_date, parameters_->shared_data_->get_unix_time(webhook_set_date_)); + const auto UPDATE_DELAY_WARNING_TIME = 10 * 60; + LOG_IF(ERROR, update_delay_time > UPDATE_DELAY_WARNING_TIME) + << "Receive very old update " << get_update_type_name(update_type) << " sent at " << message_date << " to chat " + << chat_id << " with a delay of " << update_delay_time << " seconds: " << to_string(message); + auto left_time = message_date + 86400 - now; + add_message(std::move(message)); + + auto message_info = get_message(chat_id, message_id); + CHECK(message_info != nullptr); + + message_info->is_content_changed = false; + add_update(update_type, JsonMessage(message_info, true, get_update_type_name(update_type).str(), this), left_time, + chat_id); + } + new_message_queues_.erase(chat_id); +} + +void Client::remove_replies_to_message(int64 chat_id, int64 reply_to_message_id, bool only_from_cache) { + if (!only_from_cache) { + auto yet_unsent_it = yet_unsent_reply_message_ids_.find({chat_id, reply_to_message_id}); + if (yet_unsent_it != yet_unsent_reply_message_ids_.end()) { + for (auto message_id : yet_unsent_it->second) { + auto &message = yet_unsent_messages_[{chat_id, message_id}]; + CHECK(message.reply_to_message_id == reply_to_message_id); + message.is_reply_to_message_deleted = true; + } + } + } + + auto it = reply_message_ids_.find({chat_id, reply_to_message_id}); + if (it == reply_message_ids_.end()) { + return; + } + + if (!only_from_cache) { + for (auto message_id : it->second) { + auto message_info = get_message_editable(chat_id, message_id); + CHECK(message_info != nullptr); + CHECK(message_info->reply_to_message_id == reply_to_message_id); + message_info->reply_to_message_id = 0; + } + } + reply_message_ids_.erase(it); +} + +void Client::delete_message(int64 chat_id, int64 message_id, bool only_from_cache) { + remove_replies_to_message(chat_id, message_id, only_from_cache); + + auto it = messages_.find({chat_id, message_id}); + if (it == messages_.end()) { + if (yet_unsent_messages_.count({chat_id, message_id}) > 0) { + // yet unsent message is deleted, possible only if we are trying to write to inaccessible supergroup + auto chat_info = get_chat(chat_id); + CHECK(chat_info != nullptr); + + Status error; + if (chat_info->type != ChatInfo::Type::Supergroup) { + LOG(ERROR) << "Yet unsent message " << message_id << " is deleted in the chat " << chat_id; + error = Status::Error(403, "Forbidden: bot is not a member of the chat"); + } else { + auto supergroup_info = get_supergroup_info(chat_info->supergroup_id); + CHECK(supergroup_info != nullptr); + if (supergroup_info->is_supergroup) { + error = Status::Error(403, "Forbidden: bot is not a member of the supergroup chat"); + } else { + error = Status::Error(403, "Forbidden: bot is not a member of the channel chat"); + } + } + + on_message_send_failed(chat_id, message_id, 0, std::move(error)); + } + return; + } + + auto message_info = it->second.get(); + CHECK(message_info->lru_next != nullptr); + message_info->lru_next->lru_prev = message_info->lru_prev; + message_info->lru_prev->lru_next = message_info->lru_next; + + set_message_reply_to_message_id(message_info, 0); + + messages_.erase(it); +} + +void Client::schedule_next_delete_messages_lru() { + CHECK(!next_delete_messages_lru_timeout_.has_timeout()); + next_delete_messages_lru_timeout_.set_callback(Client::delete_messages_lru); + next_delete_messages_lru_timeout_.set_callback_data(static_cast(this)); + next_delete_messages_lru_timeout_.set_timeout_in(td::Random::fast(MESSAGES_CACHE_TIME, 2 * MESSAGES_CACHE_TIME)); +} + +void Client::delete_messages_lru(void *client_void) { + CHECK(client_void != nullptr); + auto client = static_cast(client_void); + + auto now = td::Time::now(); + int32 deleted_message_count = 0; + while (client->messages_lru_root_.lru_next->access_time < now - MESSAGES_CACHE_TIME) { + auto message = client->messages_lru_root_.lru_next; + if (client->yet_unsent_reply_message_ids_.count({message->chat_id, message->id})) { + LOG(DEBUG) << "Force usage of message " << message->id << " in " << message->chat_id; + client->update_message_lru(message); + } else { + client->delete_message(message->chat_id, message->id, true); + deleted_message_count++; + } + } + + if (deleted_message_count != 0) { + LOG(DEBUG) << "Delete " << deleted_message_count << " messages from cache"; + } + client->schedule_next_delete_messages_lru(); +} + +void Client::update_message_lru(const MessageInfo *message_info) const { + message_info->access_time = td::Time::now(); + if (message_info->lru_next != nullptr) { + message_info->lru_next->lru_prev = message_info->lru_prev; + message_info->lru_prev->lru_next = message_info->lru_next; + } + auto prev = messages_lru_root_.lru_prev; + message_info->lru_prev = prev; + prev->lru_next = message_info; + message_info->lru_next = &messages_lru_root_; + messages_lru_root_.lru_prev = message_info; +} + +Client::FullMessageId Client::add_message(object_ptr &&message, bool force_update_content) { + CHECK(message != nullptr); + CHECK(message->sending_state_ == nullptr); + + int64 chat_id = message->chat_id_; + int64 message_id = message->id_; + + LOG(DEBUG) << "Add message " << message_id << " to chat " << chat_id; + std::unique_ptr message_info; + auto it = messages_.find({chat_id, message_id}); + if (it == messages_.end()) { + message_info = std::make_unique(); + } else { + message_info = std::move(it->second); + } + + update_message_lru(message_info.get()); + + message_info->id = message_id; + message_info->chat_id = chat_id; + message_info->date = message->date_; + message_info->edit_date = message->edit_date_; + message_info->media_album_id = message->media_album_id_; + message_info->via_bot_user_id = message->via_bot_user_id_; + + CHECK(message->sender_ != nullptr); + switch (message->sender_->get_id()) { + case td_api::messageSenderUser::ID: { + auto sender = move_object_as(message->sender_); + message_info->sender_user_id = sender->user_id_; + CHECK(message_info->sender_user_id > 0); + break; + } + case td_api::messageSenderChat::ID: { + auto sender = move_object_as(message->sender_); + message_info->sender_chat_id = sender->chat_id_; + + auto chat_type = get_chat_type(chat_id); + if (chat_type != ChatType::Channel) { + if (message_info->sender_chat_id == chat_id) { + message_info->sender_user_id = group_anonymous_bot_user_id_; + } else { + message_info->sender_user_id = service_notifications_user_id_; + } + CHECK(message_info->sender_user_id > 0); + } + break; + } + default: + UNREACHABLE(); + } + + message_info->initial_chat_id = 0; + message_info->initial_sender_user_id = 0; + message_info->initial_sender_chat_id = 0; + message_info->initial_send_date = 0; + message_info->initial_message_id = 0; + message_info->initial_author_signature = td::string(); + message_info->initial_sender_name = td::string(); + if (message->forward_info_ != nullptr) { + message_info->initial_send_date = message->forward_info_->date_; + auto origin = std::move(message->forward_info_->origin_); + switch (origin->get_id()) { + case td_api::messageForwardOriginUser::ID: { + auto forward_info = move_object_as(origin); + message_info->initial_sender_user_id = forward_info->sender_user_id_; + break; + } + case td_api::messageForwardOriginChat::ID: { + auto forward_info = move_object_as(origin); + message_info->initial_sender_chat_id = forward_info->sender_chat_id_; + message_info->initial_author_signature = forward_info->author_signature_; + break; + } + case td_api::messageForwardOriginHiddenUser::ID: { + auto forward_info = move_object_as(origin); + message_info->initial_sender_name = forward_info->sender_name_; + break; + } + case td_api::messageForwardOriginChannel::ID: { + auto forward_info = move_object_as(origin); + message_info->initial_chat_id = forward_info->chat_id_; + message_info->initial_message_id = forward_info->message_id_; + message_info->initial_author_signature = forward_info->author_signature_; + break; + } + default: + UNREACHABLE(); + } + } + + message_info->author_signature = std::move(message->author_signature_); + + if (message->reply_in_chat_id_ != chat_id && message->reply_to_message_id_ != 0) { + LOG(WARNING) << "Drop reply to message " << message_id << " in chat " << chat_id << " from another chat " + << message->reply_in_chat_id_; + message->reply_in_chat_id_ = 0; + message->reply_to_message_id_ = 0; + } + set_message_reply_to_message_id(message_info.get(), message->reply_to_message_id_); + if (message_info->content == nullptr || force_update_content) { + message_info->content = std::move(message->content_); + message_info->is_content_changed = true; + + auto sticker_set_id = get_sticker_set_id(message_info->content); + if (!have_sticker_set_name(sticker_set_id)) { + send_request(make_object(sticker_set_id), + std::make_unique(this, sticker_set_id, 0, 0)); + } + } + set_message_reply_markup(message_info.get(), std::move(message->reply_markup_)); + + messages_[{chat_id, message_id}] = std::move(message_info); + message = nullptr; + + return {chat_id, message_id}; +} + +void Client::update_message_content(int64 chat_id, int64 message_id, object_ptr &&content) { + auto message_info = get_message_editable(chat_id, message_id); + if (message_info == nullptr) { + return; + } + LOG(DEBUG) << "Update content of the message " << message_id << " from chat " << chat_id; + + message_info->content = std::move(content); + message_info->is_content_changed = true; +} + +void Client::on_update_message_edited(int64 chat_id, int64 message_id, int32 edit_date, + object_ptr &&reply_markup) { + auto message_info = get_message_editable(chat_id, message_id); + if (message_info == nullptr) { + return; + } + message_info->edit_date = edit_date; + set_message_reply_markup(message_info, std::move(reply_markup)); +} + +const Client::MessageInfo *Client::get_message(int64 chat_id, int64 message_id) const { + auto it = messages_.find({chat_id, message_id}); + if (it == messages_.end()) { + LOG(DEBUG) << "Not found message " << message_id << " from chat " << chat_id; + return nullptr; + } + LOG(DEBUG) << "Found message " << message_id << " from chat " << chat_id; + + auto result = it->second.get(); + update_message_lru(result); + return result; +} + +Client::MessageInfo *Client::get_message_editable(int64 chat_id, int64 message_id) { + auto it = messages_.find({chat_id, message_id}); + if (it == messages_.end()) { + LOG(DEBUG) << "Not found message " << message_id << " from chat " << chat_id; + return nullptr; + } + LOG(DEBUG) << "Found message " << message_id << " from chat " << chat_id; + + auto result = it->second.get(); + update_message_lru(result); + return result; +} + +td::string Client::get_chat_member_status(const object_ptr &status) { + CHECK(status != nullptr); + switch (status->get_id()) { + case td_api::chatMemberStatusCreator::ID: + return "creator"; + case td_api::chatMemberStatusAdministrator::ID: + return "administrator"; + case td_api::chatMemberStatusMember::ID: + return "member"; + case td_api::chatMemberStatusRestricted::ID: + return "restricted"; + case td_api::chatMemberStatusLeft::ID: + return "left"; + case td_api::chatMemberStatusBanned::ID: + return "kicked"; + default: + UNREACHABLE(); + return ""; + } +} + +td::string Client::get_passport_element_type(int32 id) { + switch (id) { + case td_api::passportElementTypePersonalDetails::ID: + return "personal_details"; + case td_api::passportElementTypePassport::ID: + return "passport"; + case td_api::passportElementTypeDriverLicense::ID: + return "driver_license"; + case td_api::passportElementTypeIdentityCard::ID: + return "identity_card"; + case td_api::passportElementTypeInternalPassport::ID: + return "internal_passport"; + case td_api::passportElementTypeAddress::ID: + return "address"; + case td_api::passportElementTypeUtilityBill::ID: + return "utility_bill"; + case td_api::passportElementTypeBankStatement::ID: + return "bank_statement"; + case td_api::passportElementTypeRentalAgreement::ID: + return "rental_agreement"; + case td_api::passportElementTypePassportRegistration::ID: + return "passport_registration"; + case td_api::passportElementTypeTemporaryRegistration::ID: + return "temporary_registration"; + case td_api::passportElementTypePhoneNumber::ID: + return "phone_number"; + case td_api::passportElementTypeEmailAddress::ID: + return "email"; + default: + UNREACHABLE(); + return "None"; + } +} + +td_api::object_ptr Client::get_passport_element_type(Slice type) { + if (type == "personal_details") { + return make_object(); + } + if (type == "passport") { + return make_object(); + } + if (type == "driver_license") { + return make_object(); + } + if (type == "identity_card") { + return make_object(); + } + if (type == "internal_passport") { + return make_object(); + } + if (type == "address") { + return make_object(); + } + if (type == "utility_bill") { + return make_object(); + } + if (type == "bank_statement") { + return make_object(); + } + if (type == "rental_agreement") { + return make_object(); + } + if (type == "passport_registration") { + return make_object(); + } + if (type == "temporary_registration") { + return make_object(); + } + if (type == "phone_number") { + return make_object(); + } + if (type == "email") { + return make_object(); + } + return nullptr; +} + +td::int32 Client::get_unix_time() const { + CHECK(was_authorized_); + return parameters_->shared_data_->get_unix_time(td::Time::now()); +} + +td::int64 Client::as_tdlib_message_id(int32 message_id) { + return static_cast(message_id) << 20; +} + +td::int32 Client::as_client_message_id(int64 message_id) { + int32 result = static_cast(message_id >> 20); + CHECK(as_tdlib_message_id(result) == message_id); + return result; +} + +td::int64 Client::get_supergroup_chat_id(int32 supergroup_id) { + return static_cast(-1000000000000ll) - static_cast(supergroup_id); +} + +td::int64 Client::get_basic_group_chat_id(int32 basic_group_id) { + return -static_cast(basic_group_id); +} + +constexpr Client::int64 Client::GREAT_MINDS_SET_ID; +constexpr Client::Slice Client::GREAT_MINDS_SET_NAME; + +constexpr Client::Slice Client::MASK_POINTS[MASK_POINTS_SIZE]; + +constexpr int Client::LOGGING_OUT_ERROR_CODE; +constexpr Client::Slice Client::LOGGING_OUT_ERROR_DESCRIPTION; + +constexpr int Client::CLOSING_ERROR_CODE; +constexpr Client::Slice Client::CLOSING_ERROR_DESCRIPTION; + +std::unordered_map Client::methods_; + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/Client.h b/telegram-bot-api/Client.h new file mode 100644 index 0000000..2abf934 --- /dev/null +++ b/telegram-bot-api/Client.h @@ -0,0 +1,935 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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) +// +#pragma once + +#include "telegram-bot-api/Query.h" +#include "telegram-bot-api/Stats.h" +#include "telegram-bot-api/WebhookActor.h" + +#include "td/telegram/ClientActor.h" + +#include "td/actor/actor.h" +#include "td/actor/PromiseFuture.h" +#include "td/actor/SignalSlot.h" +#include "td/actor/Timeout.h" + +#include "td/utils/common.h" +#include "td/utils/Container.h" +#include "td/utils/JsonBuilder.h" +#include "td/utils/Slice.h" +#include "td/utils/Status.h" + +#include +#include +#include +#include +#include +#include + +namespace telegram_bot_api { + +struct ClientParameters; + +namespace td_api = td::td_api; + +class Client : public WebhookActor::Callback { + public: + Client(td::ActorShared<> parent, const td::string &bot_token, bool is_test_dc, td::int64 tqueue_id, + std::shared_ptr parameters, td::ActorId stat_actor); + + void start_up() override; + + void send(PromisedQueryPtr query) override; + + void close(); + + // for stats + ServerBotInfo get_bot_info() const; + + private: + using int32 = td::int32; + using int64 = td::int64; + + using Slice = td::Slice; + using Status = td::Status; + + template + using object_ptr = td_api::object_ptr; + + static constexpr bool USE_MESSAGE_DATABASE = false; + + static constexpr int32 MAX_CERTIFICATE_FILE_SIZE = 3 << 20; + static constexpr int32 MAX_DOWNLOAD_FILE_SIZE = 20 << 20; + + static constexpr int32 MESSAGES_CACHE_TIME = 3600; + + static constexpr std::size_t MIN_PENDING_UPDATES_WARNING = 200; + + static constexpr int64 GREAT_MINDS_SET_ID = 1842540969984001; + static constexpr Slice GREAT_MINDS_SET_NAME = "TelegramGreatMinds"; + + static constexpr int32 MASK_POINTS_SIZE = 4; + static constexpr Slice MASK_POINTS[MASK_POINTS_SIZE] = {"forehead", "eyes", "mouth", "chin"}; + + static constexpr int32 MAX_LENGTH = 10000; // max width or height + static constexpr int32 MAX_DURATION = 24 * 60 * 60; + + static constexpr int LOGGING_OUT_ERROR_CODE = 401; + static constexpr Slice LOGGING_OUT_ERROR_DESCRIPTION = "Unauthorized"; + + static constexpr int CLOSING_ERROR_CODE = 500; + static constexpr Slice CLOSING_ERROR_DESCRIPTION = "Internal Server Error: restart"; + + class JsonFile; + class JsonDatedFile; + class JsonDatedFiles; + class JsonUser; + class JsonUsers; + class JsonChatPermissions; + class JsonChatPhotoInfo; + class JsonChatLocation; + class JsonChat; + class JsonMessageSender; + class JsonAnimation; + class JsonAudio; + class JsonDocument; + class JsonPhotoSize; + class JsonPhoto; + class JsonChatPhoto; + class JsonThumbnail; + class JsonMaskPosition; + class JsonSticker; + class JsonStickers; + class JsonVideo; + class JsonVideoNote; + class JsonVoiceNote; + class JsonContact; + class JsonDice; + class JsonGame; + class JsonInvoice; + class JsonLocation; + class JsonVenue; + class JsonPollOption; + class JsonPoll; + class JsonPollAnswer; + class JsonEntity; + class JsonVectorEntities; + class JsonCallbackGame; + class JsonInlineKeyboardButton; + class JsonInlineKeyboard; + class JsonReplyMarkup; + class JsonMessage; + class JsonMessages; + class JsonDeletedMessage; + class JsonMessageId; + class JsonInlineQuery; + class JsonChosenInlineResult; + class JsonCallbackQuery; + class JsonInlineCallbackQuery; + class JsonShippingQuery; + class JsonPreCheckoutQuery; + class JsonBotCommand; + class JsonChatPhotos; + class JsonChatMember; + class JsonChatMembers; + class JsonGameHighScore; + class JsonAddress; + class JsonOrderInfo; + class JsonSuccessfulPaymentBot; + class JsonEncryptedPassportElement; + class JsonEncryptedCredentials; + class JsonPassportData; + class JsonProximityAlertTriggered; + class JsonUpdateTypes; + class JsonWebhookInfo; + class JsonStickerSet; + class JsonCustomJson; + + class TdOnOkCallback; + class TdOnAuthorizationCallback; + class TdOnInitCallback; + class TdOnGetUserProfilePhotosCallback; + class TdOnSendMessageCallback; + class TdOnSendMessageAlbumCallback; + class TdOnDeleteFailedToSendMessageCallback; + class TdOnEditMessageCallback; + class TdOnEditInlineMessageCallback; + class TdOnStopPollCallback; + class TdOnOkQueryCallback; + class TdOnGetReplyMessageCallback; + class TdOnGetEditedMessageCallback; + class TdOnGetCallbackQueryMessageCallback; + class TdOnGetStickerSetCallback; + class TdOnGetMyCommandsCallback; + class TdOnGetChatFullInfoCallback; + class TdOnGetChatStickerSetCallback; + class TdOnGetChatPinnedMessageCallback; + class TdOnGetChatPinnedMessageToUnpinCallback; + class TdOnGetGroupMembersCallback; + class TdOnGetSupergroupMembersCallback; + class TdOnGetSupergroupMembersCountCallback; + class TdOnGenerateChatInviteLinkCallback; + class TdOnGetGameHighScoresCallback; + class TdOnReturnFileCallback; + class TdOnReturnStickerSetCallback; + class TdOnDownloadFileCallback; + class TdOnCancelDownloadFileCallback; + class TdOnSendCustomRequestCallback; + + void on_get_reply_message(int64 chat_id, object_ptr reply_to_message); + + void on_get_edited_message(object_ptr edited_message); + + void on_get_callback_query_message(object_ptr message, int32 user_id, int state); + + void on_get_sticker_set(int64 set_id, int32 new_callback_query_user_id, int64 new_message_chat_id, + object_ptr sticker_set); + + void on_get_sticker_set_name(int64 set_id, const td::string &name); + + class TdQueryCallback { + public: + virtual void on_result(object_ptr result) = 0; + TdQueryCallback() = default; + TdQueryCallback(const TdQueryCallback &) = delete; + TdQueryCallback &operator=(const TdQueryCallback &) = delete; + TdQueryCallback(TdQueryCallback &&) = delete; + TdQueryCallback &operator=(TdQueryCallback &&) = delete; + virtual ~TdQueryCallback() = default; + }; + + struct UserInfo; + struct ChatInfo; + + enum class AccessRights { Read, Edit, Write }; + + template + class TdOnCheckUserCallback; + template + class TdOnCheckUserNoFailCallback; + template + class TdOnCheckChatCallback; + template + class TdOnCheckMessageCallback; + template + class TdOnCheckRemoteFileIdCallback; + template + class TdOnGetChatMemberCallback; + + template + class TdOnSearchStickerSetCallback; + + class TdOnResolveBotUsernameCallback; + + template + void check_user(int32 user_id, PromisedQueryPtr query, OnSuccess on_success); + + template + void check_user_no_fail(int32 user_id, PromisedQueryPtr query, OnSuccess on_success); + + template + static void check_user_read_access(const UserInfo *user_info, PromisedQueryPtr query, OnSuccess on_success); + + template + void check_chat_access(int64 chat_id, AccessRights access_rights, const ChatInfo *chat_info, PromisedQueryPtr query, + OnSuccess on_success) const; + + template + void check_chat(Slice chat_id_str, AccessRights access_rights, PromisedQueryPtr query, OnSuccess on_success); + + template + void check_remote_file_id(td::string file_id, PromisedQueryPtr query, OnSuccess on_success); + + template + void check_message(Slice chat_id_str, int64 message_id, bool allow_empty, AccessRights access_rights, + Slice message_type, PromisedQueryPtr query, OnSuccess on_success); + + template + void resolve_sticker_set(const td::string &sticker_set_name, PromisedQueryPtr query, OnSuccess on_success); + + template + void resolve_reply_markup_bot_usernames(object_ptr reply_markup, PromisedQueryPtr query, + OnSuccess on_success); + + template + void resolve_inline_query_results_bot_usernames(td::vector> results, + PromisedQueryPtr query, OnSuccess on_success); + + template + void get_chat_member(int64 chat_id, int32 user_id, PromisedQueryPtr query, OnSuccess on_success); + + void send_request(object_ptr &&f, std::unique_ptr handler); + void do_send_request(object_ptr &&f, std::unique_ptr handler); + static object_ptr execute(object_ptr &&f); + void on_update(object_ptr result); + void on_result(td::uint64 id, object_ptr result); + + void on_update_authorization_state(); + void log_out(); + void on_closed(); + void finish_closing(); + + void clear_tqueue(); + + bool allow_update_before_authorization(const td_api::Object *update) const; + void update_shared_unix_time_difference(); + + void on_update_file(object_ptr file); + + static bool to_bool(td::MutableSlice value); + + static td::Result> get_keyboard_button(td::JsonValue &button); + + td::Result> get_inline_keyboard_button(td::JsonValue &button); + + td::Result> get_reply_markup(const Query *query); + + td::Result> get_reply_markup(td::JsonValue &&value); + + static td::Result> get_labeled_price_part(td::JsonValue &value); + + static td::Result>> get_labeled_price_parts(td::JsonValue &value); + + static td::Result> get_shipping_option(td::JsonValue &option); + + static td::Result>> get_shipping_options(const Query *query); + + static td::Result>> get_shipping_options(td::JsonValue &&value); + + static td::Result> get_input_message_content( + td::JsonValue &input_message_content, bool is_input_message_content_required); + + static object_ptr get_chat_action(const Query *query); + + static td::string get_local_file_path(Slice file_uri); + + object_ptr get_input_file(const Query *query, Slice field_name, bool force_file = false) const; + + object_ptr get_input_file(const Query *query, Slice field_name, Slice file_id, + bool force_file) const; + + object_ptr get_input_thumbnail(const Query *query, Slice field_name) const; + + td::Result> get_inline_query_result(td::JsonValue &&value); + + td::Result>> get_inline_query_results(const Query *query); + + td::Result>> get_inline_query_results(td::JsonValue &&value); + + static td::Result> get_bot_command(td::JsonValue &&value); + + static td::Result>> get_bot_commands(const Query *query); + + static td::Result> get_mask_position(const Query *query, Slice field_name); + + static td::Result> get_mask_position(td::JsonValue &&value); + + static int32 mask_point_to_index(const object_ptr &mask_point); + + static object_ptr mask_index_to_point(int32 index); + + td::Result>> get_input_stickers(const Query *query) const; + + static td::Result get_passport_element_hash(Slice encoded_hash); + + static td::Result> get_passport_element_error_source( + td::JsonObject &object); + + static td::Result> get_passport_element_error(td::JsonValue &&value); + + static td::Result>> get_passport_element_errors( + const Query *query); + + static td::Result> get_caption(const Query *query); + + static td::Result> get_text_entity_type(td::JsonObject &object); + + static td::Result> get_text_entity(td::JsonValue &&value); + + static td::Result> get_formatted_text(td::string text, td::string parse_mode, + td::JsonValue &&input_entities); + + static td::Result> get_input_message_text(const Query *query); + + static td::Result> get_input_message_text(td::string text, + bool disable_web_page_preview, + td::string parse_mode, + td::JsonValue &&input_entities); + + static td::Result> get_location(const Query *query); + + static td::Result> get_chat_permissions(const Query *query, bool &allow_legacy); + + td::Result> get_input_media(const Query *query, td::JsonValue &&input_media, + bool for_album) const; + + td::Result> get_input_media(const Query *query, Slice field_name, + bool for_album) const; + + td::Result>> get_input_message_contents(const Query *query, + Slice field_name) const; + + td::Result>> get_input_message_contents( + const Query *query, td::JsonValue &&value) const; + + static object_ptr get_message_send_options(bool disable_notification); + + static td::Result> get_poll_options(const Query *query); + + static int32 get_integer_arg(const Query *query, Slice field_name, int32 default_value, + int32 min_value = std::numeric_limits::min(), + int32 max_value = std::numeric_limits::max()); + + static td::Result get_required_string_arg(const Query *query, Slice field_name); + + static int64 get_message_id(const Query *query, Slice field_name = "message_id"); + + static td::Result get_inline_message_id(const Query *query, Slice field_name = "inline_message_id"); + + static td::Result get_user_id(const Query *query, Slice field_name = "user_id"); + + int64 extract_yet_unsent_message_query_id(int64 chat_id, int64 message_id, bool *is_reply_to_message_deleted); + + void on_message_send_succeeded(object_ptr &&message, int64 old_message_id); + void on_message_send_failed(int64 chat_id, int64 old_message_id, int64 new_message_id, Status result); + + static bool init_methods(); + + void on_cmd(PromisedQueryPtr query); + + Status process_get_me_query(PromisedQueryPtr &query); + Status process_get_my_commands_query(PromisedQueryPtr &query); + Status process_set_my_commands_query(PromisedQueryPtr &query); + Status process_get_user_profile_photos_query(PromisedQueryPtr &query); + Status process_send_message_query(PromisedQueryPtr &query); + Status process_send_animation_query(PromisedQueryPtr &query); + Status process_send_audio_query(PromisedQueryPtr &query); + Status process_send_dice_query(PromisedQueryPtr &query); + Status process_send_document_query(PromisedQueryPtr &query); + Status process_send_photo_query(PromisedQueryPtr &query); + Status process_send_sticker_query(PromisedQueryPtr &query); + Status process_send_video_query(PromisedQueryPtr &query); + Status process_send_video_note_query(PromisedQueryPtr &query); + Status process_send_voice_query(PromisedQueryPtr &query); + Status process_send_game_query(PromisedQueryPtr &query); + Status process_send_invoice_query(PromisedQueryPtr &query); + Status process_send_location_query(PromisedQueryPtr &query); + Status process_send_venue_query(PromisedQueryPtr &query); + Status process_send_contact_query(PromisedQueryPtr &query); + Status process_send_poll_query(PromisedQueryPtr &query); + Status process_stop_poll_query(PromisedQueryPtr &query); + Status process_copy_message_query(PromisedQueryPtr &query); + Status process_forward_message_query(PromisedQueryPtr &query); + Status process_send_media_group_query(PromisedQueryPtr &query); + Status process_send_chat_action_query(PromisedQueryPtr &query); + Status process_edit_message_text_query(PromisedQueryPtr &query); + Status process_edit_message_live_location_query(PromisedQueryPtr &query); + Status process_edit_message_media_query(PromisedQueryPtr &query); + Status process_edit_message_caption_query(PromisedQueryPtr &query); + Status process_edit_message_reply_markup_query(PromisedQueryPtr &query); + Status process_delete_message_query(PromisedQueryPtr &query); + Status process_set_game_score_query(PromisedQueryPtr &query); + Status process_get_game_high_scores_query(PromisedQueryPtr &query); + Status process_answer_inline_query_query(PromisedQueryPtr &query); + Status process_answer_callback_query_query(PromisedQueryPtr &query); + Status process_answer_shipping_query_query(PromisedQueryPtr &query); + Status process_answer_pre_checkout_query_query(PromisedQueryPtr &query); + Status process_export_chat_invite_link_query(PromisedQueryPtr &query); + Status process_get_chat_query(PromisedQueryPtr &query); + Status process_set_chat_photo_query(PromisedQueryPtr &query); + Status process_delete_chat_photo_query(PromisedQueryPtr &query); + Status process_set_chat_title_query(PromisedQueryPtr &query); + Status process_set_chat_permissions_query(PromisedQueryPtr &query); + Status process_set_chat_description_query(PromisedQueryPtr &query); + Status process_pin_chat_message_query(PromisedQueryPtr &query); + Status process_unpin_chat_message_query(PromisedQueryPtr &query); + Status process_unpin_all_chat_messages_query(PromisedQueryPtr &query); + Status process_set_chat_sticker_set_query(PromisedQueryPtr &query); + Status process_delete_chat_sticker_set_query(PromisedQueryPtr &query); + Status process_get_chat_member_query(PromisedQueryPtr &query); + Status process_get_chat_administrators_query(PromisedQueryPtr &query); + Status process_get_chat_member_count_query(PromisedQueryPtr &query); + Status process_leave_chat_query(PromisedQueryPtr &query); + Status process_promote_chat_member_query(PromisedQueryPtr &query); + Status process_set_chat_administrator_custom_title_query(PromisedQueryPtr &query); + Status process_ban_chat_member_query(PromisedQueryPtr &query); + Status process_restrict_chat_member_query(PromisedQueryPtr &query); + Status process_unban_chat_member_query(PromisedQueryPtr &query); + Status process_get_sticker_set_query(PromisedQueryPtr &query); + Status process_upload_sticker_file_query(PromisedQueryPtr &query); + Status process_create_new_sticker_set_query(PromisedQueryPtr &query); + Status process_add_sticker_to_set_query(PromisedQueryPtr &query); + Status process_set_sticker_set_thumb_query(PromisedQueryPtr &query); + Status process_set_sticker_position_in_set_query(PromisedQueryPtr &query); + Status process_delete_sticker_from_set_query(PromisedQueryPtr &query); + Status process_set_passport_data_errors_query(PromisedQueryPtr &query); + Status process_send_custom_request_query(PromisedQueryPtr &query); + Status process_answer_custom_query_query(PromisedQueryPtr &query); + Status process_get_updates_query(PromisedQueryPtr &query); + Status process_set_webhook_query(PromisedQueryPtr &query); + Status process_get_webhook_info_query(PromisedQueryPtr &query); + Status process_get_file_query(PromisedQueryPtr &query); + + void webhook_verified(td::string cached_ip_address) override; + void webhook_success() override; + void webhook_error(Status status) override; + void webhook_closed(Status status) override; + void hangup_shared() override; + int32 get_webhook_max_connections(const Query *query) const; + static bool get_webhook_fix_ip_address(const Query *query); + void do_set_webhook(PromisedQueryPtr query, bool was_deleted); + void save_webhook() const; + td::string get_webhook_certificate_path() const; + + void do_send_message(object_ptr input_message_content, PromisedQueryPtr query); + + int64 get_send_message_query_id(PromisedQueryPtr query, bool is_multisend); + + void on_sent_message(object_ptr &&message, int64 query_id); + + void do_get_file(object_ptr file, PromisedQueryPtr query); + + bool is_file_being_downloaded(int32 file_id) const; + void on_file_download(int32 file_id, td::Result> r_file); + + void fix_reply_markup_bot_user_ids(object_ptr &reply_markup) const; + void fix_inline_query_results_bot_user_ids(td::vector> &results) const; + + void resolve_bot_usernames(PromisedQueryPtr query, td::Promise on_success); + void on_resolve_bot_username(const td::string &username, int32 user_id); + + void abort_long_poll(bool from_set_webhook); + + void fail_query_conflict(Slice message, PromisedQueryPtr &&query); + + static void fail_query_with_error(PromisedQueryPtr query, int32 error_code, Slice error_message, + Slice default_message = Slice()); + + static void fail_query_with_error(PromisedQueryPtr &&query, object_ptr error, + Slice default_message = Slice()); + + class JsonUpdates; + void do_get_updates(int32 offset, int32 limit, int32 timeout, PromisedQueryPtr query); + + void long_poll_wakeup(bool force_flag); + + void raw_event(const td::Event::Raw &event) override; + + void loop() override; + + void timeout_expired() override; + + struct UserInfo { + enum class Type { Regular, Deleted, Bot, Unknown }; + Type type = Type::Unknown; + + td::string first_name; + td::string last_name; + td::string username; + td::string language_code; + + td::string bio; + + bool have_access = false; + bool can_join_groups = false; + bool can_read_all_group_messages = false; + bool is_inline_bot = false; + }; + static void add_user(std::unordered_map &users, object_ptr &&user); + void set_user_bio(int32 user_id, td::string &&bio); + const UserInfo *get_user_info(int32 user_id) const; + + struct GroupInfo { + td::string description; + td::string invite_link; + int32 member_count = 0; + bool left = false; + bool kicked = false; + bool is_active = false; + int32 upgraded_to_supergroup_id = 0; + }; + static void add_group(std::unordered_map &groups, object_ptr &&group); + void set_group_description(int32 group_id, td::string &&descripton); + void set_group_invite_link(int32 group_id, td::string &&invite_link); + const GroupInfo *get_group_info(int32 group_id) const; + + struct SupergroupInfo { + td::string username; + td::string description; + td::string invite_link; + int64 sticker_set_id = 0; + int32 date = 0; + int32 slow_mode_delay = 0; + int64 linked_chat_id = 0; + object_ptr location; + object_ptr status; + bool is_supergroup = false; + bool can_set_sticker_set = false; + bool has_location = false; + }; + static void add_supergroup(std::unordered_map &supergroups, + object_ptr &&supergroup); + void set_supergroup_description(int32 supergroup_id, td::string &&descripton); + void set_supergroup_invite_link(int32 supergroup_id, td::string &&invite_link); + void set_supergroup_sticker_set_id(int32 supergroup_id, int64 sticker_set_id); + void set_supergroup_can_set_sticker_set(int32 supergroup_id, bool can_set_sticker_set); + void set_supergroup_slow_mode_delay(int32 supergroup_id, int32 slow_mode_delay); + void set_supergroup_linked_chat_id(int32 supergroup_id, int64 linked_chat_id); + void set_supergroup_location(int32 supergroup_id, object_ptr location); + const SupergroupInfo *get_supergroup_info(int32 supergroup_id) const; + + struct ChatInfo { + enum class Type { Private, Group, Supergroup, Unknown }; + Type type = Type::Unknown; + td::string title; + object_ptr photo; + object_ptr permissions; + union { + int32 user_id; + int32 group_id; + int32 supergroup_id; + }; + }; + ChatInfo *add_chat(int64 chat_id); + const ChatInfo *get_chat(int64 chat_id) const; + + enum class ChatType { Private, Group, Supergroup, Channel, Unknown }; + + ChatType get_chat_type(int64 chat_id) const; + + td::string get_chat_description(int64 chat_id) const; + + struct MessageInfo { + mutable double access_time = 1e20; + mutable const MessageInfo *lru_next = nullptr; + mutable const MessageInfo *lru_prev = nullptr; + + int64 id = 0; + int32 sender_user_id = 0; + int64 sender_chat_id = 0; + int64 chat_id = 0; + int32 date = 0; + int32 edit_date = 0; + int64 initial_chat_id = 0; + int32 initial_sender_user_id = 0; + int64 initial_sender_chat_id = 0; + int32 initial_send_date = 0; + int64 initial_message_id = 0; + td::string initial_author_signature; + td::string initial_sender_name; + td::string author_signature; + int64 reply_to_message_id = 0; + int64 media_album_id = 0; + int32 via_bot_user_id = 0; + object_ptr content; + object_ptr reply_markup; + + mutable bool is_reply_to_message_deleted = false; + mutable bool is_content_changed = false; + }; + + static int64 &get_reply_to_message_id(object_ptr &message); + + void set_message_reply_to_message_id(MessageInfo *message_info, int64 reply_to_message_id); + + static td::CSlice get_callback_data(const object_ptr &type); + + static bool are_equal_inline_keyboard_buttons(const td_api::inlineKeyboardButton *lhs, + const td_api::inlineKeyboardButton *rhs); + + static bool are_equal_inline_keyboards(const td_api::replyMarkupInlineKeyboard *lhs, + const td_api::replyMarkupInlineKeyboard *rhs); + + void set_message_reply_markup(MessageInfo *message_info, object_ptr &&reply_markup); + + static int64 get_sticker_set_id(const object_ptr &content); + + bool have_sticker_set_name(int64 sticker_set_id) const; + + Slice get_sticker_set_name(int64 sticker_set_id) const; + + int32 choose_added_member_id(const td_api::messageChatAddMembers *message_add_members) const; + + bool need_skip_update_message(int64 chat_id, const object_ptr &message, bool is_edited) const; + + void json_store_file(td::JsonObjectScope &object, const td_api::file *file, bool with_path = false) const; + + void json_store_thumbnail(td::JsonObjectScope &object, const td_api::thumbnail *thumbnail) const; + + static void json_store_callback_query_payload(td::JsonObjectScope &object, + const td_api::CallbackQueryPayload *payload); + + static void json_store_permissions(td::JsonObjectScope &object, const td_api::chatPermissions *permissions); + + void remove_replies_to_message(int64 chat_id, int64 reply_to_message_id, bool only_from_cache); + void delete_message(int64 chat_id, int64 message_id, bool only_from_cache); + static void delete_messages_lru(void *client_void); + void schedule_next_delete_messages_lru(); + void update_message_lru(const MessageInfo *message_info) const; + + void add_new_message(object_ptr &&message, bool is_edited); + void process_new_message_queue(int64 chat_id); + + struct FullMessageId { + int64 chat_id; + int64 message_id; + + FullMessageId() : chat_id(0), message_id(0) { + } + FullMessageId(int64 chat_id, int64 message_id) : chat_id(chat_id), message_id(message_id) { + } + + bool operator==(const FullMessageId &other) const { + return chat_id == other.chat_id && message_id == other.message_id; + } + }; + + struct FullMessageIdHash { + std::size_t operator()(FullMessageId full_message_id) const { + return std::hash()(full_message_id.chat_id) * 2023654985u + + std::hash()(full_message_id.message_id); + } + }; + + FullMessageId add_message(object_ptr &&message, bool force_update_content = false); + const MessageInfo *get_message(int64 chat_id, int64 message_id) const; + MessageInfo *get_message_editable(int64 chat_id, int64 message_id); + + void update_message_content(int64 chat_id, int64 message_id, object_ptr &&content); + + void on_update_message_edited(int64 chat_id, int64 message_id, int32 edit_date, + object_ptr &&reply_markup); + + int32 get_unix_time() const; + + static int64 as_tdlib_message_id(int32 message_id); + + static int32 as_client_message_id(int64 message_id); + + static int64 get_supergroup_chat_id(int32 supergroup_id); + + static int64 get_basic_group_chat_id(int32 basic_group_id); + + void add_update_poll(object_ptr &&update); + + void add_update_poll_answer(object_ptr &&update); + + void add_new_inline_query(int64 inline_query_id, int32 sender_user_id, object_ptr location, + const td::string &query, const td::string &offset); + + void add_new_chosen_inline_result(int32 sender_user_id, object_ptr location, + const td::string &query, const td::string &result_id, + const td::string &inline_message_id); + + void add_new_callback_query(object_ptr &&query); + void process_new_callback_query_queue(int32 user_id, int state); + + void add_new_inline_callback_query(object_ptr &&query); + + void add_new_shipping_query(object_ptr &&query); + + void add_new_pre_checkout_query(object_ptr &&query); + + void add_new_custom_event(object_ptr &&event); + + void add_new_custom_query(object_ptr &&query); + + // append only before Size + enum class UpdateType : int32 { + Message, + EditedMessage, + ChannelPost, + EditedChannelPost, + InlineQuery, + ChosenInlineResult, + CallbackQuery, + CustomEvent, + CustomQuery, + ShippingQuery, + PreCheckoutQuery, + Poll, + PollAnswer, + Size + }; + + static Slice get_update_type_name(UpdateType update_type); + + static td::uint32 get_allowed_update_types(td::MutableSlice allowed_updates, bool is_internal); + + bool update_allowed_update_types(const Query *query); + + template + void add_update(UpdateType update_type, const T &update, int32 timeout, int64 webhook_queue_id); + + void add_update_impl(UpdateType update_type, const td::VirtuallyJsonable &update, int32 timeout, + int64 webhook_queue_id); + + std::size_t get_pending_update_count() const; + + static bool is_chat_member(const object_ptr &status); + + static td::string get_chat_member_status(const object_ptr &status); + + static td::string get_passport_element_type(int32 id); + + static object_ptr get_passport_element_type(Slice type); + + bool have_message_access(int64 chat_id) const; + + // by default all 13 update types up to PollAnswer are allowed + static constexpr td::uint32 DEFAULT_ALLOWED_UPDATE_TYPES = ((1 << 13) - 1); + + object_ptr authorization_state_; + bool was_authorized_ = false; + bool closing_ = false; + bool logging_out_ = false; + bool need_close_ = false; + bool clear_tqueue_ = false; + + td::ActorShared<> parent_; + td::string bot_token_; + td::string bot_token_with_dc_; + td::string bot_token_id_; + bool is_test_dc_; + int64 tqueue_id_; + double start_timestamp_ = 0; + + int32 my_id_ = -1; + int32 authorization_date_ = -1; + + int32 group_anonymous_bot_user_id_ = 0; + int32 service_notifications_user_id_ = 0; + + static std::unordered_map methods_; + + MessageInfo messages_lru_root_; + std::unordered_map, FullMessageIdHash> messages_; // message cache + std::unordered_map users_; // user info cache + std::unordered_map groups_; // group info cache + std::unordered_map supergroups_; // supergroup info cache + std::unordered_map chats_; // chat info cache + + std::unordered_map, FullMessageIdHash> + reply_message_ids_; // message -> replies to it + std::unordered_map, FullMessageIdHash> + yet_unsent_reply_message_ids_; // message -> replies to it + + td::Timeout next_delete_messages_lru_timeout_; + + std::unordered_map> file_download_listeners_; + std::unordered_set download_started_file_ids_; + + struct YetUnsentMessage { + int64 reply_to_message_id = 0; + bool is_reply_to_message_deleted = false; + int64 send_message_query_id = 0; + }; + std::unordered_map yet_unsent_messages_; + + struct PendingSendMessageQuery { + PromisedQueryPtr query; + bool is_multisend = false; + int32 awaited_messages = 0; + td::vector messages; + object_ptr error; + }; + std::unordered_map + pending_send_message_queries_; // query_id -> PendingSendMessageQuery + int64 current_send_message_query_id_ = 1; + + struct NewMessage { + object_ptr message; + bool is_edited = false; + + NewMessage(object_ptr &&message, bool is_edited) + : message(std::move(message)), is_edited(is_edited) { + } + }; + struct NewMessageQueue { + std::queue queue_; + bool has_active_request_ = false; + }; + std::unordered_map new_message_queues_; // chat_id -> queue + + struct NewCallbackQueryQueue { + std::queue> queue_; + bool has_active_request_ = false; + }; + std::unordered_map new_callback_query_queues_; // sender_user_id -> queue + + std::unordered_map sticker_set_names_; + + int32 cur_temp_bot_user_id_ = 1; + std::unordered_map bot_user_ids_; + std::unordered_set unresolved_bot_usernames_; + std::unordered_map temp_to_real_bot_user_id_; + std::unordered_map> awaiting_bot_resolve_queries_; + + struct PendingBotResolveQuery { + std::size_t pending_resolve_count = 0; + PromisedQueryPtr query; + td::Promise on_success; + }; + std::unordered_map pending_bot_resolve_queries_; + int64 current_bot_resolve_query_id_ = 1; + + td::string dir_; + td::string absolute_dir_; + td::ActorOwn td_client_; + td::ActorContext context_; + std::queue cmd_queue_; + td::vector> pending_updates_; + td::Container> handlers_; + + static constexpr int32 LONG_POLL_MAX_TIMEOUT = 50; + static constexpr double LONG_POLL_MAX_DELAY = 0.01; + static constexpr double LONG_POLL_WAIT_AFTER = 0.002; + int32 long_poll_limit_ = 0; + int32 long_poll_offset_ = 0; + bool long_poll_was_wakeup_ = false; + double long_poll_hard_timeout_ = 0; + td::Slot long_poll_slot_; + PromisedQueryPtr long_poll_query_; + + static constexpr int32 BOT_UPDATES_WARNING_DELAY = 30; + double next_bot_updates_warning_date_ = 0; + bool was_bot_updates_warning_ = false; + + td::uint32 allowed_update_types_ = DEFAULT_ALLOWED_UPDATE_TYPES; + + bool has_webhook_certificate_ = false; + enum class WebhookQueryType { Cancel, Verify }; + WebhookQueryType webhook_query_type_ = WebhookQueryType::Cancel; + td::ActorOwn webhook_id_; + PromisedQueryPtr webhook_set_query_; + td::string webhook_url_; + double webhook_set_date_ = 0; + int32 webhook_max_connections_ = 0; + td::string webhook_ip_address_; + bool webhook_fix_ip_address_ = false; + int32 last_webhook_error_date_ = 0; + Status last_webhook_error_; + double next_allowed_set_webhook_date_ = 0; + double next_set_webhook_logging_date_ = 0; + double next_webhook_is_not_modified_warning_date_ = 0; + std::size_t last_pending_update_count_ = MIN_PENDING_UPDATES_WARNING; + + double local_unix_time_difference_ = 0; // Unix time - now() + + int32 previous_get_updates_offset_ = -1; + double previous_get_updates_start_date_ = 0; + double previous_get_updates_finish_date_ = 0; + double next_conflict_response_date_ = 0; + + td::uint64 webhook_generation_ = 1; + + std::shared_ptr parameters_; + + td::ActorId stat_actor_; +}; + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/ClientManager.cpp b/telegram-bot-api/ClientManager.cpp new file mode 100644 index 0000000..a5e7689 --- /dev/null +++ b/telegram-bot-api/ClientManager.cpp @@ -0,0 +1,406 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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 "telegram-bot-api/ClientManager.h" + +#include "telegram-bot-api/Client.h" +#include "telegram-bot-api/ClientParameters.h" +#include "telegram-bot-api/WebhookActor.h" + +#include "td/telegram/ClientActor.h" + +#include "td/db/binlog/Binlog.h" +#include "td/db/binlog/ConcurrentBinlog.h" +#include "td/db/BinlogKeyValue.h" +#include "td/db/DbKey.h" +#include "td/db/TQueue.h" + +#include "td/net/HttpFile.h" + +#include "td/actor/MultiPromise.h" + +#include "td/utils/common.h" +#include "td/utils/format.h" +#include "td/utils/logging.h" +#include "td/utils/misc.h" +#include "td/utils/Parser.h" +#include "td/utils/port/IPAddress.h" +#include "td/utils/port/Stat.h" +#include "td/utils/Slice.h" +#include "td/utils/StackAllocator.h" +#include "td/utils/StringBuilder.h" +#include "td/utils/Time.h" + +#include +#include + +namespace telegram_bot_api { + +void ClientManager::close(td::Promise &&promise) { + close_promises_.push_back(std::move(promise)); + if (close_flag_) { + return; + } + + close_flag_ = true; + auto ids = clients_.ids(); + for (auto id : ids) { + auto *client_info = clients_.get(id); + CHECK(client_info); + send_closure(client_info->client_, &Client::close); + } + if (ids.empty()) { + close_db(); + } +} + +void ClientManager::send(PromisedQueryPtr query) { + if (close_flag_) { + // automatically send 429 + return; + } + + td::string token = query->token().str(); + if (token[0] == '0' || token.size() > 80u || token.find('/') != td::string::npos || + token.find(':') == td::string::npos) { + return fail_query(401, "Unauthorized: invalid token specified", std::move(query)); + } + auto r_user_id = td::to_integer_safe(query->token().substr(0, token.find(':'))); + if (r_user_id.is_error() || r_user_id.ok() < 0 || !token_range_(r_user_id.ok())) { + return fail_query(401, "Unauthorized: unallowed token specified", std::move(query)); + } + + if (query->is_test_dc()) { + token += "/test"; + } + + auto id_it = token_to_id_.find(token); + if (id_it == token_to_id_.end()) { + std::string ip_address; + if (query->peer_address().is_valid() && !query->peer_address().is_reserved()) { // external connection + ip_address = query->peer_address().get_ip_str().str(); + } else { + // invalid peer address or connection from the local network + ip_address = query->get_header("x-real-ip").str(); + } + if (!ip_address.empty()) { + td::IPAddress tmp; + tmp.init_host_port(ip_address, 0).ignore(); + tmp.clear_ipv6_interface(); + if (tmp.is_valid()) { + ip_address = tmp.get_ip_str().str(); + } + } + LOG(DEBUG) << "Receive incoming query for new bot " << token << " from " << query->peer_address(); + if (!ip_address.empty()) { + LOG(DEBUG) << "Check Client creation flood control for IP address " << ip_address; + auto res = flood_controls_.emplace(std::move(ip_address), td::FloodControlFast()); + auto &flood_control = res.first->second; + if (res.second) { + flood_control.add_limit(60, 20); // 20 in a minute + flood_control.add_limit(60 * 60, 600); // 600 in an hour + } + td::uint32 now = static_cast(td::Time::now()); + td::uint32 wakeup_at = flood_control.get_wakeup_at(); + if (wakeup_at > now) { + LOG(INFO) << "Failed to create Client from IP address " << ip_address; + return query->set_retry_after_error(static_cast(wakeup_at - now) + 1); + } + flood_control.add_event(static_cast(now)); + } + + auto id = clients_.create(ClientInfo{BotStatActor(stat_.actor_id(&stat_)), token, td::ActorOwn()}); + auto *client_info = clients_.get(id); + auto stat_actor = client_info->stat_.actor_id(&client_info->stat_); + auto client_id = td::create_actor( + PSLICE() << "Client/" << token, actor_shared(this, id), query->token().str(), query->is_test_dc(), + get_tqueue_id(r_user_id.ok(), query->is_test_dc()), parameters_, std::move(stat_actor)); + + auto method = query->method(); + if (method != "deletewebhook" && method != "setwebhook") { + auto bot_token_with_dc = PSTRING() << query->token() << (query->is_test_dc() ? ":T" : ""); + auto webhook_info = parameters_->shared_data_->webhook_db_->get(bot_token_with_dc); + if (!webhook_info.empty()) { + send_closure(client_id, &Client::send, + get_webhook_restore_query(bot_token_with_dc, webhook_info, parameters_->shared_data_)); + } + } + + clients_.get(id)->client_ = std::move(client_id); + std::tie(id_it, std::ignore) = token_to_id_.emplace(token, id); + } + auto *client_info = clients_.get(id_it->second); + + if (!query->is_internal()) { + query->set_stat_actor(client_info->stat_.actor_id(&client_info->stat_)); + } + send_closure(client_info->client_, &Client::send, std::move(query)); // will send 429 if the client is already closed +} + +void ClientManager::get_stats(td::PromiseActor promise, + td::vector> args) { + if (close_flag_) { + promise.set_value(td::BufferSlice("Closing")); + return; + } + size_t buf_size = 1 << 14; + auto buf = td::StackAllocator::alloc(buf_size); + td::StringBuilder sb(buf.as_slice()); + + td::Slice id_filter; + for (auto &arg : args) { + if (arg.first == "id") { + id_filter = arg.second; + } + if (arg.first == "v") { + auto r_verbosity = td::to_integer_safe(arg.second); + if (r_verbosity.is_ok()) { + parameters_->shared_data_->next_verbosity_level_ = r_verbosity.ok(); + } + } + } + + auto now = td::Time::now(); + td::int32 active_bot_count = 0; + std::multimap top_bot_ids; + for (auto id : clients_.ids()) { + auto *client_info = clients_.get(id); + CHECK(client_info); + + if (client_info->stat_.is_active(now)) { + active_bot_count++; + } + + if (!td::begins_with(client_info->token_, id_filter)) { + continue; + } + + auto stats = client_info->stat_.as_vector(now); + double score = 0.0; + for (auto &stat : stats) { + if (stat.key_ == "update_count" || stat.key_ == "request_count") { + score -= td::to_double(stat.value_); + } + } + top_bot_ids.emplace(static_cast(score * 1e9), id); + } + + sb << stat_.get_description() << "\n"; + if (id_filter.empty()) { + sb << "uptime\t" << now - parameters_->start_timestamp_ << "\n"; + sb << "bot_count\t" << clients_.size() << "\n"; + sb << "active_bot_count\t" << active_bot_count << "\n"; + auto r_mem_stat = td::mem_stat(); + if (r_mem_stat.is_ok()) { + auto mem_stat = r_mem_stat.move_as_ok(); + sb << "rss\t" << td::format::as_size(mem_stat.resident_size_) << "\n"; + sb << "vm\t" << td::format::as_size(mem_stat.virtual_size_) << "\n"; + sb << "rss_peak\t" << td::format::as_size(mem_stat.resident_size_peak_) << "\n"; + sb << "vm_peak\t" << td::format::as_size(mem_stat.virtual_size_peak_) << "\n"; + } else { + LOG(INFO) << "Failed to get memory statistics: " << r_mem_stat.error(); + } + + ServerCpuStat::update(td::Time::now()); + auto cpu_stats = ServerCpuStat::instance().as_vector(td::Time::now()); + for (auto &stat : cpu_stats) { + sb << stat.key_ << "\t" << stat.value_ << "\n"; + } + + sb << "buffer_memory\t" << td::format::as_size(td::BufferAllocator::get_buffer_mem()) << "\n"; + sb << "active_webhook_connections\t" << WebhookActor::get_total_connections_count() << "\n"; + sb << "active_requests\t" << parameters_->shared_data_->query_count_.load() << "\n"; + sb << "active_network_queries\t" << td::get_pending_network_query_count(*parameters_->net_query_stats_) << "\n"; + auto stats = stat_.as_vector(now); + for (auto &stat : stats) { + sb << stat.key_ << "\t" << stat.value_ << "\n"; + } + } + + for (auto top_bot_id : top_bot_ids) { + auto *client_info = clients_.get(top_bot_id.second); + CHECK(client_info); + + auto bot_info = client_info->client_->get_actor_unsafe()->get_bot_info(); + sb << "\n"; + sb << "id\t" << bot_info.id_ << "\n"; + sb << "uptime\t" << now - bot_info.start_timestamp_ << "\n"; + sb << "token\t" << bot_info.token_ << "\n"; + sb << "username\t" << bot_info.username_ << "\n"; + sb << "webhook\t" << bot_info.webhook_ << "\n"; + sb << "has_custom_certificate\t" << bot_info.has_webhook_certificate_ << "\n"; + sb << "head_update_id\t" << bot_info.head_update_id_ << "\n"; + sb << "tail_update_id\t" << bot_info.tail_update_id_ << "\n"; + sb << "pending_update_count\t" << bot_info.pending_update_count_ << "\n"; + sb << "webhook_max_connections\t" << bot_info.webhook_max_connections_ << "\n"; + + auto stats = client_info->stat_.as_vector(now); + for (auto &stat : stats) { + if (stat.key_ == "update_count" || stat.key_ == "request_count") { + sb << stat.key_ << "/sec\t" << stat.value_ << "\n"; + } + } + + if (sb.is_error()) { + break; + } + } + // ignore sb overflow + promise.set_value(td::BufferSlice(sb.as_cslice())); +} + +td::int64 ClientManager::get_tqueue_id(td::int64 user_id, bool is_test_dc) { + return user_id + (static_cast(is_test_dc) << 54); +} + +void ClientManager::start_up() { + //NB: the same scheduler as for database in Td + auto current_scheduler_id = td::Scheduler::instance()->sched_id(); + auto scheduler_count = td::Scheduler::instance()->sched_count(); + auto scheduler_id = td::min(current_scheduler_id + 1, scheduler_count - 1); + + // init tqueue + { + auto tqueue_binlog = td::make_unique>(); + auto binlog = td::make_unique(); + auto tqueue = td::TQueue::create(); + td::vector failed_to_replay_log_event_ids; + td::int64 loaded_event_count = 0; + binlog + ->init("tqueue.binlog", + [&](const td::BinlogEvent &event) { + if (tqueue_binlog->replay(event, *tqueue).is_error()) { + failed_to_replay_log_event_ids.push_back(event.id_); + } else { + loaded_event_count++; + } + }) + .ensure(); + tqueue_binlog.reset(); + LOG(WARNING) << "Loaded " << loaded_event_count << " TQueue events"; + + if (!failed_to_replay_log_event_ids.empty()) { + LOG(ERROR) << "Failed to replay " << failed_to_replay_log_event_ids.size() << " TQueue events"; + for (auto &log_event_id : failed_to_replay_log_event_ids) { + binlog->erase(log_event_id); + } + } + + auto concurrent_binlog = std::make_shared(std::move(binlog), scheduler_id); + auto concurrent_tqueue_binlog = td::make_unique>(); + concurrent_tqueue_binlog->set_binlog(std::move(concurrent_binlog)); + tqueue->set_callback(std::move(concurrent_tqueue_binlog)); + + parameters_->shared_data_->tqueue_ = std::move(tqueue); + } + + // init webhook_db + auto concurrent_webhook_db = td::make_unique>(); + auto status = concurrent_webhook_db->init("webhooks_db.binlog", td::DbKey::empty(), scheduler_id); + LOG_IF(FATAL, status.is_error()) << "Can't open webhooks_db.binlog " << status.error(); + parameters_->shared_data_->webhook_db_ = std::move(concurrent_webhook_db); + + auto &webhook_db = *parameters_->shared_data_->webhook_db_; + for (auto key_value : webhook_db.get_all()) { + if (!token_range_(td::to_integer(key_value.first))) { + LOG(WARNING) << "DROP WEBHOOK: " << key_value.first << " ---> " << key_value.second; + webhook_db.erase(key_value.first); + continue; + } + + auto query = get_webhook_restore_query(key_value.first, key_value.second, parameters_->shared_data_); + send_closure_later(actor_id(this), &ClientManager::send, std::move(query)); + } +} + +PromisedQueryPtr ClientManager::get_webhook_restore_query(td::Slice token, td::Slice webhook_info, + std::shared_ptr shared_data) { + // create Query with empty promise + td::vector containers; + auto add_string = [&containers](td::Slice str) { + containers.emplace_back(str); + return containers.back().as_slice(); + }; + + token = add_string(token); + + LOG(WARNING) << "WEBHOOK: " << token << " ---> " << webhook_info; + + bool is_test_dc = false; + if (td::ends_with(token, ":T")) { + token.remove_suffix(2); + is_test_dc = true; + } + + td::ConstParser parser{webhook_info}; + td::vector> args; + if (parser.try_skip("cert/")) { + args.emplace_back(add_string("certificate"), add_string("previous")); + } + + if (parser.try_skip("#maxc")) { + args.emplace_back(add_string("max_connections"), add_string(parser.read_till('/'))); + parser.skip('/'); + } + + if (parser.try_skip("#ip")) { + args.emplace_back(add_string("ip_address"), add_string(parser.read_till('/'))); + parser.skip('/'); + } + + if (parser.try_skip("#fix_ip")) { + args.emplace_back(add_string("fix_ip_address"), add_string("1")); + parser.skip('/'); + } + + if (parser.try_skip("#allow")) { + args.emplace_back(add_string("allowed_updates"), add_string(parser.read_till('/'))); + parser.skip('/'); + } + + args.emplace_back(add_string("url"), add_string(parser.read_all())); + + const auto method = add_string("setwebhook"); + auto query = std::make_unique(std::move(containers), token, is_test_dc, method, std::move(args), + td::vector>(), + td::vector(), std::move(shared_data), td::IPAddress()); + query->set_internal(true); + return PromisedQueryPtr(query.release(), PromiseDeleter(td::PromiseActor>())); +} + +void ClientManager::hangup_shared() { + auto id = get_link_token(); + auto *info = clients_.get(id); + CHECK(info != nullptr); + info->client_.release(); + token_to_id_.erase(info->token_); + clients_.erase(id); + + if (close_flag_ && clients_.empty()) { + close_db(); + } +} + +void ClientManager::close_db() { + LOG(WARNING) << "Closing databases"; + td::MultiPromiseActorSafe mpromise("close binlogs"); + mpromise.add_promise(td::PromiseCreator::lambda( + [actor_id = actor_id(this)](td::Unit) { send_closure(actor_id, &ClientManager::finish_close); })); + + parameters_->shared_data_->tqueue_->close(mpromise.get_promise()); + parameters_->shared_data_->webhook_db_->close(mpromise.get_promise()); +} + +void ClientManager::finish_close() { + LOG(WARNING) << "Stop ClientManager"; + auto promises = std::move(close_promises_); + for (auto &promise : promises) { + promise.set_value(td::Unit()); + } + stop(); +} + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/ClientManager.h b/telegram-bot-api/ClientManager.h new file mode 100644 index 0000000..c76dc7a --- /dev/null +++ b/telegram-bot-api/ClientManager.h @@ -0,0 +1,80 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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) +// +#pragma once + +#include "telegram-bot-api/Client.h" +#include "telegram-bot-api/Query.h" +#include "telegram-bot-api/Stats.h" + +#include "td/actor/actor.h" +#include "td/actor/PromiseFuture.h" + +#include "td/utils/buffer.h" +#include "td/utils/common.h" +#include "td/utils/Container.h" +#include "td/utils/FloodControlFast.h" +#include "td/utils/Slice.h" + +#include +#include +#include + +namespace telegram_bot_api { + +struct ClientParameters; +struct SharedData; + +class ClientManager final : public td::Actor { + public: + struct TokenRange { + td::uint64 rem; + td::uint64 mod; + bool operator()(td::uint64 x) { + return x % mod == rem; + } + }; + ClientManager(std::shared_ptr parameters, TokenRange token_range) + : parameters_(std::move(parameters)), token_range_(token_range) { + } + + void send(PromisedQueryPtr query); + + void get_stats(td::PromiseActor promise, td::vector> args); + + void close(td::Promise &&promise); + + private: + class ClientInfo { + public: + BotStatActor stat_; + td::string token_; + td::ActorOwn client_; + }; + td::Container clients_; + BotStatActor stat_{td::ActorId()}; + + std::shared_ptr parameters_; + TokenRange token_range_; + + std::unordered_map token_to_id_; + std::unordered_map flood_controls_; + + bool close_flag_ = false; + td::vector> close_promises_; + + static td::int64 get_tqueue_id(td::int64 user_id, bool is_test_dc); + + static PromisedQueryPtr get_webhook_restore_query(td::Slice token, td::Slice webhook_info, + std::shared_ptr shared_data); + + void start_up() override; + void hangup_shared() override; + void close_db(); + void finish_close(); +}; + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/ClientParameters.h b/telegram-bot-api/ClientParameters.h new file mode 100644 index 0000000..018fca6 --- /dev/null +++ b/telegram-bot-api/ClientParameters.h @@ -0,0 +1,74 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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) +// +#pragma once + +#include "td/actor/actor.h" + +#include "td/db/KeyValueSyncInterface.h" +#include "td/db/TQueue.h" + +#include "td/net/GetHostByNameActor.h" + +#include "td/utils/common.h" +#include "td/utils/List.h" +#include "td/utils/port/IPAddress.h" + +#include +#include +#include + +namespace td { +class NetQueryStats; +} + +namespace telegram_bot_api { + +struct SharedData { + std::atomic query_count_{0}; + std::atomic next_verbosity_level_{-1}; + + // not thread-safe + td::ListNode query_list_; + td::unique_ptr webhook_db_; + td::unique_ptr tqueue_; + + double unix_time_difference_{-1e100}; + + static constexpr size_t TQUEUE_EVENT_BUFFER_SIZE = 1000; + td::TQueue::Event event_buffer_[TQUEUE_EVENT_BUFFER_SIZE]; + + td::int32 get_unix_time(double now) const { + auto result = unix_time_difference_ + now; + if (result <= 0) { + return 0; + } + if (result >= std::numeric_limits::max()) { + return std::numeric_limits::max(); + } + return static_cast(result); + } +}; + +struct ClientParameters { + bool local_mode_ = false; + + td::int32 api_id_ = 0; + td::string api_hash_; + + td::int32 default_max_webhook_connections_ = 0; + td::IPAddress webhook_proxy_ip_address_; + + double start_timestamp_ = 0; + + td::ActorId get_host_by_name_actor_id_; + + std::shared_ptr shared_data_; + + std::shared_ptr net_query_stats_; +}; + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/HttpConnection.cpp b/telegram-bot-api/HttpConnection.cpp new file mode 100644 index 0000000..2b87707 --- /dev/null +++ b/telegram-bot-api/HttpConnection.cpp @@ -0,0 +1,97 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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 "telegram-bot-api/HttpConnection.h" + +#include "telegram-bot-api/Query.h" + +#include "td/net/HttpHeaderCreator.h" + +#include "td/utils/common.h" +#include "td/utils/JsonBuilder.h" +#include "td/utils/logging.h" +#include "td/utils/Parser.h" + +namespace telegram_bot_api { + +void HttpConnection::handle(td::unique_ptr http_query, + td::ActorOwn connection) { + CHECK(connection_->empty()); + connection_ = std::move(connection); + + LOG(DEBUG) << "Handle " << *http_query; + td::Parser url_path_parser(http_query->url_path_); + if (url_path_parser.peek_char() != '/') { + return send_http_error(404, "Not Found: absolute URI is specified in the Request-Line"); + } + + if (!url_path_parser.try_skip("/bot")) { + return send_http_error(404, "Not Found"); + } + + auto token = url_path_parser.read_till('/'); + bool is_test_dc = false; + if (url_path_parser.try_skip("/test")) { + is_test_dc = true; + } + url_path_parser.skip('/'); + if (url_path_parser.status().is_error()) { + return send_http_error(404, "Not Found"); + } + + auto method = url_path_parser.data(); + auto query = std::make_unique(std::move(http_query->container_), token, is_test_dc, method, + std::move(http_query->args_), std::move(http_query->headers_), + std::move(http_query->files_), shared_data_, http_query->peer_address_); + + td::PromiseActor> promise; + td::FutureActor> future; + td::init_promise_future(&promise, &future); + future.set_event(td::EventCreator::yield(actor_id())); + auto promised_query = PromisedQueryPtr(query.release(), PromiseDeleter(std::move(promise))); + send_closure(client_manager_, &ClientManager::send, std::move(promised_query)); + result_ = std::move(future); +} + +void HttpConnection::wakeup() { + if (result_.empty()) { + return; + } + LOG_CHECK(result_.is_ok()) << result_.move_as_error(); + + auto query = result_.move_as_ok(); + send_response(query->http_status_code(), std::move(query->answer()), query->retry_after()); +} + +void HttpConnection::send_response(int http_status_code, td::BufferSlice &&content, int retry_after) { + td::HttpHeaderCreator hc; + hc.init_status_line(http_status_code); + hc.set_keep_alive(); + hc.set_content_type("application/json"); + if (retry_after > 0) { + hc.add_header("Retry-After", PSLICE() << retry_after); + } + hc.set_content_size(content.size()); + + auto r_header = hc.finish(); + LOG(DEBUG) << "Response headers: " << r_header.ok(); + if (r_header.is_error()) { + LOG(ERROR) << "Bad response headers"; + send_closure(std::move(connection_), &td::HttpInboundConnection::write_error, r_header.move_as_error()); + return; + } + LOG(DEBUG) << "Send result: " << content; + + send_closure(connection_, &td::HttpInboundConnection::write_next_noflush, td::BufferSlice(r_header.ok())); + send_closure(connection_, &td::HttpInboundConnection::write_next_noflush, std::move(content)); + send_closure(std::move(connection_), &td::HttpInboundConnection::write_ok); +} + +void HttpConnection::send_http_error(int http_status_code, td::Slice description) { + send_response(http_status_code, td::json_encode(JsonQueryError(http_status_code, description)), 0); +} + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/HttpConnection.h b/telegram-bot-api/HttpConnection.h new file mode 100644 index 0000000..8d583db --- /dev/null +++ b/telegram-bot-api/HttpConnection.h @@ -0,0 +1,53 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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) +// +#pragma once + +#include "telegram-bot-api/ClientManager.h" +#include "telegram-bot-api/Query.h" + +#include "td/actor/actor.h" +#include "td/actor/PromiseFuture.h" + +#include "td/net/HttpInboundConnection.h" +#include "td/net/HttpQuery.h" + +#include "td/utils/buffer.h" +#include "td/utils/Slice.h" + +#include + +namespace telegram_bot_api { + +struct SharedData; + +class HttpConnection : public td::HttpInboundConnection::Callback { + public: + explicit HttpConnection(td::ActorId client_manager, std::shared_ptr shared_data) + : client_manager_(client_manager), shared_data_(std::move(shared_data)) { + } + + void handle(td::unique_ptr http_query, td::ActorOwn connection) override; + + void wakeup() override; + + private: + td::FutureActor> result_; + td::ActorId client_manager_; + td::ActorOwn connection_; + std::shared_ptr shared_data_; + + void hangup() override { + connection_.release(); + stop(); + } + + void send_response(int http_status_code, td::BufferSlice &&content, int retry_after); + + void send_http_error(int http_status_code, td::Slice description); +}; + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/HttpServer.h b/telegram-bot-api/HttpServer.h new file mode 100644 index 0000000..6bfcdff --- /dev/null +++ b/telegram-bot-api/HttpServer.h @@ -0,0 +1,72 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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) +// +#pragma once + +#include "td/actor/actor.h" + +#include "td/net/HttpInboundConnection.h" +#include "td/net/TcpListener.h" + +#include "td/utils/FloodControlFast.h" +#include "td/utils/format.h" +#include "td/utils/logging.h" +#include "td/utils/port/SocketFd.h" +#include "td/utils/Time.h" + +#include + +namespace telegram_bot_api { + +class HttpServer : public td::TcpListener::Callback { + public: + HttpServer(int port, std::function()> creator) + : port_(port), creator_(std::move(creator)) { + flood_control_.add_limit(1, 1); // 1 in a second + flood_control_.add_limit(60, 10); // 10 in a minute + } + + private: + td::int32 port_; + std::function()> creator_; + td::ActorOwn listener_; + td::FloodControlFast flood_control_; + + void start_up() override { + auto now = td::Time::now(); + auto wakeup_at = flood_control_.get_wakeup_at(); + if (wakeup_at > now) { + set_timeout_at(wakeup_at); + return; + } + flood_control_.add_event(static_cast(now)); + LOG(INFO) << "Create tcp listener " << td::tag("port", port_); + listener_ = td::create_actor(PSLICE() << "TcpListener" << td::tag("port", port_), port_, + actor_shared(this, 1)); + } + void hangup_shared() override { + LOG(ERROR) << "TCP listener was closed"; + listener_.release(); + yield(); + } + void accept(td::SocketFd fd) override { + auto scheduler_count = td::Scheduler::instance()->sched_count(); + auto scheduler_id = scheduler_count - 1; + if (scheduler_id > 0) { + scheduler_id--; + } + td::create_actor("HttpInboundConnection", std::move(fd), 0, 20, 500, creator_(), + scheduler_id) + .release(); + } + void loop() override { + if (listener_.empty()) { + start_up(); + } + } +}; + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/HttpStatConnection.cpp b/telegram-bot-api/HttpStatConnection.cpp new file mode 100644 index 0000000..5d666b2 --- /dev/null +++ b/telegram-bot-api/HttpStatConnection.cpp @@ -0,0 +1,53 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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 "telegram-bot-api/HttpStatConnection.h" + +#include "td/net/HttpHeaderCreator.h" + +#include "td/utils/common.h" +#include "td/utils/logging.h" + +namespace telegram_bot_api { + +void HttpStatConnection::handle(td::unique_ptr http_query, + td::ActorOwn connection) { + CHECK(connection_->empty()); + connection_ = std::move(connection); + + td::PromiseActor promise; + td::FutureActor future; + init_promise_future(&promise, &future); + future.set_event(td::EventCreator::yield(actor_id())); + LOG(DEBUG) << "SEND"; + send_closure(client_manager_, &ClientManager::get_stats, std::move(promise), http_query->get_args()); + result_ = std::move(future); +} + +void HttpStatConnection::wakeup() { + if (result_.empty()) { + return; + } + LOG_CHECK(result_.is_ok()) << result_.move_as_error(); + + auto content = result_.move_as_ok(); + td::HttpHeaderCreator hc; + hc.init_status_line(200); + hc.set_keep_alive(); + hc.set_content_type("text/plain"); + hc.set_content_size(content.size()); + + auto r_header = hc.finish(); + if (r_header.is_error()) { + send_closure(connection_.release(), &td::HttpInboundConnection::write_error, r_header.move_as_error()); + return; + } + send_closure(connection_, &td::HttpInboundConnection::write_next_noflush, td::BufferSlice(r_header.ok())); + send_closure(connection_, &td::HttpInboundConnection::write_next_noflush, std::move(content)); + send_closure(connection_.release(), &td::HttpInboundConnection::write_ok); +} + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/HttpStatConnection.h b/telegram-bot-api/HttpStatConnection.h new file mode 100644 index 0000000..0574309 --- /dev/null +++ b/telegram-bot-api/HttpStatConnection.h @@ -0,0 +1,40 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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) +// +#pragma once + +#include "telegram-bot-api/ClientManager.h" + +#include "td/actor/actor.h" +#include "td/actor/PromiseFuture.h" + +#include "td/net/HttpInboundConnection.h" +#include "td/net/HttpQuery.h" + +#include "td/utils/buffer.h" + +namespace telegram_bot_api { + +class HttpStatConnection : public td::HttpInboundConnection::Callback { + public: + explicit HttpStatConnection(td::ActorId client_manager) : client_manager_(client_manager) { + } + void handle(td::unique_ptr http_query, td::ActorOwn connection) override; + + void wakeup() override; + + private: + td::FutureActor result_; + td::ActorId client_manager_; + td::ActorOwn connection_; + + void hangup() override { + connection_.release(); + stop(); + } +}; + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/Query.cpp b/telegram-bot-api/Query.cpp new file mode 100644 index 0000000..41cb1ec --- /dev/null +++ b/telegram-bot-api/Query.cpp @@ -0,0 +1,119 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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 "telegram-bot-api/Query.h" + +#include "telegram-bot-api/Stats.h" + +#include "td/actor/actor.h" + +#include "td/utils/format.h" +#include "td/utils/logging.h" +#include "td/utils/misc.h" +#include "td/utils/port/IPAddress.h" +#include "td/utils/Time.h" + +#include + +namespace telegram_bot_api { + +std::unordered_map> empty_parameters; + +Query::Query(td::vector &&container, td::Slice token, bool is_test_dc, td::MutableSlice method, + td::vector> &&args, + td::vector> &&headers, td::vector &&files, + std::shared_ptr shared_data, const td::IPAddress &peer_address) + : state_(State::Query) + , shared_data_(shared_data) + , peer_address_(peer_address) + , container_(std::move(container)) + , token_(token) + , is_test_dc_(is_test_dc) + , method_(method) + , args_(std::move(args)) + , headers_(std::move(headers)) + , files_(std::move(files)) { + if (method_.empty()) { + method_ = arg("method"); + } + td::to_lower_inplace(method_); + start_timestamp_ = td::Time::now(); + LOG(INFO) << "QUERY: create " << td::tag("ptr", this) << *this; + if (shared_data_) { + shared_data_->query_count_++; + if (method_ != "getupdates") { + shared_data_->query_list_.put(this); + } + } +} + +td::int64 Query::query_size() const { + return std::accumulate( + container_.begin(), container_.end(), td::int64{0}, + [](td::int64 acc, const td::BufferSlice &slice) { return static_cast(acc + slice.size()); }); +} + +td::int64 Query::files_size() const { + return std::accumulate(files_.begin(), files_.end(), td::int64{0}, + [](td::int64 acc, const td::HttpFile &file) { return acc + file.size; }); +} + +td::int64 Query::files_max_size() const { + return std::accumulate(files_.begin(), files_.end(), td::int64{0}, + [](td::int64 acc, const td::HttpFile &file) { return td::max(acc, file.size); }); +} + +void Query::set_ok(td::BufferSlice result) { + CHECK(state_ == State::Query); + LOG(INFO) << "QUERY: got ok " << td::tag("ptr", this) << td::tag("text", result.as_slice()); + answer_ = std::move(result); + state_ = State::OK; + http_status_code_ = 200; + send_response_stat(); +} + +void Query::set_error(int http_status_code, td::BufferSlice result) { + LOG(INFO) << "QUERY: got error " << td::tag("ptr", this) << td::tag("code", http_status_code) + << td::tag("text", result.as_slice()); + CHECK(state_ == State::Query); + answer_ = std::move(result); + state_ = State::Error; + http_status_code_ = http_status_code; + send_response_stat(); +} + +void Query::set_retry_after_error(int retry_after) { + retry_after_ = retry_after; + + std::unordered_map> parameters; + parameters.emplace("retry_after", std::make_unique(retry_after)); + set_error(429, td::json_encode( + JsonQueryError(429, PSLICE() << "Too Many Requests: retry after " << retry_after, parameters))); +} + +td::StringBuilder &operator<<(td::StringBuilder &sb, const Query &query) { + auto padded_time = + td::lpad(PSTRING() << td::format::as_time(td::Time::now_cached() - query.start_timestamp()), 10, ' '); + sb << "[bot" << td::rpad(query.token().str(), 46, ' ') << "][time:" << padded_time << ']' + << td::tag("method", td::lpad(query.method().str(), 20, ' ')); + if (!query.args().empty()) { + sb << td::oneline(PSLICE() << query.args()); + } + if (!query.files().empty()) { + sb << query.files(); + } + return sb; +} + +void Query::send_response_stat() { + if (stat_actor_.empty()) { + return; + } + send_closure(stat_actor_, &BotStatActor::add_event, + ServerBotStat::Response{is_ok(), answer().size()}, td::Time::now()); +} + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/Query.h b/telegram-bot-api/Query.h new file mode 100644 index 0000000..24bdac3 --- /dev/null +++ b/telegram-bot-api/Query.h @@ -0,0 +1,278 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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) +// +#pragma once + +#include "telegram-bot-api/ClientParameters.h" + +#include "td/actor/actor.h" +#include "td/actor/PromiseFuture.h" + +#include "td/net/HttpFile.h" + +#include "td/utils/buffer.h" +#include "td/utils/common.h" +#include "td/utils/JsonBuilder.h" +#include "td/utils/List.h" +#include "td/utils/port/IPAddress.h" +#include "td/utils/Slice.h" +#include "td/utils/StringBuilder.h" + +#include +#include +#include +#include + +namespace telegram_bot_api { + +class BotStatActor; + +class Query : public td::ListNode { + public: + enum class State : td::int8 { Query, OK, Error }; + + td::Slice token() const { + return token_; + } + bool is_test_dc() const { + return is_test_dc_; + } + td::Slice method() const { + return method_; + } + bool has_arg(td::Slice key) const { + auto it = std::find_if(args_.begin(), args_.end(), + [&key](const std::pair &s) { return s.first == key; }); + return it != args_.end(); + } + td::MutableSlice arg(td::Slice key) const { + auto it = std::find_if(args_.begin(), args_.end(), + [&key](const std::pair &s) { return s.first == key; }); + return it == args_.end() ? td::MutableSlice() : it->second; + } + const td::vector> &args() const { + return args_; + } + td::Slice get_header(td::Slice key) const { + auto it = std::find_if(headers_.begin(), headers_.end(), + [&key](const std::pair &s) { return s.first == key; }); + return it == headers_.end() ? td::Slice() : it->second; + } + const td::HttpFile *file(td::Slice key) const { + auto it = std::find_if(files_.begin(), files_.end(), [&key](const td::HttpFile &f) { return f.field_name == key; }); + return it == files_.end() ? nullptr : &*it; + } + const td::vector &files() const { + return files_; + } + + const td::IPAddress &peer_address() const { + return peer_address_; + } + + // for stats + td::int32 file_count() const { + return static_cast(files_.size()); + } + + td::int64 query_size() const; + + td::int64 files_size() const; + + td::int64 files_max_size() const; + + td::BufferSlice &answer() { + return answer_; + } + + int http_status_code() const { + return http_status_code_; + } + + int retry_after() const { + return retry_after_; + } + + void set_ok(td::BufferSlice result); + + void set_error(int http_status_code, td::BufferSlice result); + + void set_retry_after_error(int retry_after); + + bool is_ready() const { + return state_ != State::Query; + } + + bool is_error() const { + return state_ == State::Error; + } + + bool is_ok() const { + return state_ == State::OK; + } + + bool is_internal() const { + return is_internal_; + } + + void set_internal(bool is_internal) { + is_internal_ = is_internal; + } + + Query(td::vector &&container, td::Slice token, bool is_test_dc, td::MutableSlice method, + td::vector> &&args, + td::vector> &&headers, td::vector &&files, + std::shared_ptr shared_data, const td::IPAddress &peer_address); + Query(const Query &) = delete; + Query &operator=(const Query &) = delete; + Query(Query &&) = delete; + Query &operator=(Query &&) = delete; + ~Query() { + if (shared_data_) { + shared_data_->query_count_--; + } + } + + double start_timestamp() const { + return start_timestamp_; + } + + void set_stat_actor(td::ActorId stat_actor) { + stat_actor_ = stat_actor; + } + void send_response_stat(); + + private: + State state_; + std::shared_ptr shared_data_; + double start_timestamp_; + td::IPAddress peer_address_; + td::ActorId stat_actor_; + + // request + td::vector container_; + td::Slice token_; + bool is_test_dc_; + td::MutableSlice method_; + td::vector> args_; + td::vector> headers_; + td::vector files_; + bool is_internal_ = false; + + // response + td::BufferSlice answer_; + int http_status_code_ = 0; + int retry_after_ = 0; +}; + +td::StringBuilder &operator<<(td::StringBuilder &sb, const Query &query); + +// fix for outdated C++14 libraries +// https://stackoverflow.com/questions/26947704/implicit-conversion-failure-from-initializer-list +extern std::unordered_map> empty_parameters; + +class JsonParameters : public td::Jsonable { + public: + explicit JsonParameters(const std::unordered_map> ¶meters) + : parameters_(parameters) { + } + void store(td::JsonValueScope *scope) const { + auto object = scope->enter_object(); + for (auto ¶meter : parameters_) { + CHECK(parameter.second != nullptr); + object(parameter.first, *parameter.second); + } + } + + private: + const std::unordered_map> ¶meters_; +}; + +template +class JsonQueryOk : public td::Jsonable { + public: + JsonQueryOk(const T &result, td::Slice description) : result_(result), description_(description) { + } + void store(td::JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("ok", td::JsonTrue()); + object("result", result_); + if (!description_.empty()) { + object("description", description_); + } + } + + private: + const T &result_; + td::Slice description_; +}; + +class JsonQueryError : public td::Jsonable { + public: + JsonQueryError( + int error_code, td::Slice description, + const std::unordered_map> ¶meters = empty_parameters) + : error_code_(error_code), description_(description), parameters_(parameters) { + } + void store(td::JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("ok", td::JsonFalse()); + object("error_code", error_code_); + object("description", description_); + if (!parameters_.empty()) { + object("parameters", JsonParameters(parameters_)); + } + } + + private: + int error_code_; + td::Slice description_; + const std::unordered_map> ¶meters_; +}; + +class PromiseDeleter { + public: + explicit PromiseDeleter(td::PromiseActor> &&promise) : promise_(std::move(promise)) { + } + PromiseDeleter() = default; + PromiseDeleter(const PromiseDeleter &) = delete; + PromiseDeleter &operator=(const PromiseDeleter &) = delete; + PromiseDeleter(PromiseDeleter &&) = default; + PromiseDeleter &operator=(PromiseDeleter &&) = default; + void operator()(Query *raw_ptr) { + td::unique_ptr query(raw_ptr); // now I cannot forget to delete this pointer + if (!promise_.empty_promise()) { + if (!query->is_ready()) { + query->set_retry_after_error(5); + } + + promise_.set_value(std::move(query)); + } + } + ~PromiseDeleter() { + CHECK(promise_.empty()); + } + + private: + td::PromiseActor> promise_; +}; +using PromisedQueryPtr = std::unique_ptr; + +template +void answer_query(const Jsonable &result, PromisedQueryPtr query, td::Slice description = td::Slice()) { + query->set_ok(td::json_encode(JsonQueryOk(result, description))); + query.reset(); // send query into promise explicitly +} + +inline void fail_query( + int http_status_code, td::Slice description, PromisedQueryPtr query, + const std::unordered_map> ¶meters = empty_parameters) { + query->set_error(http_status_code, + td::json_encode(JsonQueryError(http_status_code, description, parameters))); + query.reset(); // send query into promise explicitly +} + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/Stats.cpp b/telegram-bot-api/Stats.cpp new file mode 100644 index 0000000..b32d0de --- /dev/null +++ b/telegram-bot-api/Stats.cpp @@ -0,0 +1,159 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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 "telegram-bot-api/Stats.h" + +#include "td/utils/common.h" +#include "td/utils/logging.h" +#include "td/utils/port/thread.h" +#include "td/utils/StringBuilder.h" + +namespace telegram_bot_api { + +ServerCpuStat::ServerCpuStat() { + for (std::size_t i = 1; i < SIZE; i++) { + stat_[i] = td::TimedStat(DURATIONS[i], td::Time::now()); + } +} + +void ServerCpuStat::add_event(const td::CpuStat &cpu_stat, double now) { + std::lock_guard guard(mutex_); + for (auto &stat : stat_) { + stat.add_event(cpu_stat, now); + } +} + +td::string ServerCpuStat::get_description() const { + td::string res = "DURATION"; + for (auto &descr : DESCR) { + res += "\t"; + res += descr; + } + return res; +} + +static td::string to_percentage(td::uint64 ticks, td::uint64 total_ticks) { + static double multiplier = 100.0 * (td::thread::hardware_concurrency() ? td::thread::hardware_concurrency() : 1); + return PSTRING() << (static_cast(ticks) / static_cast(total_ticks) * multiplier) << "%"; +} + +td::vector CpuStat::as_vector() const { + td::vector res; + if (cnt_ < 2 || first_.total_ticks_ >= last_.total_ticks_) { + res.push_back({"total_cpu", "UNKNOWN"}); + res.push_back({"user_cpu", "UNKNOWN"}); + res.push_back({"system_cpu", "UNKNOWN"}); + } else { + auto total_ticks = last_.total_ticks_ - first_.total_ticks_; + auto user_ticks = last_.process_user_ticks_ - first_.process_user_ticks_; + auto system_ticks = last_.process_system_ticks_ - first_.process_system_ticks_; + res.push_back({"total_cpu", to_percentage(user_ticks + system_ticks, total_ticks)}); + res.push_back({"user_cpu", to_percentage(user_ticks, total_ticks)}); + res.push_back({"system_cpu", to_percentage(system_ticks, total_ticks)}); + } + return res; +} + +td::vector ServerCpuStat::as_vector(double now) { + std::lock_guard guard(mutex_); + + td::vector res = stat_[0].get_stat(now).as_vector(); + for (std::size_t i = 1; i < SIZE; i++) { + auto other = stat_[i].get_stat(now).as_vector(); + CHECK(other.size() == res.size()); + for (size_t j = 0; j < res.size(); j++) { + res[j].value_ += "\t"; + res[j].value_ += other[j].value_; + } + } + return res; +} + +constexpr int ServerCpuStat::DURATIONS[SIZE]; +constexpr const char *ServerCpuStat::DESCR[SIZE]; + +void ServerBotStat::normalize(double duration) { + if (duration == 0) { + return; + } + request_count_ /= duration; + request_bytes_ /= duration; + request_file_count_ /= duration; + request_files_bytes_ /= duration; + response_count_ /= duration; + response_count_ok_ /= duration; + response_count_error_ /= duration; + response_bytes_ /= duration; + update_count_ /= duration; +} + +void ServerBotStat::add(const ServerBotStat &stat) { + request_count_ += stat.request_count_; + request_bytes_ += stat.request_bytes_; + request_file_count_ += stat.request_file_count_; + request_files_bytes_ += stat.request_files_bytes_; + request_files_max_bytes_ = td::max(request_files_max_bytes_, stat.request_files_max_bytes_); + + response_count_ += stat.response_count_; + response_count_ok_ += stat.response_count_ok_; + response_count_error_ += stat.response_count_error_; + response_bytes_ += stat.response_bytes_; + + update_count_ += stat.update_count_; +} + +td::vector ServerBotStat::as_vector() const { + td::vector res; + auto add_item = [&res](td::string name, auto value) { + res.push_back({std::move(name), td::to_string(value)}); + }; + add_item("request_count", request_count_); + add_item("request_bytes", request_bytes_); + add_item("request_file_count", request_file_count_); + add_item("request_files_bytes", request_files_bytes_); + add_item("request_max_bytes", request_files_max_bytes_); + add_item("response_count", response_count_); + add_item("response_count_ok", response_count_ok_); + add_item("response_count_error", response_count_error_); + add_item("response_bytes", response_bytes_); + add_item("update_count", update_count_); + return res; +} + +td::vector BotStatActor::as_vector(double now) { + auto first_sd = stat_[0].stat_duration(now); + first_sd.first.normalize(first_sd.second); + td::vector res = first_sd.first.as_vector(); + for (std::size_t i = 1; i < SIZE; i++) { + auto next_sd = stat_[i].stat_duration(now); + next_sd.first.normalize(next_sd.second); + auto other = next_sd.first.as_vector(); + CHECK(other.size() == res.size()); + for (size_t j = 0; j < res.size(); j++) { + res[j].value_ += "\t"; + res[j].value_ += other[j].value_; + } + } + return res; +} + +td::string BotStatActor::get_description() const { + td::string res = "DURATION"; + for (auto &descr : DESCR) { + res += "\t"; + res += descr; + } + return res; +} + +bool BotStatActor::is_active(double now) const { + return last_activity_timestamp_ > now - 86400; +} + +constexpr int BotStatActor::DURATIONS[SIZE]; +constexpr const char *BotStatActor::DESCR[SIZE]; + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/Stats.h b/telegram-bot-api/Stats.h new file mode 100644 index 0000000..a47386c --- /dev/null +++ b/telegram-bot-api/Stats.h @@ -0,0 +1,197 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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) +// +#pragma once + +#include "td/actor/actor.h" + +#include "td/utils/common.h" +#include "td/utils/logging.h" +#include "td/utils/port/Stat.h" +#include "td/utils/Time.h" +#include "td/utils/TimedStat.h" + +#include + +namespace telegram_bot_api { + +struct StatItem { + td::string key_; + td::string value_; +}; + +class CpuStat { + public: + void on_event(const td::CpuStat &event) { + if (cnt_ == 0) { + first_ = event; + cnt_ = 1; + } else { + cnt_ = 2; + last_ = event; + } + } + + td::vector as_vector() const; + + private: + int cnt_ = 0; + td::CpuStat first_; + td::CpuStat last_; +}; + +class ServerCpuStat { + public: + static ServerCpuStat &instance() { + static ServerCpuStat stat; + return stat; + } + static void update(double now) { + auto r_event = td::cpu_stat(); + if (r_event.is_error()) { + return; + } + instance().add_event(r_event.ok(), now); + LOG(WARNING) << "CPU usage: " << instance().stat_[1].get_stat(now).as_vector()[0].value_; + } + + td::string get_description() const; + + td::vector as_vector(double now); + + private: + static constexpr std::size_t SIZE = 4; + static constexpr const char *DESCR[SIZE] = {"inf", "5sec", "1min", "1hour"}; + static constexpr int DURATIONS[SIZE] = {0, 5, 60, 60 * 60}; + + std::mutex mutex_; + td::TimedStat stat_[SIZE]; + + ServerCpuStat(); + + void add_event(const td::CpuStat &stat, double now); +}; + +class ServerBotInfo { + public: + td::string id_; + td::string token_; + td::string username_; + td::string webhook_; + bool has_webhook_certificate_ = false; + td::int32 head_update_id_ = 0; + td::int32 tail_update_id_ = 0; + td::int32 webhook_max_connections_ = 0; + std::size_t pending_update_count_ = 0; + double start_timestamp_ = 0; + double last_query_timestamp_ = 0; +}; + +struct ServerBotStat { + double request_count_ = 0; + double request_bytes_ = 0; + double request_file_count_ = 0; + double request_files_bytes_ = 0; + td::int64 request_files_max_bytes_ = 0; + + double response_count_ = 0; + double response_count_ok_ = 0; + double response_count_error_ = 0; + double response_bytes_ = 0; + + double update_count_ = 0; + + void normalize(double duration); + + void add(const ServerBotStat &stat); + + struct Update {}; + void on_event(const Update &update) { + update_count_ += 1; + } + + struct Response { + bool ok_; + size_t size_; + }; + void on_event(const Response &answer) { + response_count_++; + if (answer.ok_) { + response_count_ok_++; + } else { + response_count_error_++; + } + response_bytes_ += static_cast(answer.size_); + } + + struct Request { + td::int64 size_; + td::int64 file_count_; + td::int64 files_size_; + td::int64 files_max_size_; + }; + void on_event(const Request &request) { + request_count_++; + request_bytes_ += static_cast(request.size_); + request_file_count_ += static_cast(request.file_count_); + request_files_bytes_ += static_cast(request.files_size_); + request_files_max_bytes_ = td::max(request_files_max_bytes_, request.files_max_size_); + } + + td::vector as_vector() const; +}; + +class BotStatActor final : public td::Actor { + public: + BotStatActor() = default; + explicit BotStatActor(td::ActorId parent) : parent_(parent) { + for (std::size_t i = 0; i < SIZE; i++) { + stat_[i] = td::TimedStat(DURATIONS[i], td::Time::now()); + } + register_actor("ServerBotStat", this).release(); + } + + BotStatActor(const BotStatActor &) = delete; + BotStatActor &operator=(const BotStatActor &other) = delete; + BotStatActor(BotStatActor &&) = default; + BotStatActor &operator=(BotStatActor &&other) { + if (!empty()) { + do_stop(); + } + this->Actor::operator=(std::move(other)); + std::move(other.stat_, other.stat_ + SIZE, stat_); + parent_ = other.parent_; + return *this; + } + ~BotStatActor() override = default; + + template + void add_event(const EventT &event, double now) { + last_activity_timestamp_ = now; + for (auto &stat : stat_) { + stat.add_event(event, now); + } + if (!parent_.empty()) { + send_closure(parent_, &BotStatActor::add_event, event, now); + } + } + + td::vector as_vector(double now); + td::string get_description() const; + + bool is_active(double now) const; + + private: + static constexpr std::size_t SIZE = 4; + static constexpr const char *DESCR[SIZE] = {"inf", "5sec", "1min", "1hour"}; + static constexpr int DURATIONS[SIZE] = {0, 5, 60, 60 * 60}; + + td::TimedStat stat_[SIZE]; + td::ActorId parent_; + double last_activity_timestamp_ = -1e9; +}; + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/WebhookActor.cpp b/telegram-bot-api/WebhookActor.cpp new file mode 100644 index 0000000..be8ef2e --- /dev/null +++ b/telegram-bot-api/WebhookActor.cpp @@ -0,0 +1,751 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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 "telegram-bot-api/WebhookActor.h" + +#include "telegram-bot-api/ClientParameters.h" + +#include "td/db/TQueue.h" + +#include "td/net/GetHostByNameActor.h" +#include "td/net/HttpHeaderCreator.h" +#include "td/net/HttpProxy.h" +#include "td/net/SslStream.h" +#include "td/net/TransparentProxy.h" + +#include "td/actor/actor.h" +#include "td/actor/PromiseFuture.h" + +#include "td/utils/base64.h" +#include "td/utils/buffer.h" +#include "td/utils/common.h" +#include "td/utils/format.h" +#include "td/utils/JsonBuilder.h" +#include "td/utils/logging.h" +#include "td/utils/misc.h" +#include "td/utils/port/Clocks.h" +#include "td/utils/port/IPAddress.h" +#include "td/utils/port/SocketFd.h" +#include "td/utils/Random.h" +#include "td/utils/ScopeGuard.h" +#include "td/utils/Span.h" +#include "td/utils/Time.h" + +#include + +namespace telegram_bot_api { + +static int VERBOSITY_NAME(webhook) = VERBOSITY_NAME(DEBUG); + +std::atomic WebhookActor::total_connections_count_{0}; + +WebhookActor::WebhookActor(td::ActorShared callback, td::int64 tqueue_id, td::HttpUrl url, + td::string cert_path, td::int32 max_connections, bool from_db_flag, + td::string cached_ip_address, bool fix_ip_address, + std::shared_ptr parameters) + : callback_(std::move(callback)) + , tqueue_id_(tqueue_id) + , url_(std::move(url)) + , cert_path_(std::move(cert_path)) + , parameters_(std::move(parameters)) + , fix_ip_address_(fix_ip_address) + , from_db_flag_(from_db_flag) + , max_connections_(max_connections) { + CHECK(max_connections_ > 0); + + if (!cached_ip_address.empty()) { + auto r_ip_address = td::IPAddress::get_ip_address(cached_ip_address); + if (r_ip_address.is_ok()) { + ip_address_ = r_ip_address.move_as_ok(); + ip_address_.set_port(url_.port_); + } + } + + auto r_ascii_host = td::idn_to_ascii(url_.host_); + if (r_ascii_host.is_ok()) { + url_.host_ = r_ascii_host.move_as_ok(); + } + + LOG(INFO) << "Set webhook for " << tqueue_id << " with certificate = \"" << cert_path_ + << "\", protocol = " << (url_.protocol_ == td::HttpUrl::Protocol::Http ? "http" : "https") + << ", host = " << url_.host_ << ", port = " << url_.port_ << ", query = " << url_.query_ + << ", max_connections = " << max_connections_; +} + +void WebhookActor::relax_wakeup_at(double wakeup_at, const char *source) { + if (wakeup_at_ == 0 || wakeup_at < wakeup_at_) { + VLOG(webhook) << "Wake up in " << wakeup_at - td::Time::now() << " from " << source; + wakeup_at_ = wakeup_at; + } +} + +void WebhookActor::resolve_ip_address() { + if (fix_ip_address_) { + return; + } + if (td::Time::now() < next_ip_address_resolve_timestamp_) { + relax_wakeup_at(next_ip_address_resolve_timestamp_, "resolve_ip_address"); + return; + } + + bool future_created = false; + if (future_ip_address_.empty()) { + td::PromiseActor promise; + init_promise_future(&promise, &future_ip_address_); + future_created = true; + send_closure(parameters_->get_host_by_name_actor_id_, &td::GetHostByNameActor::run, url_.host_, url_.port_, false, + td::PromiseCreator::from_promise_actor(std::move(promise))); + } + + if (future_ip_address_.is_ready()) { + next_ip_address_resolve_timestamp_ = + td::Time::now() + IP_ADDRESS_CACHE_TIME + td::Random::fast(0, IP_ADDRESS_CACHE_TIME / 10); + relax_wakeup_at(next_ip_address_resolve_timestamp_, "resolve_ip_address"); + + auto r_ip_address = future_ip_address_.move_as_result(); + if (r_ip_address.is_error()) { + CHECK(!(r_ip_address.error() == td::Status::Error::HANGUP_ERROR_CODE>())); + return on_error(r_ip_address.move_as_error()); + } + auto new_ip_address = r_ip_address.move_as_ok(); + if (!check_ip_address(new_ip_address)) { + return on_error(td::Status::Error(PSLICE() << "IP address " << new_ip_address.get_ip_str() << " is reserved")); + } + if (!(ip_address_ == new_ip_address)) { + VLOG(webhook) << "IP address has changed: " << ip_address_ << " --> " << new_ip_address; + ip_address_ = new_ip_address; + ip_generation_++; + if (was_checked_) { + on_webhook_verified(); + } + } + VLOG(webhook) << "IP address was verified"; + } else { + if (future_created) { + future_ip_address_.set_event(td::EventCreator::yield(actor_id())); + } + } +} + +td::Status WebhookActor::create_connection() { + if (!ip_address_.is_valid()) { + VLOG(webhook) << "Can't create connection: IP address is not ready"; + return td::Status::Error("IP address is not ready"); + } + if (parameters_->webhook_proxy_ip_address_.is_valid()) { + auto r_proxy_socket_fd = td::SocketFd::open(parameters_->webhook_proxy_ip_address_); + if (r_proxy_socket_fd.is_error()) { + td::Slice error_message = "Can't connect to the webhook proxy"; + auto error = td::Status::Error(PSLICE() << error_message << ": " << r_proxy_socket_fd.error()); + VLOG(webhook) << error; + on_webhook_error(error_message); + on_error(td::Status::Error(error_message)); + return error; + } + if (!was_checked_) { + TRY_STATUS(create_ssl_stream()); // check certificate + + // verify webhook even we can't establish connection to the webhook + was_checked_ = true; + on_webhook_verified(); + } + + VLOG(webhook) << "Create connection through proxy " << parameters_->webhook_proxy_ip_address_; + class Callback : public td::TransparentProxy::Callback { + public: + Callback(td::ActorId actor, td::int64 id) : actor_(actor), id_(id) { + } + void set_result(td::Result result) override { + send_closure(std::move(actor_), &WebhookActor::on_socket_ready_async, std::move(result), id_); + CHECK(actor_.empty()); + } + Callback(const Callback &) = delete; + Callback &operator=(const Callback &) = delete; + Callback(Callback &&) = delete; + Callback &operator=(Callback &&) = delete; + ~Callback() { + if (!actor_.empty()) { + send_closure(std::move(actor_), &WebhookActor::on_socket_ready_async, td::Status::Error("Cancelled"), id_); + } + } + void on_connected() override { + // nothing to do + } + + private: + td::ActorId actor_; + td::int64 id_; + }; + + auto id = pending_sockets_.create(td::ActorOwn<>()); + VLOG(webhook) << "Creating socket " << id; + *pending_sockets_.get(id) = td::create_actor( + "HttpProxy", r_proxy_socket_fd.move_as_ok(), ip_address_, td::string(), td::string(), + td::make_unique(actor_id(this), id), td::ActorShared<>()); + return td::Status::Error("Proxy connection is not ready"); + } + + auto r_fd = td::SocketFd::open(ip_address_); + if (r_fd.is_error()) { + td::Slice error_message = "Can't connect to the webhook"; + auto error = td::Status::Error(PSLICE() << error_message << ": " << r_fd.error()); + VLOG(webhook) << error; + on_webhook_error(error_message); + on_error(r_fd.move_as_error()); + return error; + } + return create_connection(r_fd.move_as_ok()); +} + +td::Result WebhookActor::create_ssl_stream() { + if (url_.protocol_ == td::HttpUrl::Protocol::Http) { + return td::SslStream(); + } + + auto r_ssl_stream = td::SslStream::create(url_.host_, cert_path_, td::SslStream::VerifyPeer::On, !cert_path_.empty()); + if (r_ssl_stream.is_error()) { + td::Slice error_message = "Can't create an SSL connection"; + auto error = td::Status::Error(PSLICE() << error_message << ": " << r_ssl_stream.error()); + VLOG(webhook) << error; + on_webhook_error(PSLICE() << error_message << ": " << r_ssl_stream.error().public_message()); + on_error(r_ssl_stream.move_as_error()); + return std::move(error); + } + return r_ssl_stream.move_as_ok(); +} + +td::Status WebhookActor::create_connection(td::SocketFd fd) { + TRY_RESULT(ssl_stream, create_ssl_stream()); + + auto id = connections_.create(Connection()); + auto *conn = connections_.get(id); + conn->actor_id_ = td::create_actor( + PSLICE() << "Connect:" << id, std::move(fd), std::move(ssl_stream), std::numeric_limits::max(), 20, 60, + td::ActorShared(actor_id(this), id)); + conn->ip_generation_ = ip_generation_; + conn->event_id_ = {}; + conn->id_ = id; + ready_connections_.put(conn->to_list_node()); + total_connections_count_.fetch_add(1, std::memory_order_relaxed); + + if (!was_checked_) { + was_checked_ = true; + on_webhook_verified(); + } + VLOG(webhook) << "Create connection " << id; + return td::Status::OK(); +} + +void WebhookActor::on_socket_ready_async(td::Result r_fd, td::int64 id) { + pending_sockets_.erase(id); + if (r_fd.is_ok()) { + VLOG(webhook) << "Socket " << id << " is ready"; + ready_sockets_.push_back(r_fd.move_as_ok()); + } else { + VLOG(webhook) << "Failed to open socket " << id; + on_webhook_error(r_fd.error().message()); + on_error(r_fd.move_as_error()); + } + loop(); +} + +void WebhookActor::create_new_connections() { + size_t need_connections = queues_.size(); + if (need_connections > static_cast(max_connections_)) { + need_connections = max_connections_; + } + if (!was_checked_) { + need_connections = 1; + } + + auto now = td::Time::now(); + td::FloodControlFast *flood; + bool active; + if (last_success_timestamp_ + 10 < now) { + flood = &pending_new_connection_flood_; + if (need_connections > 1) { + need_connections = 1; + } + active = false; + } else { + flood = &active_new_connection_flood_; + if (need_connections == 0) { + need_connections = 1; + } + active = true; + } + VLOG_IF(webhook, connections_.size() < need_connections) + << "Create new connections " << td::tag("have", connections_.size()) << td::tag("need", need_connections) + << td::tag("pending sockets", pending_sockets_.size()) << td::tag("ready sockets", ready_sockets_.size()) + << td::tag("active", active); + while (connections_.size() + pending_sockets_.size() + ready_sockets_.size() < need_connections) { + auto wakeup_at = flood->get_wakeup_at(); + if (now < wakeup_at) { + relax_wakeup_at(wakeup_at, "create_new_connections"); + VLOG(webhook) << "Create new connection: flood control " + << td::tag("after", td::format::as_time(wakeup_at - now)); + break; + } + flood->add_event(static_cast(now)); + if (create_connection().is_error()) { + relax_wakeup_at(now + 1.0, "create_new_connections error"); + return; + } + } + SCOPE_EXIT { + ready_sockets_.clear(); + }; + while (!ready_sockets_.empty() && connections_.size() + pending_sockets_.size() < need_connections) { + auto socket_fd = std::move(ready_sockets_.back()); + ready_sockets_.pop_back(); + if (create_connection(std::move(socket_fd)).is_error()) { + relax_wakeup_at(now + 1.0, "create_new_connections error 2"); + return; + } + } +} + +void WebhookActor::loop() { + VLOG(webhook) << "Enter loop"; + wakeup_at_ = 0; + if (!stop_flag_) { + load_updates(); + } + if (!stop_flag_) { + resolve_ip_address(); + } + if (!stop_flag_) { + create_new_connections(); + } + if (!stop_flag_) { + send_updates(); + } + if (!stop_flag_) { + if (wakeup_at_ != 0) { + set_timeout_at(wakeup_at_); + } + } + if (stop_flag_) { + VLOG(webhook) << "Stop"; + stop(); + } +} + +void WebhookActor::update() { + VLOG(webhook) << "New updates in tqueue"; + tqueue_empty_ = false; + loop(); +} + +void WebhookActor::load_updates() { + if (tqueue_empty_) { + VLOG(webhook) << "Load updates: tqueue is empty"; + return; + } + if (queue_updates_.size() >= max_loaded_updates_) { + CHECK(queue_updates_.size() == max_loaded_updates_); + VLOG(webhook) << "Load updates: maximum allowed number of updates is already loaded"; + return; + } + auto &tqueue = parameters_->shared_data_->tqueue_; + if (tqueue_offset_.empty()) { + tqueue_offset_ = tqueue->get_head(tqueue_id_); + } + VLOG(webhook) << "Trying to load new updates from offset " << tqueue_offset_; + + auto limit = td::min(SharedData::TQUEUE_EVENT_BUFFER_SIZE, max_loaded_updates_ - queue_updates_.size()); + auto updates = mutable_span(parameters_->shared_data_->event_buffer_, limit); + + auto now = td::Time::now(); + auto unix_time_now = parameters_->shared_data_->get_unix_time(now); + size_t total_size = 0; + if (tqueue_offset_.empty()) { + updates.truncate(0); + } else { + auto r_size = tqueue->get(tqueue_id_, tqueue_offset_, false, unix_time_now, updates); + if (r_size.is_error()) { + VLOG(webhook) << "Failed to get new updates: " << r_size.error(); + tqueue_offset_ = tqueue->get_head(tqueue_id_); + r_size = tqueue->get(tqueue_id_, tqueue_offset_, false, unix_time_now, updates); + r_size.ensure(); + } + total_size = r_size.ok(); + } + if (updates.empty()) { + tqueue_empty_ = true; + } + + for (auto &update : updates) { + VLOG(webhook) << "Load update " << update.id; + if (update_map_.find(update.id) != update_map_.end()) { + LOG(ERROR) << "Receive duplicated event from tqueue " << update.id; + continue; + } + auto &dest = update_map_[update.id]; + dest.id_ = update.id; + dest.json_ = update.data.str(); + dest.state_ = Update::State::Begin; + dest.delay_ = 1; + dest.wakeup_at_ = now; + CHECK(update.expires_at >= unix_time_now); + dest.expires_at_ = update.expires_at; + dest.queue_id_ = update.extra; + tqueue_offset_ = update.id.next().move_as_ok(); + begin_updates_n_++; + + if (dest.queue_id_ == 0) { + dest.queue_id_ = unique_queue_id_++; + } + + auto &queue_updates = queue_updates_[dest.queue_id_]; + if (queue_updates.event_ids.empty()) { + queues_.emplace(dest.wakeup_at_, dest.queue_id_); + } + queue_updates.event_ids.push(dest.id_); + } + + bool need_warning = false; + if (total_size <= MIN_PENDING_UPDATES_WARNING / 2) { + if (last_pending_update_count_ > MIN_PENDING_UPDATES_WARNING) { + need_warning = true; + last_pending_update_count_ = MIN_PENDING_UPDATES_WARNING; + } + } else if (total_size >= last_pending_update_count_) { + need_warning = true; + while (total_size >= last_pending_update_count_) { + last_pending_update_count_ *= 2; + } + } + if (need_warning) { + LOG(WARNING) << "Found " << updates.size() << " updates out of " << total_size - update_map_.size() << " + " + << update_map_.size() << " in " << queues_.size() << " queues after last error \"" + << last_error_message_ << "\" at " << last_error_date_; + } + + if (updates.size() == total_size && last_update_was_successful_) { + send_closure(callback_, &Callback::webhook_success); + } + + if (!updates.empty()) { + VLOG(webhook) << "Loaded " << updates.size() << " new updates from offset " << tqueue_offset_ + << " out of requested " << limit << ". Have total of " << update_map_.size() << " updates loaded in " + << queue_updates_.size() << " queues"; + } +} + +void WebhookActor::drop_event(td::TQueue::EventId event_id) { + auto it = update_map_.find(event_id); + auto queue_id = it->second.queue_id_; + update_map_.erase(it); + + auto queue_updates_it = queue_updates_.find(queue_id); + + CHECK(!queue_updates_it->second.event_ids.empty()); + CHECK(event_id == queue_updates_it->second.event_ids.front()); + queue_updates_it->second.event_ids.pop(); + if (queue_updates_it->second.event_ids.empty()) { + queue_updates_.erase(queue_updates_it); + } else { + auto &update = update_map_[queue_updates_it->second.event_ids.front()]; + queues_.emplace(update.wakeup_at_, update.queue_id_); + } + + parameters_->shared_data_->tqueue_->forget(tqueue_id_, event_id); +} + +void WebhookActor::on_update_ok(td::TQueue::EventId event_id) { + last_update_was_successful_ = true; + last_success_timestamp_ = td::Time::now(); + VLOG(webhook) << "Receive ok for update " << event_id; + + drop_event(event_id); +} + +void WebhookActor::on_update_error(td::TQueue::EventId event_id, td::Slice error, int retry_after) { + last_update_was_successful_ = false; + double now = td::Time::now(); + + auto it = update_map_.find(event_id); + if (parameters_->shared_data_->get_unix_time(now) > it->second.expires_at_) { + LOG(WARNING) << "Drop update " << event_id << ": " << error; + drop_event(event_id); + return; + } + auto &update = it->second; + update.state_ = Update::State::Begin; + const int MAX_RETRY_AFTER = 3600; + retry_after = td::clamp(retry_after, 0, MAX_RETRY_AFTER); + update.delay_ = retry_after != 0 + ? retry_after + : td::min(WEBHOOK_MAX_RESEND_TIMEOUT, update.delay_ * (update.fail_count_ > 0 ? 2 : 1)); + update.wakeup_at_ = update.fail_count_ > 0 ? now + update.delay_ : now; + update.fail_count_++; + queues_.emplace(update.wakeup_at_, update.queue_id_); + VLOG(webhook) << "Delay update " << event_id << " for " << (update.wakeup_at_ - now) << " seconds because of " + << error << " after " << update.fail_count_ << " fails"; + begin_updates_n_++; +} + +td::Status WebhookActor::send_update() { + if (ready_connections_.empty()) { + return td::Status::Error("No connection"); + } + + if (queues_.empty()) { + return td::Status::Error("No updates"); + } + auto it = queues_.begin(); + if (it->wakeup_at > td::Time::now()) { + relax_wakeup_at(it->wakeup_at, "send_update"); + return td::Status::Error("No ready updates"); + } + + auto queue_id = it->id; + queues_.erase(it); + auto event_id = queue_updates_[queue_id].event_ids.front(); + + auto &update = update_map_[event_id]; + + auto body = td::json_encode(JsonUpdate(update.id_.value(), update.json_)); + + td::HttpHeaderCreator hc; + hc.init_post(url_.query_); + hc.add_header("Host", url_.host_); + if (!url_.userinfo_.empty()) { + hc.add_header("Authorization", PSLICE() << "Basic " << td::base64_encode(url_.userinfo_)); + } + hc.set_content_type("application/json"); + hc.set_content_size(body.size()); + hc.set_keep_alive(); + hc.add_header("Accept-Encoding", "gzip, deflate"); + auto r_header = hc.finish(); + if (r_header.is_error()) { + return td::Status::Error(400, "URL is too long"); + } + + auto &connection = *Connection::from_list_node(ready_connections_.get()); + begin_updates_n_--; + update.state_ = Update::State::Send; + connection.event_id_ = update.id_; + + VLOG(webhook) << "Send update " << update.id_ << " from queue " << queue_id << " into connection " << connection.id_ + << ": " << update.json_; + VLOG(webhook) << "Request headers: " << r_header.ok(); + + send_closure(connection.actor_id_, &td::HttpOutboundConnection::write_next_noflush, td::BufferSlice(r_header.ok())); + send_closure(connection.actor_id_, &td::HttpOutboundConnection::write_next_noflush, std::move(body)); + send_closure(connection.actor_id_, &td::HttpOutboundConnection::write_ok); + return td::Status::OK(); +} + +void WebhookActor::send_updates() { + VLOG(webhook) << "Have " << begin_updates_n_ << " pending updates to send"; + while (begin_updates_n_ > 0) { + if (send_update().is_error()) { + return; + } + } +} + +void WebhookActor::handle(td::unique_ptr response) { + auto connection_id = get_link_token(); + if (response) { + VLOG(webhook) << "Got response from connection " << connection_id; + } else { + VLOG(webhook) << "Got hangup from connection " << connection_id; + } + auto *connection_ptr = connections_.get(connection_id); + if (connection_ptr == nullptr) { + return; + } + + bool close_connection = false; + td::string query_error; + td::int32 retry_after = 0; + bool need_close = false; + + if (response) { + if (response->type_ != td::HttpQuery::Type::Response || !response->keep_alive_ || + ip_generation_ != connection_ptr->ip_generation_) { + close_connection = true; + } + if (response->type_ == td::HttpQuery::Type::Response) { + if (200 <= response->code_ && response->code_ <= 299) { + auto method = response->get_arg("method"); + td::to_lower_inplace(method); + if (!method.empty() && method != "deletewebhook" && method != "setwebhook" && method != "close" && + method != "logout" && !td::begins_with(method, "get")) { + VLOG(webhook) << "Receive request " << method << " in response to webhook"; + auto query = + std::make_unique(std::move(response->container_), td::MutableSlice(), false, td::MutableSlice(), + std::move(response->args_), std::move(response->headers_), + std::move(response->files_), parameters_->shared_data_, response->peer_address_); + auto promised_query = + PromisedQueryPtr(query.release(), PromiseDeleter(td::PromiseActor>())); + send_closure(callback_, &Callback::send, std::move(promised_query)); + } + first_error_410_time_ = 0; + } else { + query_error = PSTRING() << "Wrong response from the webhook: " << response->code_ << " " << response->reason_; + if (response->code_ == 410) { + if (first_error_410_time_ == 0) { + first_error_410_time_ = td::Time::now(); + } else { + if (td::Time::now() > first_error_410_time_ + WEBHOOK_DROP_TIMEOUT) { + LOG(WARNING) << "Close webhook because of HTTP 410 errors"; + need_close = true; + } + } + } else { + first_error_410_time_ = 0; + } + retry_after = response->get_retry_after(); + // LOG(WARNING) << query_error; + on_webhook_error(query_error); + } + } else { + query_error = PSTRING() << "Wrong response from the webhook: " << *response; + on_webhook_error(query_error); + } + VLOG(webhook) << *response; + } else { + query_error = "Webhook connection closed"; + connection_ptr->actor_id_.release(); + close_connection = true; + } + + auto event_id = connection_ptr->event_id_; + if (!event_id.empty()) { + if (query_error.empty()) { + on_update_ok(event_id); + } else { + on_update_error(event_id, query_error, retry_after); + } + } else { + CHECK(!query_error.empty()); + } + + connection_ptr->event_id_ = {}; + if (need_close || close_connection) { + VLOG(webhook) << "Close connection " << connection_id; + connections_.erase(connection_ptr->id_); + total_connections_count_.fetch_sub(1, std::memory_order_relaxed); + } else { + ready_connections_.put(connection_ptr->to_list_node()); + } + + if (need_close) { + send_closure_later(actor_id(this), &WebhookActor::close); + } else { + loop(); + } +} + +void WebhookActor::start_up() { + max_loaded_updates_ = max_connections_ * 2; + + last_success_timestamp_ = td::Time::now(); + active_new_connection_flood_.add_limit(1, 10 * max_connections_); + active_new_connection_flood_.add_limit(5, 20 * max_connections_); + + pending_new_connection_flood_.add_limit(1, 1); + + if (!parameters_->local_mode_) { + if (url_.protocol_ == td::HttpUrl::Protocol::Https) { + if (url_.port_ != 443 && url_.port_ != 88 && url_.port_ != 80 && url_.port_ != 8443) { + VLOG(webhook) << "Can't create webhook: port " << url_.port_ << " is forbidden"; + on_error(td::Status::Error("Webhook can be set up only on ports 80, 88, 443 or 8443")); + } + } else { + CHECK(url_.protocol_ == td::HttpUrl::Protocol::Http); + VLOG(webhook) << "Can't create connection: HTTP is forbidden"; + on_error(td::Status::Error("HTTPS url must be provided for webhook")); + } + } + + if (fix_ip_address_ && !stop_flag_) { + if (!ip_address_.is_valid()) { + on_error(td::Status::Error("Invalid IP address specified")); + } else if (!check_ip_address(ip_address_)) { + on_error(td::Status::Error(PSLICE() << "IP address " << ip_address_.get_ip_str() << " is reserved")); + } + } + + if (from_db_flag_ && !stop_flag_) { + was_checked_ = true; + on_webhook_verified(); + } + + yield(); +} + +void WebhookActor::hangup_shared() { + handle(nullptr); + loop(); +} + +void WebhookActor::hangup() { + VLOG(webhook) << "Stop"; + callback_.release(); + stop(); +} + +void WebhookActor::close() { + VLOG(webhook) << "Close"; + send_closure(std::move(callback_), &Callback::webhook_closed, td::Status::OK()); + stop(); +} + +void WebhookActor::tear_down() { + total_connections_count_.fetch_sub(connections_.size(), std::memory_order_relaxed); +} + +void WebhookActor::on_webhook_verified() { + td::string ip_address_str; + if (ip_address_.is_valid()) { + ip_address_str = ip_address_.get_ip_str().str(); + } + send_closure(callback_, &Callback::webhook_verified, std::move(ip_address_str)); +} + +bool WebhookActor::check_ip_address(const td::IPAddress &addr) const { + if (!addr.is_valid()) { + return false; + } + if (parameters_->local_mode_) { + // allow any valid IP address + return true; + } + if (!addr.is_ipv4()) { + VLOG(webhook) << "Bad IP address (not IPv4): " << addr; + return false; + } + return !addr.is_reserved(); +} + +void WebhookActor::on_error(td::Status status) { + VLOG(webhook) << "Receive webhook error " << status; + if (!was_checked_) { + CHECK(!callback_.empty()); + send_closure(std::move(callback_), &Callback::webhook_closed, std::move(status)); + stop_flag_ = true; + } +} + +void WebhookActor::on_connection_error(td::Status error) { + CHECK(error.is_error()); + on_webhook_error(error.message()); +} + +void WebhookActor::on_webhook_error(td::Slice error) { + if (was_checked_) { + send_closure(callback_, &Callback::webhook_error, td::Status::Error(error)); + last_error_date_ = td::Clocks::system(); // local time to output it to the log + last_error_message_ = error.str(); + } +} + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/WebhookActor.h b/telegram-bot-api/WebhookActor.h new file mode 100644 index 0000000..cedd4ca --- /dev/null +++ b/telegram-bot-api/WebhookActor.h @@ -0,0 +1,230 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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) +// +#pragma once + +#include "telegram-bot-api/Query.h" + +#include "td/db/TQueue.h" + +#include "td/net/HttpOutboundConnection.h" +#include "td/net/HttpQuery.h" +#include "td/net/SslStream.h" + +#include "td/actor/actor.h" +#include "td/actor/PromiseFuture.h" + +#include "td/utils/common.h" +#include "td/utils/Container.h" +#include "td/utils/FloodControlFast.h" +#include "td/utils/HttpUrl.h" +#include "td/utils/JsonBuilder.h" +#include "td/utils/List.h" +#include "td/utils/port/IPAddress.h" +#include "td/utils/port/SocketFd.h" +#include "td/utils/Slice.h" +#include "td/utils/Status.h" +#include "td/utils/VectorQueue.h" + +#include +#include +#include +#include +#include +#include + +namespace telegram_bot_api { + +struct ClientParameters; + +class WebhookActor : public td::HttpOutboundConnection::Callback { + public: + class Callback : public td::Actor { + public: + virtual void webhook_verified(td::string cached_ip) = 0; + virtual void webhook_success() = 0; + virtual void webhook_error(td::Status status) = 0; + virtual void webhook_closed(td::Status status) = 0; + virtual void send(PromisedQueryPtr query) = 0; + }; + + WebhookActor(td::ActorShared callback, td::int64 tqueue_id, td::HttpUrl url, td::string cert_path, + td::int32 max_connections, bool from_db_flag, td::string cached_ip_address, bool fix_ip_address, + std::shared_ptr parameters); + + void update(); + + void close(); + + static td::int64 get_total_connections_count() { + return total_connections_count_; + } + + private: + static constexpr std::size_t MIN_PENDING_UPDATES_WARNING = 50; + static constexpr int IP_ADDRESS_CACHE_TIME = 30 * 60; // 30 minutes + static constexpr int WEBHOOK_MAX_RESEND_TIMEOUT = 60; + static constexpr int WEBHOOK_DROP_TIMEOUT = 60 * 60 * 23; + + static std::atomic total_connections_count_; + + td::ActorShared callback_; + td::int64 tqueue_id_; + bool tqueue_empty_ = false; + std::size_t last_pending_update_count_ = MIN_PENDING_UPDATES_WARNING; + td::HttpUrl url_; + td::string cert_path_; + std::shared_ptr parameters_; + + double last_error_date_ = 0; + td::string last_error_message_ = ""; + + bool fix_ip_address_ = false; + + bool stop_flag_ = false; + + bool was_checked_ = false; + bool from_db_flag_ = false; + + class Update { + public: + td::TQueue::EventId id_; + td::string json_; + td::int32 expires_at_ = 0; + double wakeup_at_ = 0; + int delay_ = 0; + int fail_count_ = 0; + enum State { Begin, Send } state_ = State::Begin; + td::int64 queue_id_{0}; + }; + + struct QueueUpdates { + td::VectorQueue event_ids; + }; + + struct Queue { + Queue() = default; + Queue(double wakeup_at, td::int64 id) + : wakeup_at(wakeup_at), integer_wakeup_at(static_cast(wakeup_at * 1e9)), id(id) { + } + double wakeup_at{0}; + td::int64 integer_wakeup_at{0}; + td::int64 id{0}; + + bool operator<(const Queue &other) const { + return std::tie(integer_wakeup_at, id) < std::tie(other.integer_wakeup_at, other.id); + } + }; + + td::int32 begin_updates_n_ = 0; + + td::TQueue::EventId tqueue_offset_; + std::size_t max_loaded_updates_ = 0; + struct EventIdHash { + std::size_t operator()(td::TQueue::EventId event_id) const { + return std::hash()(event_id.value()); + } + }; + std::unordered_map update_map_; + std::unordered_map queue_updates_; + std::set queues_; + td::int64 unique_queue_id_ = static_cast(1) << 60; + + double first_error_410_time_ = 0; + + td::IPAddress ip_address_; + td::int32 ip_generation_ = 0; + double next_ip_address_resolve_timestamp_ = 0; + td::FutureActor future_ip_address_; + + class Connection : public td::ListNode { + public: + Connection() = default; + Connection(const Connection &) = delete; + Connection &operator=(const Connection &) = delete; + Connection(Connection &&) = default; + Connection &operator=(Connection &&) = default; + ~Connection() = default; + + td::ActorOwn actor_id_; + td::uint64 id_ = 0; + td::TQueue::EventId event_id_; + td::int32 ip_generation_ = -1; + static Connection *from_list_node(ListNode *node) { + return static_cast(node); + } + ListNode *to_list_node() { + return this; + } + }; + td::Container> pending_sockets_; + td::vector ready_sockets_; + + td::int32 max_connections_ = 0; + td::Container connections_; + td::ListNode ready_connections_; + td::FloodControlFast active_new_connection_flood_; + td::FloodControlFast pending_new_connection_flood_; + double last_success_timestamp_ = 0; + double wakeup_at_ = 0; + bool last_update_was_successful_ = true; + + void relax_wakeup_at(double wakeup_at, const char *source); + + void resolve_ip_address(); + + td::Result create_ssl_stream(); + td::Status create_connection() TD_WARN_UNUSED_RESULT; + td::Status create_connection(td::SocketFd fd) TD_WARN_UNUSED_RESULT; + void on_socket_ready_async(td::Result r_fd, td::int64 id); + + void create_new_connections(); + + void drop_event(td::TQueue::EventId event_id); + + void load_updates(); + void on_update_ok(td::TQueue::EventId event_id); + void on_update_error(td::TQueue::EventId event_id, td::Slice error, int retry_after); + td::Status send_update() TD_WARN_UNUSED_RESULT; + void send_updates(); + + void loop() override; + void handle(td::unique_ptr response) override; + + void hangup_shared() override; + + void hangup() override; + + void tear_down() override; + + void start_up() override; + + bool check_ip_address(const td::IPAddress &addr) const; + + void on_error(td::Status status); + void on_connection_error(td::Status error) override; + void on_webhook_error(td::Slice error); + void on_webhook_verified(); +}; + +class JsonUpdate : public td::Jsonable { + public: + JsonUpdate(td::int32 id, td::Slice update) : id_(id), update_(update) { + } + void store(td::JsonValueScope *scope) const { + auto object = scope->enter_object(); + object("update_id", id_); + object << td::JsonRaw(",\n"); + CHECK(!update_.empty()); + object << td::JsonRaw(update_); + } + + private: + td::int32 id_; + td::Slice update_; +}; + +} // namespace telegram_bot_api diff --git a/telegram-bot-api/telegram-bot-api.cpp b/telegram-bot-api/telegram-bot-api.cpp new file mode 100644 index 0000000..bd25f6b --- /dev/null +++ b/telegram-bot-api/telegram-bot-api.cpp @@ -0,0 +1,523 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020 +// +// 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 "telegram-bot-api/ClientManager.h" +#include "telegram-bot-api/ClientParameters.h" +#include "telegram-bot-api/HttpConnection.h" +#include "telegram-bot-api/HttpServer.h" +#include "telegram-bot-api/HttpStatConnection.h" +#include "telegram-bot-api/Query.h" +#include "telegram-bot-api/Stats.h" + +#include "td/telegram/ClientActor.h" + +#include "td/db/binlog/Binlog.h" +#include "td/db/TQueue.h" + +#include "td/net/GetHostByNameActor.h" +#include "td/net/HttpInboundConnection.h" + +#include "td/actor/actor.h" +#include "td/actor/PromiseFuture.h" + +#include "td/utils/buffer.h" +#include "td/utils/common.h" +#include "td/utils/crypto.h" +#include "td/utils/ExitGuard.h" +#include "td/utils/FileLog.h" +#include "td/utils/format.h" +//#include "td/utils/GitInfo.h" +#include "td/utils/logging.h" +#include "td/utils/MemoryLog.h" +#include "td/utils/misc.h" +#include "td/utils/OptionParser.h" +#include "td/utils/port/path.h" +#include "td/utils/port/rlimit.h" +#include "td/utils/port/signals.h" +#include "td/utils/port/stacktrace.h" +#include "td/utils/port/user.h" +#include "td/utils/Slice.h" +#include "td/utils/Status.h" +#include "td/utils/Time.h" + +#include "memprof/memprof.h" + +#include +#include +#include +#include +#include + +namespace telegram_bot_api { + +static std::atomic_flag need_rotate_log; + +static void rotate_log_signal_handler(int sig) { + need_rotate_log.clear(); +} + +static std::atomic_flag need_quit; + +static void quit_signal_handler(int sig) { + need_quit.clear(); +} + +static td::MemoryLog<1 << 20> memory_log; + +void print_log() { + auto buf = memory_log.get_buffer(); + auto pos = memory_log.get_pos(); + td::signal_safe_write("------- Log dump -------\n"); + td::signal_safe_write(buf.substr(pos), false); + td::signal_safe_write(buf.substr(0, pos), false); + td::signal_safe_write("\n", false); + td::signal_safe_write("------------------------\n"); +} + +static void fail_signal_handler(int sig) { + td::signal_safe_write_signal_number(sig); + td::Stacktrace::PrintOptions options; + options.use_gdb = true; + td::Stacktrace::print_to_stderr(options); + print_log(); + _Exit(EXIT_FAILURE); +} + +static std::atomic_flag need_change_verbosity_level; + +static void change_verbosity_level_signal_handler(int sig) { + need_change_verbosity_level.clear(); +} + +static std::atomic_flag need_dump_log; + +static void dump_log_signal_handler(int sig) { + need_dump_log.clear(); +} + +static void sigsegv_signal_handler(int signum, void *addr) { + td::signal_safe_write_pointer(addr); + fail_signal_handler(signum); +} + +int main(int argc, char *argv[]) { + SET_VERBOSITY_LEVEL(VERBOSITY_NAME(FATAL)); + td::ExitGuard exit_guard; + + need_rotate_log.test_and_set(); + need_quit.test_and_set(); + need_change_verbosity_level.test_and_set(); + need_dump_log.test_and_set(); + + td::Stacktrace::init(); + + td::setup_signals_alt_stack().ensure(); + td::set_signal_handler(td::SignalType::User, rotate_log_signal_handler).ensure(); + td::ignore_signal(td::SignalType::HangUp).ensure(); + td::ignore_signal(td::SignalType::Pipe).ensure(); + td::set_signal_handler(td::SignalType::Quit, quit_signal_handler).ensure(); + td::set_signal_handler(td::SignalType::Abort, fail_signal_handler).ensure(); + td::set_signal_handler(td::SignalType::Other, fail_signal_handler).ensure(); + td::set_extended_signal_handler(td::SignalType::Error, sigsegv_signal_handler).ensure(); + + td::set_runtime_signal_handler(0, change_verbosity_level_signal_handler).ensure(); + td::set_runtime_signal_handler(1, dump_log_signal_handler).ensure(); + + td::init_openssl_threads(); + + auto start_time = td::Time::now(); + auto shared_data = std::make_shared(); + auto parameters = std::make_unique(); + parameters->shared_data_ = shared_data; + parameters->start_timestamp_ = start_time; + auto net_query_stats = td::create_net_query_stats(); + parameters->net_query_stats_ = net_query_stats; + + td::OptionParser options; + bool need_print_usage = false; + int http_port = 8081; + int http_stat_port = 0; + td::string log_file_path; + int verbosity_level = 0; + td::int64 log_max_file_size = 2000000000; + td::string working_directory; + td::string temporary_directory; + td::string username; + td::string groupname; + td::uint64 max_connections = 0; + ClientManager::TokenRange token_range{0, 1}; + + parameters->api_id_ = [](auto x) -> td::int32 { + if (x) { + return td::to_integer(td::Slice(x)); + } + return 0; + }(std::getenv("TELEGRAM_API_ID")); + parameters->api_hash_ = [](auto x) -> std::string { + if (x) { + return x; + } + return std::string(); + }(std::getenv("TELEGRAM_API_HASH")); + + options.set_usage(td::Slice(argv[0]), "--api_id= --api-hash= [--local] [OPTION]..."); + options.set_description("Telegram Bot API server"); + options.add_option('h', "help", "display this help text and exit", [&] { need_print_usage = true; }); + options.add_option('\0', "local", "allow the Bot API server to serve local requests", + [&] { parameters->local_mode_ = true; }); + options.add_checked_option( + '\0', "api-id", + "application identifier for Telegram API access, which can be obtained at https://my.telegram.org (defaults to " + "the value of the TELEGRAM_API_ID environment variable)", + td::OptionParser::parse_integer(parameters->api_id_)); + options.add_option('\0', "api-hash", + "application identifier hash for Telegram API access, which can be obtained at " + "https://my.telegram.org (defaults to the value of the TELEGRAM_API_HASH environment variable)", + td::OptionParser::parse_string(parameters->api_hash_)); + options.add_checked_option('p', "http-port", PSLICE() << "HTTP listening port (default is " << http_port << ")", + td::OptionParser::parse_integer(http_port)); + options.add_checked_option('s', "http-stat-port", "HTTP statistics port", + td::OptionParser::parse_integer(http_stat_port)); + options.add_option('d', "dir", "server working directory", td::OptionParser::parse_string(working_directory)); + options.add_option('t', "temp-dir", "directory for storing HTTP server temporary files", + td::OptionParser::parse_string(temporary_directory)); + options.add_checked_option('\0', "filter", + "\"/\". Allow only bots with 'bot_user_id % modulo == remainder'", + [&](td::Slice rem_mod) { + td::Slice rem; + td::Slice mod; + std::tie(rem, mod) = td::split(rem_mod, '/'); + TRY_RESULT(rem_i, td::to_integer_safe(rem)); + TRY_RESULT(mod_i, td::to_integer_safe(mod)); + token_range = {rem_i, mod_i}; + return td::Status::OK(); + }); + options.add_checked_option('\0', "max-webhook-connections", + "default value of the maximum webhook connections per bot", + td::OptionParser::parse_integer(parameters->default_max_webhook_connections_)); + + options.add_option('l', "log", "path to the file where the log will be written", + td::OptionParser::parse_string(log_file_path)); + options.add_checked_option('v', "verbosity", "log verbosity level", td::OptionParser::parse_integer(verbosity_level)); + options.add_checked_option( + '\0', "log-max-file-size", + PSLICE() << "maximum size of the log file in bytes before it will be auto-rotated (default is " + << log_max_file_size << ")", + td::OptionParser::parse_integer(log_max_file_size)); + + options.add_option('u', "username", "effective user name to switch to", td::OptionParser::parse_string(username)); + options.add_option('g', "groupname", "effective group name to switch to", td::OptionParser::parse_string(groupname)); + options.add_checked_option('c', "max-connections", "maximum number of open file descriptors", + td::OptionParser::parse_integer(max_connections)); + + options.add_checked_option( + '\0', "proxy", PSLICE() << "HTTP proxy server for outgoing webhook requests in the format http://host:port", + [&](td::Slice address) { + if (td::begins_with(address, "http://")) { + address.remove_prefix(7); + } else if (td::begins_with(address, "https://")) { + address.remove_prefix(8); + } + return parameters->webhook_proxy_ip_address_.init_host_port(address.str()); + }); + options.add_check([&] { + if (parameters->api_id_ <= 0 || parameters->api_hash_.empty()) { + return td::Status::Error("You must provide valid api-id and api-hash obtained at https://my.telegram.org"); + } + return td::Status::OK(); + }); + options.add_check([&] { + if (verbosity_level < 0) { + return td::Status::Error("Wrong verbosity level specified"); + } + return td::Status::OK(); + }); + auto r_non_options = options.run(argc, argv, 0); + if (need_print_usage) { + LOG(PLAIN) << options; + return 0; + } + if (r_non_options.is_error()) { + LOG(PLAIN) << argv[0] << ": " << r_non_options.error(); + LOG(PLAIN) << options; + return 1; + } + + class CombineLog : public td::LogInterface { + public: + void append(td::CSlice slice, int log_level) override { + if (first_ && log_level <= first_verbosity_level_) { + first_->append(slice, log_level); + } + if (second_ && log_level <= second_verbosity_level_) { + second_->append(slice, log_level); + } + } + + void set_first(LogInterface *first) { + first_ = first; + } + + void set_second(LogInterface *second) { + second_ = second; + } + + void set_first_verbosity_level(int verbosity_level) { + first_verbosity_level_ = verbosity_level; + } + + void set_second_verbosity_level(int verbosity_level) { + second_verbosity_level_ = verbosity_level; + } + + void rotate() override { + if (first_) { + first_->rotate(); + } + if (second_) { + second_->rotate(); + } + } + + td::vector get_file_paths() override { + td::vector result; + if (first_) { + td::append(result, first_->get_file_paths()); + } + if (second_) { + td::append(result, second_->get_file_paths()); + } + return result; + } + + private: + LogInterface *first_ = nullptr; + int first_verbosity_level_ = VERBOSITY_NAME(FATAL); + LogInterface *second_ = nullptr; + int second_verbosity_level_ = VERBOSITY_NAME(FATAL); + }; + CombineLog log; + log.set_first(td::default_log_interface); + log.set_second(&memory_log); + td::log_interface = &log; + + td::FileLog file_log; + td::TsLog ts_log(&file_log); + + auto init_status = [&] { + if (max_connections != 0) { + TRY_STATUS_PREFIX(td::set_resource_limit(td::ResourceLimitType::NoFile, max_connections), + "Can't set file descriptor limit: "); + } + + if (!username.empty()) { + TRY_STATUS_PREFIX(td::change_user(username, groupname), "Can't change effective user: "); + } + + if (!working_directory.empty()) { + TRY_STATUS_PREFIX(td::chdir(working_directory), "Can't set working directory: "); + } + + if (!temporary_directory.empty()) { + TRY_STATUS_PREFIX(td::set_temporary_dir(temporary_directory), "Can't set temporary directory: "); + } + + if (!log_file_path.empty()) { + TRY_STATUS_PREFIX(file_log.init(log_file_path, log_max_file_size), "Can't open log file: "); + log.set_first(&ts_log); + } + + return td::Status::OK(); + }(); + if (init_status.is_error()) { + LOG(PLAIN) << init_status.error(); + LOG(PLAIN) << options; + return 1; + } + + if (parameters->default_max_webhook_connections_ <= 0) { + parameters->default_max_webhook_connections_ = parameters->local_mode_ ? 100 : 40; + } + + ::td::VERBOSITY_NAME(dns_resolver) = VERBOSITY_NAME(WARNING); + + int memory_verbosity_level = td::max(VERBOSITY_NAME(INFO), verbosity_level); + SET_VERBOSITY_LEVEL(memory_verbosity_level); + log.set_first_verbosity_level(verbosity_level); + log.set_second_verbosity_level(memory_verbosity_level); + + // LOG(WARNING) << "Bot API server with commit " << td::GitInfo::commit() << ' ' + // << (td::GitInfo::is_dirty() ? "(dirty)" : "") << " started"; + LOG(WARNING) << "Bot API server started"; + + const int threads_n = 5; // +3 for Td, one for slow HTTP connections and one for DNS resolving + td::ConcurrentScheduler sched; + sched.init(threads_n); + + td::GetHostByNameActor::Options get_host_by_name_options; + get_host_by_name_options.scheduler_id = threads_n; + parameters->get_host_by_name_actor_id_ = + sched.create_actor_unsafe(0, "GetHostByName", std::move(get_host_by_name_options)) + .release(); + + auto client_manager = + sched.create_actor_unsafe(0, "ClientManager", std::move(parameters), token_range).release(); + sched + .create_actor_unsafe( + 0, "HttpServer", http_port, + [client_manager, shared_data] { + return td::ActorOwn( + td::create_actor("HttpConnection", client_manager, shared_data)); + }) + .release(); + if (http_stat_port != 0) { + sched + .create_actor_unsafe( + 0, "HttpStatsServer", http_stat_port, + [client_manager] { + return td::ActorOwn( + td::create_actor("HttpStatConnection", client_manager)); + }) + .release(); + } + sched.start(); + + double last_cron_time = start_time; + double last_dump_time = start_time - 1000.0; + double last_tqueue_gc_time = start_time - 1000.0; + td::int64 tqueue_deleted_events = 0; + td::int64 last_tqueue_deleted_events = 0; + bool close_flag = false; + std::atomic_bool can_quit{false}; + ServerCpuStat::instance(); // create ServerCpuStat instance + while (true) { + sched.run_main(1); + + if (!need_rotate_log.test_and_set()) { + td::log_interface->rotate(); + } + + if (!need_quit.test_and_set()) { + if (close_flag) { + LOG(WARNING) << "Receive stop signal again. Exit immediately..."; + std::_Exit(0); + } + + LOG(WARNING) << "Stopping engine by a signal"; + close_flag = true; + auto guard = sched.get_main_guard(); + send_closure(client_manager, &ClientManager::close, td::PromiseCreator::lambda([&can_quit](td::Unit) { + can_quit.store(true); + td::Scheduler::instance()->yield(); + })); + } + if (can_quit.exchange(false)) { + break; + } + + if (!need_change_verbosity_level.test_and_set()) { + auto current_global_verbosity_level = GET_VERBOSITY_LEVEL(); + SET_VERBOSITY_LEVEL(256 - current_global_verbosity_level); + verbosity_level = 256 - verbosity_level; + log.set_first_verbosity_level(verbosity_level); + } + + auto next_verbosity_level = shared_data->next_verbosity_level_.exchange(-1); + if (next_verbosity_level != -1) { + verbosity_level = next_verbosity_level; + memory_verbosity_level = td::max(VERBOSITY_NAME(INFO), verbosity_level); + SET_VERBOSITY_LEVEL(memory_verbosity_level); + + log.set_first_verbosity_level(verbosity_level); + log.set_second_verbosity_level(memory_verbosity_level); + } + + if (!need_dump_log.test_and_set()) { + print_log(); + } + + double now = td::Time::now(); + if (now > last_cron_time + 1.0) { + last_cron_time = now; + ServerCpuStat::update(now); + } + + if (now > last_tqueue_gc_time + 60.0) { + auto unix_time = shared_data->get_unix_time(now); + LOG(INFO) << "Run TQueue GC at " << unix_time; + last_tqueue_gc_time = now; + auto guard = sched.get_main_guard(); + auto deleted_events = shared_data->tqueue_->run_gc(unix_time); + LOG(INFO) << "TQueue GC deleted " << deleted_events << " events"; + + tqueue_deleted_events += deleted_events; + if (tqueue_deleted_events > last_tqueue_deleted_events + 10000) { + LOG(WARNING) << "TQueue GC already deleted " << tqueue_deleted_events << " events since the start"; + last_tqueue_deleted_events = tqueue_deleted_events; + } + } + + if (now > last_dump_time + 300.0) { + last_dump_time = now; + if (is_memprof_on()) { + LOG(WARNING) << "Memory dump:"; + td::vector v; + dump_alloc([&](const AllocInfo &info) { v.push_back(info); }); + std::sort(v.begin(), v.end(), [](const AllocInfo &a, const AllocInfo &b) { return a.size > b.size; }); + size_t total_size = 0; + size_t other_size = 0; + int count = 0; + for (auto &info : v) { + if (count++ < 50) { + LOG(WARNING) << td::format::as_size(info.size) << td::format::as_array(info.backtrace); + } else { + other_size += info.size; + } + total_size += info.size; + } + LOG(WARNING) << td::tag("other", td::format::as_size(other_size)); + LOG(WARNING) << td::tag("total", td::format::as_size(total_size)); + LOG(WARNING) << td::tag("total traces", get_ht_size()); + LOG(WARNING) << td::tag("fast_backtrace_success_rate", get_fast_backtrace_success_rate()); + } + LOG(WARNING) << td::tag("buffer_mem", td::format::as_size(td::BufferAllocator::get_buffer_mem())); + LOG(WARNING) << td::tag("buffer_slice_size", td::format::as_size(td::BufferAllocator::get_buffer_slice_size())); + + auto query_count = shared_data->query_count_.load(); + LOG(WARNING) << td::tag("pending queries", query_count); + + td::uint64 i = 0; + bool was_gap = false; + for (auto end = &shared_data->query_list_, cur = end->prev; cur != end; cur = cur->prev, i++) { + if (i < 20 || i > query_count - 20 || i % (query_count / 50 + 1) == 0) { + if (was_gap) { + LOG(WARNING) << "..."; + was_gap = false; + } + LOG(WARNING) << static_cast(*cur); + } else { + was_gap = true; + } + } + + td::dump_pending_network_queries(*net_query_stats); + } + } + + LOG(WARNING) << "--------------------FINISH ENGINE--------------------"; + CHECK(net_query_stats.use_count() == 1); + net_query_stats = nullptr; + sched.finish(); + SET_VERBOSITY_LEVEL(VERBOSITY_NAME(FATAL)); + td::log_interface = td::default_log_interface; + return 0; +} + +} // namespace telegram_bot_api + +int main(int argc, char *argv[]) { + return telegram_bot_api::main(argc, argv); +}