diff --git a/CMakeLists.txt b/CMakeLists.txt index 870cb4f27d84..5255aa1cbeb3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -185,6 +185,10 @@ if (USERVER_FEATURE_MYSQL) add_subdirectory(mysql "${CMAKE_BINARY_DIR}/userver/mysql") endif() +if (USERVER_FEATURE_CORE) + add_subdirectory(telegram "${CMAKE_BINARY_DIR}/userver/telegram") +endif() + if (USERVER_IS_THE_ROOT_PROJECT AND USERVER_FEATURE_CORE) add_subdirectory(samples) endif() diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index 168e62ef4f58..59212cae2c4b 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -46,6 +46,9 @@ add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-tcp_service) add_subdirectory(tcp_full_duplex_service) add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-tcp_full_duplex_service) +add_subdirectory(telegram_bot_service) +add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-telegram_bot_service) + if (USERVER_FEATURE_MONGODB) add_subdirectory(http_caching) add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-http_caching) diff --git a/samples/telegram_bot_service/CMakeLists.txt b/samples/telegram_bot_service/CMakeLists.txt new file mode 100644 index 000000000000..56ee80ce05ee --- /dev/null +++ b/samples/telegram_bot_service/CMakeLists.txt @@ -0,0 +1,6 @@ +project(userver-samples-telegram_bot_service CXX) + +add_executable(${PROJECT_NAME} "telegram_bot_service.cpp") +target_link_libraries(${PROJECT_NAME} userver-core userver-telegram) + +userver_sample_testsuite_add() diff --git a/samples/telegram_bot_service/assets/userver_greeting.jpg b/samples/telegram_bot_service/assets/userver_greeting.jpg new file mode 100644 index 000000000000..3daa667a63e2 Binary files /dev/null and b/samples/telegram_bot_service/assets/userver_greeting.jpg differ diff --git a/samples/telegram_bot_service/dynamic_config_fallback.json b/samples/telegram_bot_service/dynamic_config_fallback.json new file mode 100644 index 000000000000..f14f60bb851e --- /dev/null +++ b/samples/telegram_bot_service/dynamic_config_fallback.json @@ -0,0 +1,76 @@ +{ + "BAGGAGE_SETTINGS": { + "allowed_keys": [] + }, + "HTTP_CLIENT_CONNECTION_POOL_SIZE": 1000, + "HTTP_CLIENT_CONNECT_THROTTLE": { + "max-size": 100, + "token-update-interval-ms": 0 + }, + "USERVER_BAGGAGE_ENABLED": false, + "USERVER_CACHES": {}, + "USERVER_CANCEL_HANDLE_REQUEST_BY_DEADLINE": false, + "USERVER_CHECK_AUTH_IN_HANDLERS": true, + "USERVER_DEADLINE_PROPAGATION_ENABLED": true, + "USERVER_DUMPS": {}, + "USERVER_FILES_CONTENT_TYPE_MAP": { + ".css": "text/css", + ".gif": "image/gif", + ".htm": "text/html", + ".html": "text/html", + ".jpeg": "image/jpeg", + ".js": "application/javascript", + ".json": "application/json", + ".md": "text/markdown", + ".png": "image/png", + ".svg": "image/svg+xml", + "__default__": "text/plain" + }, + "USERVER_HANDLER_STREAM_API_ENABLED": false, + "USERVER_HTTP_PROXY": "", + "USERVER_LOG_DYNAMIC_DEBUG": { + "force-disabled": [], + "force-enabled": [] + }, + "USERVER_LOG_REQUEST": true, + "USERVER_LOG_REQUEST_HEADERS": false, + "USERVER_LRU_CACHES": {}, + "USERVER_NO_LOG_SPANS": { + "names": [], + "prefixes": [] + }, + "USERVER_RPS_CCONTROL": { + "down-level": 1, + "down-rate-percent": 2, + "min-limit": 10, + "no-limit-seconds": 1000, + "overload-off-seconds": 3, + "overload-on-seconds": 3, + "up-level": 2, + "up-rate-percent": 2 + }, + "USERVER_RPS_CCONTROL_ACTIVATED_FACTOR_METRIC": 5, + "USERVER_RPS_CCONTROL_CUSTOM_STATUS": {}, + "USERVER_RPS_CCONTROL_ENABLED": false, + "USERVER_TASK_PROCESSOR_PROFILER_DEBUG": { + "fs-task-processor": { + "enabled": false, + "execution-slice-threshold-us": 1000000 + }, + "main-task-processor": { + "enabled": false, + "execution-slice-threshold-us": 2000 + } + }, + "USERVER_TASK_PROCESSOR_QOS": { + "default-service": { + "default-task-processor": { + "wait_queue_overload": { + "action": "ignore", + "length_limit": 5000, + "time_limit_us": 3000 + } + } + } + } +} diff --git a/samples/telegram_bot_service/static_config.yaml b/samples/telegram_bot_service/static_config.yaml new file mode 100644 index 000000000000..a503c7137452 --- /dev/null +++ b/samples/telegram_bot_service/static_config.yaml @@ -0,0 +1,61 @@ +# yaml +components_manager: + coro_pool: + initial_size: 500 # Preallocate 500 coroutines at startup. + max_size: 1000 # Do not keep more than 1000 preallocated coroutines. + + task_processors: # Task processor is an executor for coroutine tasks + + main-task-processor: # Make a task processor for CPU-bound couroutine tasks. + worker_threads: 4 # Process tasks in 4 threads. + thread_name: main-worker # OS will show the threads of this task processor with 'main-worker' prefix. + + fs-task-processor: # Make a separate task processor for filesystem bound tasks. + thread_name: fs-worker + worker_threads: 4 + + default_task_processor: main-task-processor + + components: # Configuring components that were registered via component_list + http-client: + fs-task-processor: fs-task-processor + + dns-client: # Asynchronous DNS component + fs-task-processor: fs-task-processor + + testsuite-support: + tests-control: + # Some options from server::handlers::HttpHandlerBase + path: /tests/{action} + method: POST + task_processor: main-task-processor + + server: + # ... + listener: # configuring the main listening socket... + port: 8080 # ...to listen on this port and... + task_processor: main-task-processor # ...process incoming requests on this task processor. + + logging: + fs-task-processor: fs-task-processor + loggers: + default: + file_path: '@stderr' + level: debug + overflow_behavior: discard # Drop logs if the system is too busy to write them down. + + tracer: # Component that helps to trace execution times and requests in logs. + service-name: telegram-bot-service # "You know. You all know exactly who I am. Say my name. " (c) + + dynamic-config: # Dynamic config storage options, do nothing + fs-cache-path: '' + dynamic-config-fallbacks: # Load options from file and push them into the dynamic config storage. + fallback-path: /etc/telegram_bot_service/dynamic_config_fallback.json + + telegram-bot-client: + bot-token: your-bot-token + + handler-greet-user: + fs-task-processor: fs-task-processor + greeting-photo-path: /etc/telegram_bot_service/assets/userver_greeting.jpg + polling-frequency: 50ms diff --git a/samples/telegram_bot_service/telegram_bot_service.cpp b/samples/telegram_bot_service/telegram_bot_service.cpp new file mode 100644 index 000000000000..ba5b9bbcfead --- /dev/null +++ b/samples/telegram_bot_service/telegram_bot_service.cpp @@ -0,0 +1,182 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +namespace samples::telegram_bot { + +class GreetUser final : public telegram::bot::TelegramBotLongPoller { + public: + static constexpr std::string_view kName = "handler-greet-user"; + + GreetUser(const components::ComponentConfig& config, + const components::ComponentContext& context); + + void HandleUpdate(telegram::bot::Update update, + telegram::bot::ClientPtr client) override; + + static yaml_config::Schema GetStaticConfigSchema(); + + private: + std::int64_t SendGreeting( + telegram::bot::ClientPtr client, + std::int64_t chat_id, + const std::optional& username) const; + + static std::string GreetingCaption( + const std::optional& username); + + static std::vector GreetingMessageEntities( + std::string_view caption); + + telegram::bot::SendPhotoMethod::Parameters::Photo GreetingPhoto() const; + + static std::shared_ptr ReadPhoto( + const components::ComponentConfig& config, + const components::ComponentContext& context); + + telegram::bot::RequestOptions request_options_; + mutable rcu::Variable photo_file_id_; + const std::shared_ptr photo_; +}; + +} // namespace samples::telegram_bot + +namespace samples::telegram_bot { + +GreetUser::GreetUser(const components::ComponentConfig& config, + const components::ComponentContext& context) + : telegram::bot::TelegramBotLongPoller (config, context), + request_options_{std::chrono::seconds{5}}, + photo_(ReadPhoto(config, context)) {} + +void GreetUser::HandleUpdate(telegram::bot::Update update, + telegram::bot::ClientPtr client) { + if (!update.message) { + return; + } + try { + const std::int64_t chat_id = update.message->chat->id; + const auto& username = update.message->chat->first_name; + SendGreeting(client, chat_id, username); + } catch (std::exception& ex) { + LOG_ERROR() << "Error send greeting: " << ex.what(); + } +} + +yaml_config::Schema GreetUser::GetStaticConfigSchema() { + return yaml_config::MergeSchemas(R"( +type: object +description: The component for sending a greeting to a user on behalf of a bot +additionalProperties: false +properties: + fs-task-processor: + type: string + description: fs-task-processor + greeting-photo-path: + type: string + description: The path to the greeting picture +)"); + } + +std::int64_t GreetUser::SendGreeting( + telegram::bot::ClientPtr client, + std::int64_t chat_id, + const std::optional& username) const { + auto upload_action = telegram::bot::SendChatActionMethod::Parameters::Action::kUploadPhoto; + auto send_action_request = client->SendChatAction({chat_id, upload_action}, + request_options_); + send_action_request.Perform(); + + telegram::bot::SendPhotoMethod::Parameters parameters{chat_id, + GreetingPhoto()}; + parameters.caption = GreetingCaption(username); + parameters.caption_entities = GreetingMessageEntities( + parameters.caption.value()); + + auto send_photo_request = client->SendPhoto(parameters, request_options_); + auto result = send_photo_request.Perform(); + if (!result.photo->empty()) { + auto file_id = photo_file_id_.StartWrite(); + *file_id = std::move(result.photo->back().file_id); + file_id.Commit(); + } + return result.message_id; +} + +std::string GreetUser::GreetingCaption( + const std::optional& username) { + if (username) { + return fmt::format("Hello, {}! This is a test telegram bot written on " + "userver.", + username.value()); + } else { + return "Hello! This is a test telegram bot written on userver."; + } +} + +std::vector GreetUser::GreetingMessageEntities( + std::string_view caption) { + std::vector result; + telegram::bot::MessageEntity& linkEntity = result.emplace_back(); + + std::wstring_convert> convert; + std::wstring wcaption = convert.from_bytes(caption.data()); + + linkEntity.offset = wcaption.size() - 8; + linkEntity.length = 7; + linkEntity.type = telegram::bot::MessageEntity::Type::kTextLink; + linkEntity.url = "https://userver.tech/index.html"; + return result; +} + +telegram::bot::SendPhotoMethod::Parameters::Photo GreetUser::GreetingPhoto() const { + { + auto file_id = photo_file_id_.Read(); + if (!file_id->empty()) { + return *file_id; + } + } + return telegram::bot::InputFile{photo_, + "/userver_greeting_photo.jpg", + "image/jpeg"}; +} + +std::shared_ptr GreetUser::ReadPhoto( + const components::ComponentConfig& config, + const components::ComponentContext& context) { + const auto fs_tp_name = config["fs-task-processor"].As(); + const auto greeting_photo_path = config["greeting-photo-path"] + .As(); + auto& fs_task_processor = context.GetTaskProcessor(fs_tp_name); + return std::make_shared( + fs::ReadFileContents(fs_task_processor, greeting_photo_path)); +} + +} // namespace samples::telegram_bot + +int main(int argc, char* argv[]) { + const auto component_list = + components::MinimalServerComponentList() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + return utils::DaemonMain(argc, argv, component_list); +} diff --git a/samples/telegram_bot_service/tests/conftest.py b/samples/telegram_bot_service/tests/conftest.py new file mode 100644 index 000000000000..2726c8af3307 --- /dev/null +++ b/samples/telegram_bot_service/tests/conftest.py @@ -0,0 +1,59 @@ +import pytest + + +pytest_plugins = ['pytest_userver.plugins.core'] + +USERVER_CONFIG_HOOKS = ['prepare_service_config'] + +BOT_TOKEN = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" + + +def ensure_no_trailing_separator(url: str) -> str: + if url.endswith('/'): + return url[:-1] + return url + + +@pytest.fixture(scope='session') +def prepare_service_config(mockserver_info): + def do_patch(config, config_vars): + components = config['components_manager']['components'] + components['telegram-bot-client']['bot-token'] = BOT_TOKEN + components['telegram-bot-client']['api-base-url'] = \ + ensure_no_trailing_separator(mockserver_info.base_url) + + return do_patch + + +@pytest.fixture(autouse=True) +def mock_get_updates(mockserver, updates, tg_method): + @mockserver.json_handler(tg_method('getUpdates')) + def get_updates_mock(*, body_json): + offset = body_json['offset'] + limit = body_json['limit'] + + result_updates = [] + for update in updates: + if len(result_updates) == limit: + break + if update['update_id'] >= offset: + result_updates.append(update) + + return { + 'ok': True, + 'result': result_updates + } + return get_updates_mock + + +@pytest.fixture +def updates(): + return [] + + +@pytest.fixture +def tg_method(): + def get_tg_method(method): + return '/bot{}/{}'.format(BOT_TOKEN, method) + + return get_tg_method diff --git a/samples/telegram_bot_service/tests/test_bot.py b/samples/telegram_bot_service/tests/test_bot.py new file mode 100644 index 000000000000..b88f963ac5c0 --- /dev/null +++ b/samples/telegram_bot_service/tests/test_bot.py @@ -0,0 +1,102 @@ +def get_private_message(user_id, message_id, date, text, + first_name="first_name", last_name="last_name", + username="username"): + return { + "message_id": message_id, + "from": { + "id":user_id, + "is_bot":False, + "first_name": first_name, + "last_name": last_name, + "username": username, + }, + "chat": { + "id": user_id, + "first_name": first_name, + "last_name": last_name, + "username": username, + "type":"private" + }, + "date": date, + "text": text + } + +async def test_greeting(service_client, mockserver, updates, tg_method): + user_id = 1 + first_name = '(0,0)' + start_message_id = 144 + date = 1696763977 + + updates.append({ + "update_id": 316773233, + "message": get_private_message(user_id, start_message_id, date, + "hello", first_name=first_name) + }) + + @mockserver.json_handler(tg_method('sendChatAction')) + def send_chat_action_mock(*, body_json): + assert body_json['chat_id'] == user_id + assert body_json['action'] == 'upload_photo' + + return { + 'ok': True, + 'result': True + } + + @mockserver.aiohttp_json_handler(tg_method('sendPhoto')) + async def send_photo_mock(request): + assert request.content_type == 'multipart/form-data' + multipart_reader = await request.multipart() + + text_content_types = [ + 'application/json; charset=utf-8', + 'text/plain; charset=utf-8' + ] + + part = await multipart_reader.next() + assert part.name == 'chat_id' + assert 'content-type' in part.headers + assert part.headers['content-type'] in text_content_types + assert await part.text() == '1' + + part = await multipart_reader.next() + assert part.name == 'photo' + assert 'content-type' in part.headers + assert part.headers['content-type'] == 'image/jpeg' + assert len(await part.read()) == 67028 + + part = await multipart_reader.next() + assert part.name == 'caption' + assert 'content-type' in part.headers + assert part.headers['content-type'] in text_content_types + greeting = 'Hello, {}! '.format(first_name) + \ + 'This is a test telegram bot written on userver.' + assert await part.text() == greeting + + part = await multipart_reader.next() + assert part.name == 'caption_entities' + assert 'content-type' in part.headers + assert part.headers['content-type'] == 'application/json; charset=utf-8' + assert await part.json() == [{ + 'length': 7, + 'offset': 48 + len(first_name), + 'type': 'text_link', + 'url': 'https://userver.tech/index.html' + }] + + assert await multipart_reader.next() is None + + return { + 'ok': True, + 'result': { + 'message_id': start_message_id + 1, + 'date': date + 3, + 'from': { + 'id': 123456, + 'is_bot': True, + 'first_name': 'TestBot' + } + } + } + + await mockserver.get_callqueue_for(tg_method('sendPhoto')).wait_call(2) diff --git a/scripts/telegram/README.md b/scripts/telegram/README.md new file mode 100644 index 000000000000..5f89c3bf45f5 --- /dev/null +++ b/scripts/telegram/README.md @@ -0,0 +1,15 @@ +# userver: Telegram scripts + +Генерация примера для типов: + +```bash +cd types_generator +python main.py -f ./example/api.txt --hpp ./example --cpp ./example +``` + +Генерация примера для методов: + +```bash +cd method_generator +python main.py -f ./example/api.txt --hpp ./example --cpp ./example +``` diff --git a/scripts/telegram/method_generator/example/api.txt b/scripts/telegram/method_generator/example/api.txt new file mode 100644 index 000000000000..51c66921be92 --- /dev/null +++ b/scripts/telegram/method_generator/example/api.txt @@ -0,0 +1,12 @@ +-------------------------------------------------------------------------------- +getUpdates +Use this method to receive incoming updates using long polling (wiki). Returns an Array of Update objects. Please note that this parameter doesn't affect updates created before the call to the getUpdates, so unwanted updates may be received for a short period of time. + +Parameter Type Required Description +offset Integer Optional Identifier of the first update to be returned. Must be greater by one than the highest among the identifiers of previously received updates. By default, updates starting with the earliest unconfirmed update are returned. An update is considered confirmed as soon as getUpdates is called with an offset higher than its update_id. The negative offset can be specified to retrieve updates starting from -offset update from the end of the updates queue. All previous updates will be forgotten. +limit Integer Optional Limits the number of updates to be retrieved. Values between 1-100 are accepted. Defaults to 100. +timeout Integer Optional Timeout in seconds for long polling. Defaults to 0, i.e. usual short polling. Should be positive, short polling should be used for testing purposes only. +allowed_updates Array of String Optional A JSON-serialized list of the update types you want your bot to receive. For example, specify [“message”, “edited_channel_post”, “callback_query”] to only receive updates of these types. See Update for a complete list of available update types. Specify an empty list to receive all update types except chat_member (default). If not specified, the previous setting will be used. +-------------------------------------------------------------------------------- +getMe +A simple method for testing your bot's authentication token. Requires no parameters. Returns basic information about the bot in form of a User object. diff --git a/scripts/telegram/method_generator/main.py b/scripts/telegram/method_generator/main.py new file mode 100644 index 000000000000..abca3ebbda3a --- /dev/null +++ b/scripts/telegram/method_generator/main.py @@ -0,0 +1,18 @@ +import argparse +import parser +import printer + +def main(args): + with open(args.filename, "r") as f: + lines = [line for line in f] + objs = parser.parse(lines) + printer.print_files(objs, args.header_path, args.cpp_path) + + +if __name__ == '__main__': + argparser = argparse.ArgumentParser() + argparser.add_argument('-f', '--file', dest="filename", required=True, metavar="FILE") + argparser.add_argument('-p', '--hpp', dest="header_path", required=True, metavar="PATH") + argparser.add_argument('-c', '--cpp', dest="cpp_path", required=True, metavar="PATH") + args = argparser.parse_args() + main(args) diff --git a/scripts/telegram/method_generator/parser.py b/scripts/telegram/method_generator/parser.py new file mode 100644 index 000000000000..eba34d9d51a5 --- /dev/null +++ b/scripts/telegram/method_generator/parser.py @@ -0,0 +1,131 @@ +ArrayPrefix = "Array of " + +TypeToCppType = { + "Integer or String": "std::string", + "Integer": "std::int64_t", + "String": "std::string", + "Boolean": "bool", + "True": "bool", + "Float": "double", + "Float number": "double", + "Duration": "std::chrono::seconds", + "Timepoint": "std::chrono::system_clock::time_point", +} + +ParametersSectionBegin = "Parameter\tType\tRequired\tDescription\n" +ObjectSectionBegin = "--------------------------------------------------------------------------------\n" + +class Field: + def __init__(self, name, type_, required, description): + self.name = name + self.type_ = type_ + self.description = description + + self.type_is_struct = False + self.base_cpptype = "" + self.cpptype = "" + self.full_cpp_type = "" + + self.is_optional = (required == "Optional") + self.is_required = (required == "Yes") + + self.arrays_count = 0 + + self.set_cpp_type() + + self.is_simple = self.full_cpp_type == "bool" or \ + self.full_cpp_type == "double" or \ + self.full_cpp_type == "std::int64_t" or \ + self.full_cpp_type == "std::chrono::seconds" or \ + self.full_cpp_type == "std::chrono::system_clock::time_point" + + def set_cpp_type(self): + type_ = self.type_ + + while type_.startswith(ArrayPrefix): + self.arrays_count += 1 + type_ = type_[len(ArrayPrefix):] + + cpptype = "" + + if type_ in TypeToCppType: + cpptype = TypeToCppType[type_] + else: + cpptype = type_ + self.type_is_struct = True + + self.base_cpptype = cpptype + + for i in range(self.arrays_count): + cpptype = "std::vector<{}>".format(cpptype) + + self.cpptype = cpptype + + if self.type_is_struct and self.arrays_count == 0: + self.full_cpp_type = "std::unique_ptr<{}>".format(self.cpptype) + elif self.is_optional: + self.full_cpp_type = "std::optional<{}>".format(self.cpptype) + else: + self.full_cpp_type = self.cpptype + + +def parse_field(line): + lines = line.split("\t") + assert(len(lines) == 4) + return Field(lines[0].strip(), lines[1].strip(), lines[2].strip(), lines[3].strip()) + + +def parse_fields(lines): + assert(lines[0] == ParametersSectionBegin) + lines = lines[1:] + + fields = [] + + while len(lines) != 0 and lines[0] != "\n": + if len(lines[0].split("\t")) != 4: + break + fields.append(parse_field(lines[0])) + + lines = lines[1:] + + return fields, lines + + +class Object: + def __init__(self, name, description, fields): + self.base_name = name + self.name = self.base_name[0].upper() + self.base_name[1:] + self.method_name = "{}Method".format(self.name) + self.request_name = "{}Request".format(self.name) + self.description = description + self.fields = fields + + +def parse_section(lines): + assert(lines[0] == ObjectSectionBegin) + lines = lines[1:] + + assert(len(lines) >= 2) + obj_name = lines[0].strip() + obj_desc = lines[1].strip() + lines = lines[2:] + fields = [] + + while len(lines) != 0: + if lines[0] == ParametersSectionBegin: + fields, lines = parse_fields(lines) + elif lines[0] == ObjectSectionBegin: + break + else: + lines = lines[1:] + + return Object(obj_name, obj_desc, fields), lines + + +def parse(lines): + objs = [] + while len(lines) != 0: + obj, lines = parse_section(lines) + objs.append(obj) + + return objs diff --git a/scripts/telegram/method_generator/printer.py b/scripts/telegram/method_generator/printer.py new file mode 100644 index 000000000000..b924814d15a1 --- /dev/null +++ b/scripts/telegram/method_generator/printer.py @@ -0,0 +1,626 @@ +import re + +INDENT_1 = " " + +def add_indent(lines, indent): + for i in range(len(lines)): + if lines[i] != "": + lines[i] = indent + lines[i] + + return lines + + +def camel_to_snake(name): + name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() + + +INCLUDE_PUBLIC_PATH_USERVER_TELEGRAM = "userver/telegram" +INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM = "telegram" +INCLUDE_TYPES_SUFFIX = "/bot/types" +INCLUDE_REQUESTS_SUFFIX = "/bot/requests" + +INCLUDE_PUBLIC_PATH_USERVER_TELEGRAM_TYPES = INCLUDE_PUBLIC_PATH_USERVER_TELEGRAM + INCLUDE_TYPES_SUFFIX +INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM_TYPES = INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM + INCLUDE_TYPES_SUFFIX +INCLUDE_PUBLIC_PATH_USERVER_TELEGRAM_REQUESTS = INCLUDE_PUBLIC_PATH_USERVER_TELEGRAM + INCLUDE_REQUESTS_SUFFIX +INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM_REQUESTS = INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM + INCLUDE_REQUESTS_SUFFIX + +class IncludesPrinter: + def __init__(self, obj): + self.obj = obj + + def pragma_once(self): + return "#pragma once" + + def hpp_telegram_includes(self): + includes = ["#include "] + for field in self.obj.fields: + if field.type_is_struct and field.base_cpptype != self.obj.name: + includes.append( + "#include <{}/{}.hpp>".format( + INCLUDE_PUBLIC_PATH_USERVER_TELEGRAM_TYPES, + camel_to_snake(field.base_cpptype) + ) + ) + + includes = list(set(includes)) + includes.sort() + return includes + + + def header_base_includes(self): + includes = [] + for field in self.obj.fields: + field_type = field.full_cpp_type + if field_type.find("std::int64_t") != -1: + includes.append("#include ") + if field_type.find("std::string") != -1: + includes.append("#include ") + if field_type.find("std::vector") != -1: + includes.append("#include ") + if field_type.find("std::optional") != -1: + includes.append("#include ") + if field_type.find("std::unique_ptr") != -1: + includes.append("#include ") + if field_type.find("std::chrono") != -1: + includes.append("#include ") + + includes = list(set(includes)) + includes.sort() + + return includes + + + def header_userver_includes(self): + return ["#include "] + + def header_includes(self): + includes = [self.pragma_once()] + includes.append("") + + tg_incl = self.hpp_telegram_includes() + if len(tg_incl) != 0: + includes += tg_incl + includes.append("") + + base_incl = self.header_base_includes() + if len(base_incl) != 0: + includes += base_incl + includes.append("") + + includes += self.header_userver_includes() + return includes + + def cpp_header_include(self): + return [ + "#include <{}/{}.hpp>".format( + INCLUDE_PUBLIC_PATH_USERVER_TELEGRAM_REQUESTS, + camel_to_snake(self.obj.name) + ) + ] + + def cpp_tg_includes(self): + includes = ["#include <{}/bot/requests/request_data.hpp>".format(INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM)] + for field in self.obj.fields: + if field.full_cpp_type.find("std::unique_ptr") != -1: + includes += [ + "#include <{}/bot/formats/parse.hpp>".format(INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM), + "#include <{}/bot/formats/serialize.hpp>".format(INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM) + ] + + if field.full_cpp_type.find("std::chrono::system_clock::time_point") != -1: + includes += [ + "#include <{}/time.hpp>".format(INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM_TYPES), + ] + if field.is_optional: + includes += [ + "#include <{}/bot/formats/value_builder.hpp>".format(INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM), + ] + + includes = list(set(includes)) + + includes.sort() + return includes + + def cpp_userver_includes(self): + includes = ["#include "] + for field in self.obj.fields: + if field.full_cpp_type.find("std::optional") != -1 or field.full_cpp_type.find("std::vector") != -1: + includes += [ + "#include ", + "#include " + ] + if field.full_cpp_type.find("std::chrono") != -1: + includes += [ + "#include ", + "#include " + ] + + includes = list(set(includes)) + includes.sort() + return includes + + def cpp_includes(self): + includes = self.cpp_header_include() + includes.append("") + + ps_incl = self.cpp_tg_includes() + if len(ps_incl) != 0: + includes += ps_incl + includes.append("") + + includes += self.cpp_userver_includes() + + return includes + + +class NamespacePrinter: + def __init__(self): + pass + + def begin_lines(self): + return [ + "USERVER_NAMESPACE_BEGIN", + "", + "namespace telegram::bot {", + ] + + def end_lines(self): + return [ + "} // namespace telegram::bot", + "", + "USERVER_NAMESPACE_END" + ] + + def impl_begin_lines(self): + return ["namespace impl {"] + + def impl_end_lines(self): + return ["} // namespace impl"] + + +class FieldPrinter: + def __init__(self, field): + self.field = field + + def struct_field(self): + if self.field.is_simple: + return "{} {}{{}};".format(self.field.full_cpp_type, self.field.name) + else: + return "{} {};".format(self.field.full_cpp_type, self.field.name) + + def brief(self): + return "/// @brief {}".format(self.field.description) + + def parse_line(self): + if self.field.full_cpp_type.find("std::chrono::system_clock::time_point") == -1: + return "data[\"{}\"].template As<{}>()".format(self.field.name, self.field.full_cpp_type) + else: + return "TransformToTimePoint(data[\"{}\"].template As<{}>())".format(self.field.name, self.field.full_cpp_type) + + def parse_set_line(self): + return "parameters.{} = {}".format(self.field.name, self.parse_line()) + + def serialize_line(self, obj_name): + field_str = "" + if self.field.full_cpp_type.find("std::chrono::system_clock::time_point") == -1: + field_str = "{}.{}".format(obj_name, self.field.name) + else: + field_str = "TransformToSeconds({}.{})".format(obj_name, self.field.name) + + if self.field.is_optional: + return "SetIfNotNull(builder, \"{}\", {})".format(self.field.name, field_str) + else: + return "builder[\"{}\"] = {}".format(self.field.name, field_str) + + def constructor_args(self): + return "{} _{}".format(self.field.full_cpp_type, self.field.name) + + def constructor_init(self): + if self.field.is_simple: + return "{}(_{})".format(self.field.name, self.field.name) + else: + return "{}(std::move(_{}))".format(self.field.name, self.field.name) + + def struct_fields_lines(self): + return [ + self.brief(), + self.struct_field() + ] + + +class StructPrinter: + class ParametersPrinter: + def __init__(self, obj): + self.obj = obj + + def field_lines(self): + lines = [] + + for i in range(len(self.obj.fields)): + field = self.obj.fields[i] + field_printer = FieldPrinter(field) + + lines += field_printer.struct_fields_lines() + if i + 1 != len(self.obj.fields): + lines.append("") + + return lines + + def constructor_declaration_lines(self): + required_fields = [] + for field in self.obj.fields: + if field.is_required: + required_fields.append(field) + + lines = [] + if len(required_fields) == 0: + return lines + + contructor_pref = "Parameters(" + for i in range(len(required_fields)): + field = required_fields[i] + field_printer = FieldPrinter(field) + + prefix = " " * len(contructor_pref) + if i == 0: + prefix = contructor_pref + + suffix = "," + if i + 1 == len(required_fields): + suffix = ");" + + lines.append(prefix + field_printer.constructor_args() + suffix) + + return lines + + def constructor_realization_lines(self): + required_fields = [] + for field in self.obj.fields: + if field.is_required: + required_fields.append(field) + + lines = [] + if len(required_fields) == 0: + return lines + + contructor_pref = "{}::Parameters::Parameters(".format(self.obj.method_name) + for i in range(len(required_fields)): + field = required_fields[i] + field_printer = FieldPrinter(field) + + prefix = " " * len(contructor_pref) + if i == 0: + prefix = contructor_pref + + suffix = "," + if i + 1 == len(required_fields): + suffix = ")" + + lines.append(prefix + field_printer.constructor_args() + suffix) + + for i in range(len(required_fields)): + field = required_fields[i] + field_printer = FieldPrinter(field) + + prefix = " , " + if i == 0: + prefix = " : " + + suffix = "" + if i + 1 == len(required_fields): + suffix = " {}" + + lines.append(prefix + field_printer.constructor_init() + suffix) + + return lines + + def struct_begin(self): + return "struct Parameters{" + + def struct_end(self): + return "};" + + def parameters_lines(self): + lines = [] + lines.append(self.struct_begin()) + + cstr = self.constructor_declaration_lines() + if len(cstr) != 0: + lines += add_indent(cstr, INDENT_1) + lines.append("") + + field_lines = add_indent(self.field_lines(), INDENT_1) + if len(field_lines) != 0: + lines += field_lines + + lines.append(self.struct_end()) + return lines + + def __init__(self, obj): + self.obj = obj + self.parameters_printer = self.ParametersPrinter(obj) + + def brief(self): + return "/// @brief {}".format(self.obj.description) + + def see(self): + return "/// @see https://core.telegram.org/bots/api#{}".format(camel_to_snake(self.obj.name).replace('_', '')) + + def method_k_name(self): + return "static constexpr std::string_view kName = \"{}\";".format(self.obj.base_name) + + def http_method(self): + if self.obj.name.find("Get") != -1: + return "static constexpr auto kHttpMethod = clients::http::HttpMethod::kGet;" + else: + return "static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost;" + + def declaration_fill_request_data(self): + prefix = "static void FillRequestData(" + return [ + prefix + "clients::http::Request& request,", + " " * len(prefix) + "const Parameters& parameters);" + ] + + def realization_fill_request_data(self): + prefix = "void {}::FillRequestData(".format(self.obj.method_name) + return [ + prefix + "clients::http::Request& request,", + " " * len(prefix) + "const Parameters& parameters) {", + " FillRequestDataAsJson<{}>(request, parameters);".format(self.obj.method_name), + "}" + ] + + def declaration_parse_response_data(self): + return ["static Reply ParseResponseData(clients::http::Response& response);"] + + def realization_parse_response_data(self): + return [ + "{}::Reply {}::ParseResponseData(".format(self.obj.method_name, self.obj.method_name), + " clients::http::Response& response) {", + " return ParseResponseDataFromJson<{}>(response);".format(self.obj.method_name), + "}" + ] + + def using_reply(self): + return "using Reply = ;" + + def struct_begin(self): + return "struct {} {{".format(self.obj.method_name) + + def struct_end(self): + return "};" + + def struct_lines(self): + lines = [] + lines.append(self.brief()) + lines.append(self.see()) + lines.append(self.struct_begin()) + lines.append(INDENT_1 + self.method_k_name()) + lines.append("") + lines.append(INDENT_1 + self.http_method()) + lines.append("") + lines += add_indent(self.parameters_printer.parameters_lines(), INDENT_1) + lines.append("") + lines.append(INDENT_1 + self.using_reply()) + lines.append("") + lines += add_indent(self.declaration_fill_request_data(), INDENT_1) + lines.append("") + lines += add_indent(self.declaration_parse_response_data(), INDENT_1) + lines.append(self.struct_end()) + return lines + + def struct_impl_lines(self): + lines = [] + ctr_lines = self.parameters_printer.constructor_realization_lines() + if len(ctr_lines) != 0: + lines += ctr_lines + lines.append("") + + lines += self.realization_fill_request_data() + lines.append("") + lines += self.realization_parse_response_data() + + return lines + + +class ParseFuncPrinter: + def __init__(self, obj): + self.obj = obj + + def func_declaration_lines(self): + func_start = "{}::Parameters Parse".format(self.obj.method_name) + lines = [func_start + "(const formats::json::Value& json,"] + lines.append(" " * (len(func_start) + 1) + "formats::parse::To<{}::Parameters>);".format(self.obj.method_name)) + return lines + + def func_realization_lines(self): + func_start = "{}::Parameters Parse".format(self.obj.method_name) + lines = [func_start + "(const formats::json::Value& json,"] + lines.append(" " * (len(func_start) + 1) + "formats::parse::To<{}::Parameters> to) {{".format(self.obj.method_name)) + lines.append(INDENT_1 + "return impl::Parse(json, to);") + lines.append("}") + return lines + + def func_impl_lines(self): + lines = ["template "] + lines.append("{}::Parameters Parse(const Value& data, formats::parse::To<{}::Parameters>) {{".format( \ + self.obj.method_name, self.obj.method_name)) + + default_constructor = True + for field in self.obj.fields: + default_constructor = default_constructor and not field.is_required + + if default_constructor: + parse_lines = [] + for i in range(len(self.obj.fields)): + field = self.obj.fields[i] + field_printer = FieldPrinter(field) + + parse_lines.append(field_printer.parse_line()) + if i + 1 != len(self.obj.fields): + parse_lines[-1] += "," + + obj_lines = ["return {}::Parameters{{".format(self.obj.method_name)] + obj_lines += add_indent(parse_lines, INDENT_1) + obj_lines.append("};") + + lines += add_indent(obj_lines, INDENT_1) + else: + required_lines = [] + optional_lines = [] + for field in self.obj.fields: + field_printer = FieldPrinter(field) + if field.is_required: + required_lines.append(field_printer.parse_line()) + else: + optional_lines.append(field_printer.parse_set_line() + ";") + + for i in range(len(required_lines)): + if i + 1 != len(required_lines): + required_lines[i] += "," + + obj_lines = ["{}::Parameters parameters{{".format(self.obj.method_name)] + obj_lines += add_indent(required_lines, INDENT_1) + obj_lines.append("};") + obj_lines += optional_lines + obj_lines.append("return parameters;") + + lines += add_indent(obj_lines, INDENT_1) + + lines.append("}") + return lines + + +class SerializeFuncPrinter: + def __init__(self, obj): + self.obj = obj + + def func_declaration_lines(self): + func_start = "formats::json::Value Serialize" + lines = [func_start + "(const {}::Parameters& parameters,".format(self.obj.method_name)] + lines.append(" " * (len(func_start) + 1) + "formats::serialize::To);") + return lines + + def func_realization_lines(self): + func_start = "formats::json::Value Serialize" + lines = [func_start + "(const {}::Parameters& parameters,".format(self.obj.method_name)] + lines.append(" " * (len(func_start) + 1) + "formats::serialize::To to) {") + lines.append(INDENT_1 + "return impl::Serialize(parameters, to);") + lines.append("}") + return lines + + def func_impl_lines(self): + camel_name = camel_to_snake(self.obj.name) + lines = ["template "] + lines.append("Value Serialize(const {}::Parameters& parameters, formats::serialize::To) {{" \ + .format(self.obj.method_name)) + + serialize_lines = ["typename Value::Builder builder;"] + for field in self.obj.fields: + field_printer = FieldPrinter(field) + serialize_lines.append(field_printer.serialize_line("parameters") + ";") + serialize_lines.append("return builder.ExtractValue();") + + lines += add_indent(serialize_lines, INDENT_1) + lines.append("}") + return lines + +class UsingPrinter: + def __init__(self, obj): + self.obj = obj + + def header_request_using(self): + return ["using {}Request = Request<{}>;".format(self.obj.name, self.obj.method_name)] + + +class ObjFile: + def __init__(self, obj): + self.obj = obj + self.header_file_name = "{}.hpp".format(camel_to_snake(obj.name)) + self.cpp_file_name = "{}.cpp".format(camel_to_snake(obj.name)) + + self.includes_printer = IncludesPrinter(self.obj) + self.namespace_printer = NamespacePrinter() + self.struct_printer = StructPrinter(self.obj) + self.parse_func_printer = ParseFuncPrinter(self.obj) + self.serialize_func_printer = SerializeFuncPrinter(self.obj) + self.using_printer = UsingPrinter(self.obj) + + def header_file(self): + lines = [] + + lines += self.includes_printer.header_includes() + lines.append("") + + lines += self.namespace_printer.begin_lines() + lines.append("") + + lines += self.struct_printer.struct_lines() + lines.append("") + + lines += self.parse_func_printer.func_declaration_lines() + lines.append("") + + lines += self.serialize_func_printer.func_declaration_lines() + lines.append("") + + lines += self.using_printer.header_request_using() + lines.append("") + + lines += self.namespace_printer.end_lines() + return "\n".join(lines) + + def cpp_file(self): + lines = [] + + lines += self.includes_printer.cpp_includes() + lines.append("") + + lines += self.namespace_printer.begin_lines() + lines.append("") + + lines += self.namespace_printer.impl_begin_lines() + lines.append("") + + lines += self.parse_func_printer.func_impl_lines() + lines.append("") + + lines += self.serialize_func_printer.func_impl_lines() + lines.append("") + + lines += self.namespace_printer.impl_end_lines() + lines.append("") + + struct_impl = self.struct_printer.struct_impl_lines() + if len(struct_impl) != 0: + lines += struct_impl + lines.append("") + + lines += self.parse_func_printer.func_realization_lines() + lines.append("") + + lines += self.serialize_func_printer.func_realization_lines() + lines.append("") + + lines += self.namespace_printer.end_lines() + return "\n".join(lines) + + def write_header_file(self, path): + with open("{}/{}".format(path, self.header_file_name), "w") as f: + f.write(self.header_file()) + + def write_cpp_file(self, path): + with open("{}/{}".format(path, self.cpp_file_name), "w") as f: + f.write(self.cpp_file()) + + + def write(self, header_path, cpp_path): + self.write_header_file(header_path) + self.write_cpp_file(cpp_path) + + +def print_files(objs, header_path, cpp_path): + for obj in objs: + ObjFile(obj).write(header_path, cpp_path) diff --git a/scripts/telegram/types_generator/example/api.txt b/scripts/telegram/types_generator/example/api.txt new file mode 100644 index 000000000000..c0db56e707db --- /dev/null +++ b/scripts/telegram/types_generator/example/api.txt @@ -0,0 +1,16 @@ +-------------------------------------------------------------------------------- +Animation +This object represents an animation file (GIF or H.264/MPEG-4 AVC video without sound). +Field Type Description +file_id String Identifier for this file, which can be used to download or reuse the file +file_unique_id String Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. +width Integer Video width as defined by sender +height Integer Video height as defined by sender +duration Integer Duration of the video in seconds as defined by sender +thumbnail PhotoSize Optional. Animation thumbnail as defined by sender +file_name String Optional. Original animation filename as defined by sender +mime_type String Optional. MIME type of the file as defined by sender +file_size Integer Optional. File size in bytes. It can be bigger than 2^31 and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a signed 64-bit integer or double-precision float type are safe for storing this value. +-------------------------------------------------------------------------------- +ForumTopicReopened +This object represents a service message about a forum topic reopened in the chat. Currently holds no information. diff --git a/scripts/telegram/types_generator/main.py b/scripts/telegram/types_generator/main.py new file mode 100644 index 000000000000..abca3ebbda3a --- /dev/null +++ b/scripts/telegram/types_generator/main.py @@ -0,0 +1,18 @@ +import argparse +import parser +import printer + +def main(args): + with open(args.filename, "r") as f: + lines = [line for line in f] + objs = parser.parse(lines) + printer.print_files(objs, args.header_path, args.cpp_path) + + +if __name__ == '__main__': + argparser = argparse.ArgumentParser() + argparser.add_argument('-f', '--file', dest="filename", required=True, metavar="FILE") + argparser.add_argument('-p', '--hpp', dest="header_path", required=True, metavar="PATH") + argparser.add_argument('-c', '--cpp', dest="cpp_path", required=True, metavar="PATH") + args = argparser.parse_args() + main(args) diff --git a/scripts/telegram/types_generator/parser.py b/scripts/telegram/types_generator/parser.py new file mode 100644 index 000000000000..65226206151c --- /dev/null +++ b/scripts/telegram/types_generator/parser.py @@ -0,0 +1,126 @@ +ArrayPrefix = "Array of " + +TypeToCppType = { + "Integer": "std::int64_t", + "String": "std::string", + "Boolean": "bool", + "True": "bool", + "Float": "double", + "Float number": "double", + "Duration": "std::chrono::seconds", + "Timepoint": "std::chrono::system_clock::time_point" +} + +FieldsSectionBegin = "Field\tType\tDescription\n" +ObjectSectionBegin = "--------------------------------------------------------------------------------\n" + +class Field: + def __init__(self, name, type_, description): + self.name = name + self.type_ = type_ + self.description = description + + self.type_is_struct = False + self.base_cpptype = "" + self.cpptype = "" + self.full_cpp_type = "" + + self.is_optional = self.description.find("Optional.") != -1 + + self.arrays_count = 0 + + self.set_cpp_type() + + self.is_simple = self.full_cpp_type == "bool" or \ + self.full_cpp_type == "double" or \ + self.full_cpp_type == "std::int64_t" or \ + self.full_cpp_type == "std::chrono::seconds" or \ + self.full_cpp_type == "std::chrono::system_clock::time_point" + + def set_cpp_type(self): + type_ = self.type_ + + while type_.startswith(ArrayPrefix): + self.arrays_count += 1 + type_ = type_[len(ArrayPrefix):] + + cpptype = "" + + if type_ in TypeToCppType: + cpptype = TypeToCppType[type_] + else: + cpptype = type_ + self.type_is_struct = True + + self.base_cpptype = cpptype + + for i in range(self.arrays_count): + cpptype = "std::vector<{}>".format(cpptype) + + self.cpptype = cpptype + + if self.type_is_struct and self.arrays_count == 0: + self.full_cpp_type = "std::unique_ptr<{}>".format(self.cpptype) + elif self.is_optional: + self.full_cpp_type = "std::optional<{}>".format(self.cpptype) + else: + self.full_cpp_type = self.cpptype + + +def parse_field(line): + lines = line.split("\t") + assert(len(lines) == 3) + return Field(lines[0].strip(), lines[1].strip(), lines[2].strip()) + + +def parse_fields(lines): + assert(lines[0] == FieldsSectionBegin) + lines = lines[1:] + + fields = [] + + while len(lines) != 0 and lines[0] != "\n": + if len(lines[0].split("\t")) != 3: + break + fields.append(parse_field(lines[0])) + + lines = lines[1:] + + return fields, lines + + +class Object: + def __init__(self, name, description, fields): + self.name = name + self.description = description + self.fields = fields + + +def parse_section(lines): + assert(lines[0] == ObjectSectionBegin) + lines = lines[1:] + + assert(len(lines) >= 2) + obj_name = lines[0].strip() + obj_desc = lines[1].strip() + lines = lines[2:] + fields = [] + + while len(lines) != 0: + if lines[0] == FieldsSectionBegin: + fields, lines = parse_fields(lines) + elif lines[0] == ObjectSectionBegin: + break + else: + lines = lines[1:] + + return Object(obj_name, obj_desc, fields), lines + + +def parse(lines): + objs = [] + while len(lines) != 0: + obj, lines = parse_section(lines) + objs.append(obj) + + return objs diff --git a/scripts/telegram/types_generator/printer.py b/scripts/telegram/types_generator/printer.py new file mode 100644 index 000000000000..64e98a6071dc --- /dev/null +++ b/scripts/telegram/types_generator/printer.py @@ -0,0 +1,425 @@ +import re + +INDENT_1 = " " +INDENT_2 = " " +INDENT_3 = " " +INDENT_4 = " " + +def add_indent(lines, indent): + for i in range(len(lines)): + if lines[i] != "": + lines[i] = indent + lines[i] + + return lines + + +def camel_to_snake(name): + name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() + + +INCLUDE_PUBLIC_PATH_USERVER_TELEGRAM = "userver/telegram" +INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM = "telegram" +INCLUDE_TYPES_SUFFIX = "/bot/types" + +INCLUDE_PUBLIC_PATH_USERVER_TELEGRAM_TYPES = INCLUDE_PUBLIC_PATH_USERVER_TELEGRAM + INCLUDE_TYPES_SUFFIX +INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM_TYPES = INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM + INCLUDE_TYPES_SUFFIX + +class IncludesPrinter: + def __init__(self, obj): + self.obj = obj + + def pragma_once(self): + return "#pragma once" + + def hpp_dependent_types_includes(self): + includes = [] + for field in self.obj.fields: + if field.type_is_struct and field.base_cpptype != self.obj.name: + includes.append( + "#include <{}/{}.hpp>".format( + INCLUDE_PUBLIC_PATH_USERVER_TELEGRAM_TYPES, + camel_to_snake(field.base_cpptype) + ) + ) + + includes = list(set(includes)) + includes.sort() + return includes + + + def header_base_includes(self): + includes = [] + for field in self.obj.fields: + field_type = field.full_cpp_type + if field_type.find("std::int64_t") != -1: + includes.append("#include ") + if field_type.find("std::string") != -1: + includes.append("#include ") + if field_type.find("std::vector") != -1: + includes.append("#include ") + if field_type.find("std::optional") != -1: + includes.append("#include ") + if field_type.find("std::unique_ptr") != -1: + includes.append("#include ") + if field_type.find("std::chrono") != -1: + includes.append("#include ") + + includes = list(set(includes)) + includes.sort() + + return includes + + + def header_userver_includes(self): + return ["#include "] + + def header_includes(self): + includes = [self.pragma_once()] + includes.append("") + + dep_incl = self.hpp_dependent_types_includes() + if len(dep_incl) != 0: + includes += dep_incl + includes.append("") + + base_incl = self.header_base_includes() + if len(base_incl) != 0: + includes += base_incl + includes.append("") + + includes += self.header_userver_includes() + return includes + + def cpp_header_include(self): + return [ + "#include <{}/{}.hpp>".format( + INCLUDE_PUBLIC_PATH_USERVER_TELEGRAM_TYPES, + camel_to_snake(self.obj.name) + ) + ] + + def cpp_parse_serialize_includes(self): + includes = [] + for field in self.obj.fields: + if field.full_cpp_type.find("std::unique_ptr") != -1: + includes = [ + "#include <{}/parse.hpp>".format(INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM_TYPES), + "#include <{}/serialize.hpp>".format(INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM_TYPES) + ] + + if field.full_cpp_type.find("std::chrono::system_clock::time_point") != -1: + includes += [ + "#include <{}/time.hpp>".format(INCLUDE_PRIVATE_PATH_USERVER_TELEGRAM_TYPES), + ] + + includes = list(set(includes)) + + includes.sort() + return includes + + def cpp_userver_includes(self): + includes = ["#include "] + for field in self.obj.fields: + if field.full_cpp_type.find("std::optional") != -1 or field.full_cpp_type.find("std::vector") != -1: + includes += [ + "#include ", + "#include " + ] + if field.full_cpp_type.find("std::chrono") != -1: + includes += [ + "#include ", + "#include " + ] + + includes = list(set(includes)) + includes.sort() + return includes + + def cpp_includes(self): + includes = self.cpp_header_include() + includes.append("") + + ps_incl = self.cpp_parse_serialize_includes() + if len(ps_incl) != 0: + includes += ps_incl + includes.append("") + + includes += self.cpp_userver_includes() + + return includes + + +class NamespacePrinter: + def __init__(self): + pass + + def begin_lines(self): + return [ + "USERVER_NAMESPACE_BEGIN", + "", + "namespace telegram::bot {", + ] + + def end_lines(self): + return [ + "} // namespace telegram::bot", + "", + "USERVER_NAMESPACE_END" + ] + + def impl_begin_lines(self): + return ["namespace impl {"] + + def impl_end_lines(self): + return ["} // namespace impl"] + + +class ForwardDeclarationPrinter: + def __init__(self, obj): + self.obj = obj + + def forward_declaration(self): + lines = [] + + for field in self.obj.fields: + if field.type_is_struct and field.base_cpptype != self.obj.name: + lines.append("struct {};".format(field.base_cpptype)) + + lines = list(set(lines)) + lines.sort() + return lines + + +class FieldPrinter: + def __init__(self, field): + self.field = field + + def struct_field(self): + if self.field.is_simple: + return "{} {}{{}};".format(self.field.full_cpp_type, self.field.name) + else: + return "{} {};".format(self.field.full_cpp_type, self.field.name) + + def brief(self): + return "/// @brief {}".format(self.field.description) + + def parse_line(self): + if self.field.full_cpp_type.find("std::chrono::system_clock::time_point") == -1: + return "data[\"{}\"].template As<{}>()".format(self.field.name, self.field.full_cpp_type) + else: + return "TransformToTimePoint(data[\"{}\"].template As<{}>())".format(self.field.name, self.field.full_cpp_type) + + def serialize_line(self, obj_name): + if self.field.full_cpp_type.find("std::chrono::system_clock::time_point") == -1: + return "builder[\"{}\"] = {}.{}".format(self.field.name, obj_name, self.field.name) + else: + return "builder[\"{}\"] = TransformToSeconds({}.{})".format(self.field.name, obj_name, self.field.name) + + def struct_fields_lines(self): + return [ + self.brief(), + self.struct_field() + ] + + +class StructPrinter: + def __init__(self, obj): + self.obj = obj + + def brief(self): + return "/// @brief {}".format(self.obj.description) + + def see(self): + return "/// @see https://core.telegram.org/bots/api#{}".format(camel_to_snake(self.obj.name).replace('_', '')) + + def field_lines(self): + lines = [] + + for i in range(len(self.obj.fields)): + field = self.obj.fields[i] + field_printer = FieldPrinter(field) + + lines += field_printer.struct_fields_lines() + if i + 1 != len(self.obj.fields): + lines.append("") + + return lines + + def struct_begin(self): + return "struct {} {{".format(self.obj.name) + + def struct_end(self): + return "};" + + def struct_lines(self): + lines = [] + lines.append(self.brief()) + lines.append(self.see()) + lines.append(self.struct_begin()) + lines += add_indent(self.field_lines(), INDENT_1) + lines.append(self.struct_end()) + return lines + +class ParseFuncPrinter: + def __init__(self, obj): + self.obj = obj + + def func_declaration_lines(self): + func_start = "{} Parse".format(self.obj.name) + lines = [func_start + "(const formats::json::Value& json,"] + lines.append(" " * (len(func_start) + 1) + "formats::parse::To<{}>);".format(self.obj.name)) + return lines + + def func_realization_lines(self): + func_start = "{} Parse".format(self.obj.name) + lines = [func_start + "(const formats::json::Value& json,"] + lines.append(" " * (len(func_start) + 1) + "formats::parse::To<{}> to) {{".format(self.obj.name)) + lines.append(INDENT_1 + "return impl::Parse(json, to);") + lines.append("}") + return lines + + def func_impl_lines(self): + lines = ["template "] + lines.append("{} Parse(const Value& data, formats::parse::To<{}>) {{".format(self.obj.name, self.obj.name)) + + parse_lines = [] + for i in range(len(self.obj.fields)): + field = self.obj.fields[i] + field_printer = FieldPrinter(field) + + parse_lines.append(field_printer.parse_line()) + if i + 1 != len(self.obj.fields): + parse_lines[-1] += "," + + obj_lines = ["return {}{{".format(self.obj.name)] + obj_lines += add_indent(parse_lines, INDENT_1) + obj_lines.append("};") + + lines += add_indent(obj_lines, INDENT_1) + lines.append("}") + return lines + + +class SerializeFuncPrinter: + def __init__(self, obj): + self.obj = obj + + def func_declaration_lines(self): + func_start = "formats::json::Value Serialize" + lines = [func_start + "(const {}& {},".format(self.obj.name, camel_to_snake(self.obj.name))] + lines.append(" " * (len(func_start) + 1) + "formats::serialize::To);") + return lines + + def func_realization_lines(self): + camel_name = camel_to_snake(self.obj.name) + func_start = "formats::json::Value Serialize" + lines = [func_start + "(const {}& {},".format(self.obj.name, camel_name)] + lines.append(" " * (len(func_start) + 1) + "formats::serialize::To to) {") + lines.append(INDENT_1 + "return impl::Serialize({}, to);".format(camel_name)) + lines.append("}") + return lines + + def func_impl_lines(self): + camel_name = camel_to_snake(self.obj.name) + lines = ["template "] + lines.append("Value Serialize(const {}& {}, formats::serialize::To) {{" \ + .format(self.obj.name, camel_name)) + + serialize_lines = ["typename Value::Builder builder;"] + for field in self.obj.fields: + field_printer = FieldPrinter(field) + serialize_lines.append(field_printer.serialize_line(camel_name) + ";") + serialize_lines.append("return builder.ExtractValue();") + + lines += add_indent(serialize_lines, INDENT_1) + lines.append("}") + return lines + + +class ObjFile: + def __init__(self, obj): + self.obj = obj + self.header_file_name = "{}.hpp".format(camel_to_snake(obj.name)) + self.cpp_file_name = "{}.cpp".format(camel_to_snake(obj.name)) + + self.includes_printer = IncludesPrinter(self.obj) + self.namespace_printer = NamespacePrinter() + self.fwd_printer = ForwardDeclarationPrinter(self.obj) + self.struct_printer = StructPrinter(self.obj) + self.parse_func_printer = ParseFuncPrinter(self.obj) + self.serialize_func_printer = SerializeFuncPrinter(self.obj) + + def header_file(self): + lines = [] + + lines += self.includes_printer.header_includes() + lines.append("") + + lines += self.namespace_printer.begin_lines() + lines.append("") + + # add_lines = self.fwd_printer.forward_declaration() + # if len(add_lines) != 0: + # lines += add_lines + # lines.append("") + + lines += self.struct_printer.struct_lines() + lines.append("") + + lines += self.parse_func_printer.func_declaration_lines() + lines.append("") + + lines += self.serialize_func_printer.func_declaration_lines() + lines.append("") + + lines += self.namespace_printer.end_lines() + return "\n".join(lines) + + def cpp_file(self): + lines = [] + + lines += self.includes_printer.cpp_includes() + lines.append("") + + lines += self.namespace_printer.begin_lines() + lines.append("") + + lines += self.namespace_printer.impl_begin_lines() + lines.append("") + + lines += self.parse_func_printer.func_impl_lines() + lines.append("") + + lines += self.serialize_func_printer.func_impl_lines() + lines.append("") + + lines += self.namespace_printer.impl_end_lines() + lines.append("") + + lines += self.parse_func_printer.func_realization_lines() + lines.append("") + + lines += self.serialize_func_printer.func_realization_lines() + lines.append("") + + lines += self.namespace_printer.end_lines() + return "\n".join(lines) + + def write_header_file(self, path): + with open("{}/{}".format(path, self.header_file_name), "w") as f: + f.write(self.header_file()) + + def write_cpp_file(self, path): + with open("{}/{}".format(path, self.cpp_file_name), "w") as f: + f.write(self.cpp_file()) + + + def write(self, header_path, cpp_path): + self.write_header_file(header_path) + self.write_cpp_file(cpp_path) + + +def print_files(objs, header_path, cpp_path): + for obj in objs: + ObjFile(obj).write(header_path, cpp_path) diff --git a/telegram/CMakeLists.txt b/telegram/CMakeLists.txt new file mode 100644 index 000000000000..bdaf7af2321b --- /dev/null +++ b/telegram/CMakeLists.txt @@ -0,0 +1,30 @@ +project(userver-telegram CXX) + +file(GLOB_RECURSE SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.hpp +) + +file(GLOB_RECURSE UNIT_TEST_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/*_test.cpp +) +list(REMOVE_ITEM SOURCES ${UNIT_TEST_SOURCES}) + +add_library(${PROJECT_NAME} STATIC ${SOURCES}) + +target_link_libraries(${PROJECT_NAME} + PUBLIC + userver-core + PRIVATE + userver-uboost-coro +) +target_include_directories( + ${PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) +target_include_directories (${PROJECT_NAME} PRIVATE + $ +) diff --git a/telegram/include/userver/telegram/bot/client/client.hpp b/telegram/include/userver/telegram/bot/client/client.hpp new file mode 100644 index 000000000000..03cc199b41f2 --- /dev/null +++ b/telegram/include/userver/telegram/bot/client/client.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +class Client { + public: + ~Client() = default; + + virtual CloseRequest Close(const RequestOptions& request_options) = 0; + + virtual CopyMessageRequest CopyMessage( + const CopyMessageMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual ForwardMessageRequest ForwardMessage( + const ForwardMessageMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual GetChatRequest GetChat( + const GetChatMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual GetFileRequest GetFile( + const GetFileMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual GetMeRequest GetMe(const RequestOptions& request_options) = 0; + + virtual GetUpdatesRequest GetUpdates( + const GetUpdatesMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual LogOutRequest LogOut(const RequestOptions& request_options) = 0; + + virtual SendAnimationRequest SendAnimation( + const SendAnimationMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendAudioRequest SendAudio( + const SendAudioMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendChatActionRequest SendChatAction( + const SendChatActionMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendContactRequest SendContact( + const SendContactMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendDiceRequest SendDice( + const SendDiceMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendDocumentRequest SendDocument( + const SendDocumentMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendLocationRequest SendLocation( + const SendLocationMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendMessageRequest SendMessage( + const SendMessageMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendPhotoRequest SendPhoto( + const SendPhotoMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendPollRequest SendPoll( + const SendPollMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendVenueRequest SendVenue( + const SendVenueMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendVideoNoteRequest SendVideoNote( + const SendVideoNoteMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendVideoRequest SendVideo( + const SendVideoMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; + + virtual SendVoiceRequest SendVoice( + const SendVoiceMethod::Parameters& parameters, + const RequestOptions& request_options) = 0; +}; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/client/client_fwd.hpp b/telegram/include/userver/telegram/bot/client/client_fwd.hpp new file mode 100644 index 000000000000..754fdc24c6fb --- /dev/null +++ b/telegram/include/userver/telegram/bot/client/client_fwd.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +class Client; +using ClientPtr = std::shared_ptr; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/components/client.hpp b/telegram/include/userver/telegram/bot/components/client.hpp new file mode 100644 index 000000000000..6d376de0604b --- /dev/null +++ b/telegram/include/userver/telegram/bot/components/client.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +class TelegramBotClient : public components::LoggableComponentBase { +public: + static constexpr std::string_view kName = "telegram-bot-client"; + + TelegramBotClient(const components::ComponentConfig& config, + const components::ComponentContext& context); + + static yaml_config::Schema GetStaticConfigSchema(); + + ClientPtr GetClient(); + +private: + ClientPtr client_; +}; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/components/long_poller.hpp b/telegram/include/userver/telegram/bot/components/long_poller.hpp new file mode 100644 index 000000000000..70af411edec2 --- /dev/null +++ b/telegram/include/userver/telegram/bot/components/long_poller.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +class TelegramBotLongPoller : public components::LoggableComponentBase { +public: + static constexpr std::string_view kName = "telegram-bot-long-poller"; + + TelegramBotLongPoller(const components::ComponentConfig& config, + const components::ComponentContext& context); + + virtual void HandleUpdate(Update update, ClientPtr client) = 0; + + void OnAllComponentsLoaded() override; + + void OnAllComponentsAreStopping() override; + + static yaml_config::Schema GetStaticConfigSchema(); + +private: + void FetchAndHandleUpdates(); + + std::vector FetchUpdates(); + + ClientPtr client_; + + std::int64_t offset_ = 0; + + const std::chrono::milliseconds polling_frequency_; + const std::chrono::milliseconds polling_timeout_; + + utils::PeriodicTask periodic_; + + concurrent::BackgroundTaskStorage task_storage_; +}; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/close.hpp b/telegram/include/userver/telegram/bot/requests/close.hpp new file mode 100644 index 000000000000..0498fa435e8e --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/close.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to close the bot instance before moving it from one +/// local server to another. +/// @note You need to delete the webhook before calling this method to ensure +/// that the bot isn't launched again after server restart. +/// @note The method will return error 429 in the first 10 minutes after the +/// bot is launched. +/// @see https://core.telegram.org/bots/api#close +struct CloseMethod { + static constexpr std::string_view kName = "close"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + /// @brief Requires no parameters. + struct Parameters{}; + + /// @brief Returns True on success. + using Reply = bool; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +CloseMethod::Parameters Parse(const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const CloseMethod::Parameters& parameters, + formats::serialize::To); + +using CloseRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/copy_message.hpp b/telegram/include/userver/telegram/bot/requests/copy_message.hpp new file mode 100644 index 000000000000..86237704472f --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/copy_message.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to copy messages of any kind. +/// @note Service messages and invoice messages can't be copied. +/// @note A quiz poll can be copied only if the value of the field +/// correct_option_id is known to the bot. +/// @note The method is analogous to the method ForwardMessage, but the copied +/// message doesn't have a link to the original message. +/// @see https://core.telegram.org/bots/api#copymessage +struct CopyMessageMethod { + static constexpr std::string_view kName = "copyMessage"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + Parameters(ChatId _chat_id, + ChatId _from_chat_id, + std::int64_t _message_id); + + /// @brief Unique identifier for the target chat or username of the target + /// channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Unique identifier for the chat where the original message was + /// sent (or channel username in the format @channelusername). + ChatId from_chat_id; + + /// @brief Message identifier in the chat specified in from_chat_id. + std::int64_t message_id{}; + + /// @brief New caption for media, 0-1024 characters after entities parsing. + /// @note If not specified, the original caption is kept. + std::optional caption; + + /// @brief Mode for parsing entities in the new caption. + /// @see https://core.telegram.org/bots/api#formatting-options + /// for more details. + std::optional parse_mode; + + /// @brief List of special entities that appear in the new caption, + /// which can be specified instead of parse_mode. + std::optional> caption_entities; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding + /// and saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + /// @see https://core.telegram.org/bots/features#inline-keyboards + /// @see https://core.telegram.org/bots/features#keyboards + std::optional reply_markup; + }; + + /// @brief Returns the MessageId of the sent message on success. + using Reply = MessageId; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +CopyMessageMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const CopyMessageMethod::Parameters& parameters, + formats::serialize::To); + +using CopyMessageRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/forward_message.hpp b/telegram/include/userver/telegram/bot/requests/forward_message.hpp new file mode 100644 index 000000000000..dbc600f51321 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/forward_message.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to forward messages of any kind. +/// @note Service messages can't be forwarded. +/// @see https://core.telegram.org/bots/api#forwardmessage +struct ForwardMessageMethod { + static constexpr std::string_view kName = "forwardMessage"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + Parameters(ChatId _chat_id, + ChatId _from_chat_id, + std::int64_t _message_id); + + /// @brief Unique identifier for the target chat or username of the + /// target channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Unique identifier for the chat where the original message was + /// sent (or channel username in the format @channelusername) + ChatId from_chat_id; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the forwarded message from + /// forwarding and saving. + std::optional protect_content; + + /// @brief Message identifier in the chat specified in from_chat_id. + std::int64_t message_id{}; + }; + + /// @note Service messages can't be forwarded. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +ForwardMessageMethod::Parameters Parse(const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const ForwardMessageMethod::Parameters& parameters, + formats::serialize::To); + +using ForwardMessageRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/get_chat.hpp b/telegram/include/userver/telegram/bot/requests/get_chat.hpp new file mode 100644 index 000000000000..fedf415dc664 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/get_chat.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to get up to date information about the chat +/// (current name of the user for one-on-one conversations, current username +/// of a user, group or channel, etc.). +/// @see https://core.telegram.org/bots/api#getchat +struct GetChatMethod { + static constexpr std::string_view kName = "getChat"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kGet; + + struct Parameters{ + Parameters(ChatId _chat_id); + + /// @brief Unique identifier for the target chat or username of the + /// target supergroup or channel (in the format @channelusername). + ChatId chat_id; + }; + + /// @brief Returns a Chat object on success. + using Reply = Chat; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +GetChatMethod::Parameters Parse(const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const GetChatMethod::Parameters& parameters, + formats::serialize::To); + +using GetChatRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/get_file.hpp b/telegram/include/userver/telegram/bot/requests/get_file.hpp new file mode 100644 index 000000000000..88a4af663552 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/get_file.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to get basic information about a file and prepare +/// it for downloading. +/// @note For the moment, bots can download files of up to 20MB in size. +/// @note The file can then be downloaded via DownloadFile. The file will be +/// available by this file_path within one hour. After an hour, you can request +/// a new one by calling GetFile again. +/// @note This function may not preserve the original file name and MIME type. +/// You should save the file's MIME type and name (if available) when the File +/// object is received. +/// @see https://core.telegram.org/bots/api#getfile +struct GetFileMethod { + static constexpr std::string_view kName = "getFile"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kGet; + + struct Parameters{ + Parameters(std::string _file_id); + + /// @brief File identifier to get information about + std::string file_id; + }; + + /// @brief On success, a File object is returned. + using Reply = File; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +GetFileMethod::Parameters Parse(const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const GetFileMethod::Parameters& parameters, + formats::serialize::To); + +using GetFileRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/get_me.hpp b/telegram/include/userver/telegram/bot/requests/get_me.hpp new file mode 100644 index 000000000000..2340310a614d --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/get_me.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief A simple method for testing your bot's authentication token. +struct GetMeMethod { + static constexpr std::string_view kName = "getMe"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kGet; + + /// @brief Requires no parameters. + struct Parameters{}; + + /// @brief Returns basic information about the bot in form of a User object. + using Reply = User; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +GetMeMethod::Parameters Parse(const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const GetMeMethod::Parameters& parameters, + formats::serialize::To); + +using GetMeRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/get_updates.hpp b/telegram/include/userver/telegram/bot/requests/get_updates.hpp new file mode 100644 index 000000000000..74698fccc168 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/get_updates.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to receive incoming updates using long polling. +/// @note This method will not work if an outgoing webhook is set up. +/// @note In order to avoid getting duplicate updates, recalculate offset after +/// each server response. +/// @see https://core.telegram.org/bots/api#getupdates +/// @see https://en.wikipedia.org/wiki/Push_technology#Long_polling +struct GetUpdatesMethod { + static constexpr std::string_view kName = "getUpdates"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kGet; + + struct Parameters{ + /// @brief Identifier of the first update to be returned. + /// @note Must be greater by one than the highest among the identifiers of + /// previously received updates. + /// @note By default, updates starting with the earliest unconfirmed update + /// are returned. An update is considered confirmed as soon as GetUpdates is + /// called with an offset higher than its update_id. + /// @note The negative offset can be specified to retrieve updates starting + /// from -offset update from the end of the updates queue. + /// All previous updates will be forgotten. + std::optional offset; + + /// @brief Limits the number of updates to be retrieved. + /// Values between 1-100 are accepted. Defaults to 100. + std::optional limit; + + /// @brief Timeout in seconds for long polling. Defaults to 0, i.e. usual + /// short polling. + /// @note Should be positive, short polling should be used for testing + /// purposes only. + std::optional timeout; + + enum class UpdateType { + kMessage, kEditedMessage, kChannelPost, kEditedChannelPost, kInlineQuery, + kChosenInlineResult, kCallbackQuery, kShippingQuery, kPreCheckoutQuery, + kPoll, kPollAnswer, kMyChatMember, kChatMember, kChatJoinRequest + }; + + /// @brief List of the update types you want your bot to receive. + /// @note Specify an empty list to receive all update types except + /// chat_member (default). + /// @note If not specified, the previous setting will be used. + /// @note Please note that this parameter doesn't affect updates created + /// before the call to the getUpdates, so unwanted updates may be received + /// for a short period of time. + std::optional> allowed_updates; + }; + + /// @brief Returns an Array of Update objects + using Reply = std::vector; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +std::string_view ToString(GetUpdatesMethod::Parameters::UpdateType update_type); + +GetUpdatesMethod::Parameters::UpdateType Parse( + const formats::json::Value& value, + formats::parse::To); + +formats::json::Value Serialize( + GetUpdatesMethod::Parameters::UpdateType update_type, + formats::serialize::To); + +GetUpdatesMethod::Parameters Parse(const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const GetUpdatesMethod::Parameters& parameters, + formats::serialize::To); + +using GetUpdatesRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/log_out.hpp b/telegram/include/userver/telegram/bot/requests/log_out.hpp new file mode 100644 index 000000000000..32ac989059a0 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/log_out.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to log out from the cloud Bot API server before +/// launching the bot locally. +/// @note You must log out the bot before running it locally, otherwise there +/// is no guarantee that the bot will receive updates. +/// @note After a successful call, you can immediately log in on a local server, +/// but will not be able to log in back to the cloud Bot API server for +/// 10 minutes. +/// @see https://core.telegram.org/bots/api#logout +struct LogOutMethod { + static constexpr std::string_view kName = "logOut"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + /// @brief Requires no parameters. + struct Parameters{}; + + /// @brief Returns True on success. + using Reply = bool; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +LogOutMethod::Parameters Parse(const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const LogOutMethod::Parameters& parameters, + formats::serialize::To); + +using LogOutRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/request.hpp b/telegram/include/userver/telegram/bot/requests/request.hpp new file mode 100644 index 000000000000..3c37967ab561 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/request.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// todo +struct RequestOptions { + /// todo + std::chrono::milliseconds timeout = std::chrono::milliseconds{500}; + + /// todo + size_t retries = 2; +}; + +template +class [[nodiscard]] Request { + public: + using Method = method; + + Request(clients::http::Request&& request, + std::string_view base_url, + std::string_view bot_token, + const RequestOptions& request_options, + const typename Method::Parameters& parameters) + : http_request_(std::move(request)) { + SetUrl(base_url, bot_token); + SetRequestOptions(request_options); + Method::FillRequestData(http_request_, parameters); + } + + typename Method::Reply Perform() { + auto response = http_request_.perform(); + return Method::ParseResponseData(*response); + } + + private: + void SetRequestOptions(const RequestOptions& request_options) { + http_request_.timeout(request_options.timeout) + .retry(request_options.retries); + } + + void SetUrl(std::string_view base_url, std::string_view raw_token) { + Token token{raw_token}; + + http_request_.url(GetFullUrl(base_url, token.GetToken())) + .SetLoggedUrl(GetFullUrl(base_url, token.GetHiddenToken())); + } + + std::string GetFullUrl(std::string_view base_url, + std::string_view token, + bool is_test_env = false) { + if (!is_test_env) { + return fmt::format("{}/bot{}/{}", base_url, token, Method::kName); + } else { + return fmt::format("{}/bot{}/test/{}", base_url, token, Method::kName); + } + } + + clients::http::Request http_request_; +}; + + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_animation.hpp b/telegram/include/userver/telegram/bot/requests/send_animation.hpp new file mode 100644 index 000000000000..48dec36d5913 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_animation.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send animation files (GIF or H.264/MPEG-4 AVC +/// video without sound). +/// @see https://core.telegram.org/bots/api#sendanimation +struct SendAnimationMethod { + static constexpr std::string_view kName = "sendAnimation"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + using Animation = std::variant; + + Parameters(ChatId _chat_id, Animation _animation); + + /// @brief Unique identifier for the target chat or username of the target + /// channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Animation to send. + /// 1. Pass a file_id as std::string to send a animation that exists on the + /// Telegram servers (recommended) + /// 2. Pass an HTTP URL as a std::string to get a animation from the + /// Internet. + /// 3. Pass InputFile to upload a new animation. + /// @note Bots can currently send animation files of up to 50 MB in size, + /// this limit may be changed in the future. + /// @see https://core.telegram.org/bots/api#sending-files + Animation animation; + + /// @brief Duration of sent animation in seconds. + std::optional duration; + + /// @brief Animation width. + std::optional width; + + /// @brief Animation height. + std::optional height; + + /// @brief Thumbnail of the file sent; + /// @note Can be ignored if thumbnail generation for the file is supported + /// server-side. + /// @note The thumbnail should be in JPEG format and less than 200 kB in + /// size. A thumbnail's width and height should not exceed 320. + /// @note Thumbnails can't be reused and can be only uploaded as a new file. + /// @see https://core.telegram.org/bots/api#sending-files + std::unique_ptr thumbnail; + + /// @brief Animation caption (may also be used when resending animation + /// by file_id), 0-1024 characters after entities parsing. + std::optional caption; + + /// @brief Mode for parsing entities in the animation caption. + /// @see https://core.telegram.org/bots/api#formatting-options + /// for more details. + std::optional parse_mode; + + /// @brief List of special entities that appear in the caption, which can + /// be specified instead of parse_mode. + std::optional> caption_entities; + + /// @brief Pass True if the animation needs to be covered with a spoiler + /// animation. + std::optional has_spoiler; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding + /// and saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + std::optional reply_markup; + }; + + /// @brief On success, the sent Message is returned. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendAnimationMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize( + const SendAnimationMethod::Parameters& parameters, + formats::serialize::To); + +using SendAnimationRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_audio.hpp b/telegram/include/userver/telegram/bot/requests/send_audio.hpp new file mode 100644 index 000000000000..4b203fd0caa2 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_audio.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send audio files, if you want Telegram clients +/// to display them in the music player. +/// @note For sending voice messages, use the sendVoice method instead. +/// @see https://core.telegram.org/bots/api#sendaudio +struct SendAudioMethod { + static constexpr std::string_view kName = "sendAudio"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + using Audio = std::variant; + + Parameters(ChatId _chat_id, Audio _audio); + + /// @brief Unique identifier for the target chat or username of the + /// target channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only, + std::optional message_thread_id; + + /// @brief Audio file to send. + /// 1. Pass a file_id as std::string to send a audio that exists on the + /// Telegram servers (recommended) + /// 2. Pass an HTTP URL as a std::string to get a audio from the + /// Internet. + /// 3. Pass InputFile to upload a new audio. + /// @note Your audio must be in the .MP3 or .M4A format. + /// @note Bots can currently send audio files of up to 50 MB in size, this + /// limit may be changed in the future. + /// @see https://core.telegram.org/bots/api#sending-files + Audio audio; + + /// @brief Audio caption, 0-1024 characters after entities parsing. + std::optional caption; + + /// @brief Mode for parsing entities in the animation caption. + /// @see https://core.telegram.org/bots/api#formatting-options + /// for more details. + std::optional parse_mode; + + /// @brief List of special entities that appear in the caption, which can + // be specified instead of parse_mode. + std::optional> caption_entities; + + /// @brief Duration of the audio in seconds. + std::optional duration; + + /// @brief Performer. + std::optional performer; + + /// @brief Track name. + std::optional title; + + /// @brief Thumbnail of the file sent; + /// @note Can be ignored if thumbnail generation for the file is supported + /// server-side. + /// @note The thumbnail should be in JPEG format and less than 200 kB in + /// size. A thumbnail's width and height should not exceed 320. + /// @note Thumbnails can't be reused and can be only uploaded as a new file. + /// @see https://core.telegram.org/bots/api#sending-files + std::unique_ptr thumbnail; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding + /// and saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + std::optional reply_markup; + }; + + /// @brief On success, the sent Message is returned. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendAudioMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const SendAudioMethod::Parameters& parameters, + formats::serialize::To); + +using SendAudioRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_chat_action.hpp b/telegram/include/userver/telegram/bot/requests/send_chat_action.hpp new file mode 100644 index 000000000000..ac798d934ccf --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_chat_action.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method when you need to tell the user that something is +/// happening on the bot's side. +/// @note The status is set for 5 seconds or less (when a message arrives +/// from your bot, Telegram clients clear its typing status). +/// @example The ImageBot (https://t.me/imagebot) needs some time to process +/// a request and upload the image. Instead of sending a text message along the +/// lines of “Retrieving image, please wait…”, the bot may use sendChatAction +/// with action = Action::kUploadPhoto. The user will see a "sending photo" +/// status for the bot. +/// @note We only recommend using this method when a response from the bot will +/// take a noticeable amount of time to arrive. +/// @see https://core.telegram.org/bots/api#sendchataction +struct SendChatActionMethod { + static constexpr std::string_view kName = "sendChatAction"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + /// @brief Type of action to broadcast. + enum class Action { + kTyping, ///< For text messages + kUploadPhoto, ///< For photos + kRecordVideo, ///< For videos + kUploadVideo, ///< For videos + kRecordVoice, ///< For voice notes + kUploadVoice, ///< For voice notes + kUploadDocument, ///< For general file + kChooseSticker, ///< For stickers + kFindLocation, ///< For location data + kRecordVideoNote, ///< For video notes + kUploadVideoNote ///< For video notes + }; + + Parameters(ChatId _chat_id, Action _action); + + /// @brief Unique identifier for the target chat or username of the + /// target channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread. + /// @note For supergroups only + std::optional message_thread_id; + + /// @brief Type of action to broadcast. + Action action; + }; + + /// @brief Returns True on success. + using Reply = bool; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +std::string_view ToString(SendChatActionMethod::Parameters::Action action); + +SendChatActionMethod::Parameters::Action Parse( + const formats::json::Value& value, + formats::parse::To); + +formats::json::Value Serialize( + SendChatActionMethod::Parameters::Action action, + formats::serialize::To); + +SendChatActionMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize( + const SendChatActionMethod::Parameters& parameters, + formats::serialize::To); + +using SendChatActionRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_contact.hpp b/telegram/include/userver/telegram/bot/requests/send_contact.hpp new file mode 100644 index 000000000000..e223ef899872 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_contact.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send phone contacts. +/// @see https://core.telegram.org/bots/api#sendcontact +struct SendContactMethod { + static constexpr std::string_view kName = "sendContact"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + Parameters(ChatId _chat_id, + std::string _phone_number, + std::string _first_name); + + /// @brief Unique identifier for the target chat or username of the target + /// channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Contact's phone number. + std::string phone_number; + + /// @brief Contact's first name. + std::string first_name; + + /// @brief Contact's last name. + std::optional last_name; + + /// @brief Additional data about the contact in the form of a vCard, + /// 0-2048 bytes. + /// @see https://en.wikipedia.org/wiki/VCard + std::optional vcard; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding + /// and saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + /// @see https://core.telegram.org/bots/features#inline-keyboards + /// @see https://core.telegram.org/bots/features#keyboards + std::optional reply_markup; + }; + + /// @brief On success, the sent Message is returned. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendContactMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const SendContactMethod::Parameters& parameters, + formats::serialize::To); + +using SendContactRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_dice.hpp b/telegram/include/userver/telegram/bot/requests/send_dice.hpp new file mode 100644 index 000000000000..52013c436a5a --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_dice.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send an animated emoji that will display a +/// random value. +/// @see https://core.telegram.org/bots/api#senddice +struct SendDiceMethod { + static constexpr std::string_view kName = "sendDice"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + Parameters(ChatId _chat_id); + + /// @brief Unique identifier for the target chat or username of the + /// target channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Emoji on which the dice throw animation is based. + /// @note Currently, must be one of “🎲”, “🎯”, “🏀”, “⚽”, “🎳”, or “🎰”. + /// Dice can have values 1-6 for “🎲”, “🎯” and “🎳”, values 1-5 for “🏀” + /// and “⚽”, and values 1-64 for “🎰”. + /// @note Defaults to “🎲”. + std::optional emoji; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + /// @see https://core.telegram.org/bots/features#inline-keyboards + /// @see https://core.telegram.org/bots/features#keyboards + std::optional reply_markup; + }; + + /// @note On success, the sent Message is returned. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendDiceMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const SendDiceMethod::Parameters& parameters, + formats::serialize::To); + +using SendDiceRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_document.hpp b/telegram/include/userver/telegram/bot/requests/send_document.hpp new file mode 100644 index 000000000000..003733f84ab1 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_document.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send general files. +/// @see https://core.telegram.org/bots/api#senddocument +struct SendDocumentMethod { + static constexpr std::string_view kName = "sendDocument"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + using Document = std::variant; + + Parameters(ChatId _chat_id, Document _document); + + /// @brief Unique identifier for the target chat or username of the target + /// channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief File to send. + /// 1. Pass a file_id as std::string to send a file that exists on the + /// Telegram servers (recommended) + /// 2. Pass an HTTP URL as a std::string to get a file from the + /// Internet. + /// 3. Pass InputFile to upload a new file. + /// @note Bots can currently send files of any type of up to 50 MB in size, + /// this limit may be changed in the future. + /// @see https://core.telegram.org/bots/api#sending-files + Document document; + + /// @brief Thumbnail of the file sent; + /// @note Can be ignored if thumbnail generation for the file is supported + /// server-side. + /// @note The thumbnail should be in JPEG format and less than 200 kB in + /// size. A thumbnail's width and height should not exceed 320. + /// @note Thumbnails can't be reused and can be only uploaded as a new file. + /// @see https://core.telegram.org/bots/api#sending-files + std::unique_ptr thumbnail; + + /// @brief Document caption (may also be used when resending documents by + /// file_id), 0-1024 characters after entities parsing. + std::optional caption; + + /// @brief Mode for parsing entities in the animation caption. + /// @see https://core.telegram.org/bots/api#formatting-options + /// for more details. + std::optional parse_mode; + + /// @brief List of special entities that appear in the caption, which can + /// be specified instead of parse_mode. + std::optional> caption_entities; + + /// @brief Disables automatic server-side content type detection for files + /// uploaded using InputFile. + std::optional disable_content_type_detection; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding and + /// saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + std::optional reply_markup; + }; + + /// @brief On success, the sent Message is returned. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendDocumentMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const SendDocumentMethod::Parameters& parameters, + formats::serialize::To); + +using SendDocumentRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_location.hpp b/telegram/include/userver/telegram/bot/requests/send_location.hpp new file mode 100644 index 000000000000..038d96f82e4d --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_location.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send point on the map. +/// @see https://core.telegram.org/bots/api#sendlocation +struct SendLocationMethod { + static constexpr std::string_view kName = "sendLocation"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + Parameters(ChatId _chat_id, + double _latitude, + double _longitude); + + /// @brief Unique identifier for the target chat or username of the + /// target channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) + /// of the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Latitude of the location. + double latitude{}; + + /// @brief Longitude of the location. + double longitude{}; + + /// @brief The radius of uncertainty for the location, measured in meters; + /// 0-1500 + std::optional horizontal_accuracy; + + /// @brief Period in seconds for which the location will be updated. + /// @note Should be between 60 and 86400. + /// @see https://telegram.org/blog/live-locations + std::optional live_period; + + /// @brief Direction in which the user is moving, in degrees. + /// @note For live locations + /// @note Must be between 1 and 360 if specified. + std::optional heading; + + /// @brief A maximum distance for proximity alerts about approaching + /// another chat member, in meters. + /// @note For live locations + /// @note Must be between 1 and 100000 if specified. + std::optional proximity_alert_radius; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding + /// and saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + /// @see https://core.telegram.org/bots/features#inline-keyboards + /// @see https://core.telegram.org/bots/features#keyboards + std::optional reply_markup; + }; + + /// @brief On success, the sent Message is returned. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendLocationMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const SendLocationMethod::Parameters& parameters, + formats::serialize::To); + +using SendLocationRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_message.hpp b/telegram/include/userver/telegram/bot/requests/send_message.hpp new file mode 100644 index 000000000000..af752bdb5d4b --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_message.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send text messages. +/// @see https://core.telegram.org/bots/api#sendmessage +struct SendMessageMethod { + static constexpr std::string_view kName = "sendMessage"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + Parameters(ChatId _chat_id, std::string _text); + + /// @brief Unique identifier for the target chat or username of the + /// target channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) + // of the forum; + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Text of the message to be sent, 1-4096 characters after + /// entities parsing. + std::string text; + + /// @brief Mode for parsing entities in the message text. + /// @see https://core.telegram.org/bots/api#formatting-options for more + /// details. + std::optional parse_mode; + + /// @brief List of special entities that appear in message text, which can + /// be specified instead of parse_mode. + std::optional> entities; + + /// @brief Disables link previews for links in this message. + std::optional disable_web_page_preview; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from + /// forwarding and saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + /// @see https://core.telegram.org/bots/features#inline-keyboards + /// @see https://core.telegram.org/bots/features#keyboards + std::optional reply_markup; + }; + + /// @brief On success, the sent Message is returned. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendMessageMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const SendMessageMethod::Parameters& parameters, + formats::serialize::To); + +using SendMessageRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_photo.hpp b/telegram/include/userver/telegram/bot/requests/send_photo.hpp new file mode 100644 index 000000000000..a088d81aab49 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_photo.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send photos. On success, the sent Message is returned. +/// @see https://core.telegram.org/bots/api#sendphoto +struct SendPhotoMethod { + static constexpr std::string_view kName = "sendPhoto"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + using Photo = std::variant; + + Parameters(ChatId _chat_id, Photo _photo); + + /// @brief Unique identifier for the target chat or username of the target + /// channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Photo to send. + /// 1. Pass a file_id as std::string to send a photo that exists on the + /// Telegram servers (recommended) + /// 2. Pass an HTTP URL as a std::string to get a photo from the Internet. + /// 3. Pass InputFile to upload a new photo. + /// @note The photo must be at most 10 MB in size. The photo's width and + /// height must not exceed 10000 in total. Width and height ratio must be + /// at most 20. + /// @see https://core.telegram.org/bots/api#sending-files + Photo photo; + + /// @brief Photo caption (may also be used when resending photos by + /// file_id), 0-1024 characters after entities parsing + std::optional caption; + + /// @brief Mode for parsing entities in the photo caption. + /// @see https://core.telegram.org/bots/api#formatting-options + std::optional parse_mode; + + /// @brief List of special entities that appear in the caption, which can + /// be specified instead of parse_mode. + std::optional> caption_entities; + + /// @brief Pass True if the photo needs to be covered with a spoiler + /// animation. + std::optional has_spoiler; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding + /// and saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + /// @see https://core.telegram.org/bots/features#inline-keyboards + /// @see https://core.telegram.org/bots/features#keyboards + std::optional reply_markup; + }; + + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendPhotoMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To to); + +formats::json::Value Serialize( + const SendPhotoMethod::Parameters& parameters, + formats::serialize::To to); + +using SendPhotoRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_poll.hpp b/telegram/include/userver/telegram/bot/requests/send_poll.hpp new file mode 100644 index 000000000000..a6f307fd7209 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_poll.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send a native poll. +/// @see https://core.telegram.org/bots/api#sendpoll +struct SendPollMethod { + static constexpr std::string_view kName = "sendPoll"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + Parameters(ChatId _chat_id, + std::string _question, + std::vector _options); + + /// @brief Unique identifier for the target chat or username of the target + /// channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Poll question, 1-300 characters. + std::string question; + + /// @brief List of answer options, 2-10 strings 1-100 characters each. + std::vector options; + + /// @brief True, if the poll needs to be anonymous. + /// @note Defaults to True. + std::optional is_anonymous; + + /// @brief Poll type. + /// @note Defaults to Poll::Type::Regular. + std::optional type; + + /// @brief True, if the poll allows multiple answers, ignored for polls in + /// quiz mode. + /// @note Defaults to False. + std::optional allows_multiple_answers; + + /// @brief 0-based identifier of the correct answer option. + /// @note Required for polls in quiz mode. + std::optional correct_option_id; + + /// @brief Text that is shown when a user chooses an incorrect answer + /// or taps on the lamp icon in a quiz-style poll, 0-200 characters with + /// at most 2 line feeds after entities parsing. + std::optional explanation; + + /// @brief Mode for parsing entities in the explanation. + /// @see https://core.telegram.org/bots/api#formatting-options + /// for more details. + std::optional explanation_parse_mode; + + /// @brief AList of special entities that appear in the poll explanation, + /// which can be specified instead of parse_mode. + std::optional> explanation_entities; + + /// @brief Amount of time in seconds the poll will be active + /// after creation. + /// @note Possible values: 5s-600s + /// @note Can't be used together with close_date. + std::optional open_period; + + /// @brief Point in time (Unix timestamp) when the poll will be + /// automatically closed. + /// @note Must be at least 5 and no more than 600 seconds in the future. + /// @note Can't be used together with open_period. + std::optional close_date; + + /// @brief Pass True if the poll needs to be immediately closed. + /// @note This can be useful for poll preview. + std::optional is_closed; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding + /// and saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + /// @see https://core.telegram.org/bots/features#inline-keyboards + /// @see https://core.telegram.org/bots/features#keyboards + std::optional reply_markup; + }; + + /// @brief On success, the sent Message is returned. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendPollMethod::Parameters Parse(const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const SendPollMethod::Parameters& parameters, + formats::serialize::To); + +using SendPollRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_venue.hpp b/telegram/include/userver/telegram/bot/requests/send_venue.hpp new file mode 100644 index 000000000000..8db44980e101 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_venue.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send information about a venue. +/// @see https://core.telegram.org/bots/api#sendvenue +struct SendVenueMethod { + static constexpr std::string_view kName = "sendVenue"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + Parameters(ChatId _chat_id, + double _latitude, + double _longitude, + std::string _title, + std::string _address); + + /// @brief Unique identifier for the target chat or username of the target + /// channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Latitude of the venue. + double latitude{}; + + /// @brief Longitude of the venue. + double longitude{}; + + /// @brief Name of the venue. + std::string title; + + /// @brief Address of the venue. + std::string address; + + /// @brief Foursquare identifier of the venue. + std::optional foursquare_id; + + /// @brief Foursquare type of the venue, if known. + /// @example "arts_entertainment/default", "arts_entertainment/aquarium", + /// "food/icecream". + std::optional foursquare_type; + + /// @brief Google Places identifier of the venue. + std::optional google_place_id; + + /// @brief Google Places type of the venue. + /// @see https://developers.google.com/maps/documentation/places/web-service/supported_types + std::optional google_place_type; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding + /// and saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + /// @see https://core.telegram.org/bots/features#inline-keyboards + /// @see https://core.telegram.org/bots/features#keyboards + std::optional reply_markup; + }; + + /// @brief On success, the sent Message is returned. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendVenueMethod::Parameters Parse(const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const SendVenueMethod::Parameters& parameters, + formats::serialize::To); + +using SendVenueRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_video.hpp b/telegram/include/userver/telegram/bot/requests/send_video.hpp new file mode 100644 index 000000000000..2e488910cf3c --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_video.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send video files. +/// @see https://core.telegram.org/bots/api#sendvideo +struct SendVideoMethod { + static constexpr std::string_view kName = "sendVideo"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + using Video = std::variant; + + Parameters(ChatId _chat_id, Video _video); + + /// @brief Unique identifier for the target chat or username of the target + /// channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Video to send. + /// 1. Pass a file_id as std::string to send a video that exists on the + /// Telegram servers (recommended) + /// 2. Pass an HTTP URL as a std::string to get a video from the + /// Internet. + /// 3. Pass InputFile to upload a video file. + /// @note Telegram clients support MPEG4 videos (other formats may be sent + /// as Document). + /// @note Bots can currently send video files of up to 50 MB in size, this + /// limit may be changed in the future. + /// @see https://core.telegram.org/bots/api#sending-files + Video video; + + /// @brief Duration of sent video in seconds. + std::optional duration; + + /// @brief Video width. + std::optional width; + + /// @brief Video height. + std::optional height; + + /// @brief Thumbnail of the file sent; + /// @note Can be ignored if thumbnail generation for the file is supported + /// server-side. + /// @note The thumbnail should be in JPEG format and less than 200 kB in + /// size. A thumbnail's width and height should not exceed 320. + /// @note Thumbnails can't be reused and can be only uploaded as a new file. + /// @see https://core.telegram.org/bots/api#sending-files + std::unique_ptr thumbnail; + + /// @brief Video caption (may also be used when resending videos by + /// file_id), 0-1024 characters after entities parsing. + std::optional caption; + + /// @brief Mode for parsing entities in the animation caption. + /// @see https://core.telegram.org/bots/api#formatting-options + /// for more details. + std::optional parse_mode; + + /// @brief List of special entities that appear in the caption, which can be + /// specified instead of parse_mode. + std::optional> caption_entities; + + /// @brief Pass True if the video needs to be covered with a spoiler + /// animation. + std::optional has_spoiler; + + /// @brief Pass True if the uploaded video is suitable for streaming. + std::optional supports_streaming; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding + /// and saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + std::optional reply_markup; + }; + + /// @brief On success, the sent Message is returned. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendVideoMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const SendVideoMethod::Parameters& parameters, + formats::serialize::To); + +using SendVideoRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_video_note.hpp b/telegram/include/userver/telegram/bot/requests/send_video_note.hpp new file mode 100644 index 000000000000..56e0c7a01b64 --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_video_note.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send video messages. +/// @see https://core.telegram.org/bots/api#sendvideonote +struct SendVideoNoteMethod { + static constexpr std::string_view kName = "sendVideoNote"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + using VideoNote = std::variant; + + Parameters(ChatId _chat_id, VideoNote _video_note); + + /// @brief Unique identifier for the target chat or username of the target + /// channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Video note to send. + /// 1. Pass a file_id as std::string to send a video note that exists on the + /// Telegram servers (recommended) + /// 2. Pass InputFile to upload a new video note. + /// @note Sending video notes by a URL is currently unsupported. + /// @note As of v.4.0, Telegram clients support rounded square MPEG4 videos + /// of up to 1 minute long + /// @see https://core.telegram.org/bots/api#sending-files + VideoNote video_note; + + /// @brief Duration of sent video in seconds + std::optional duration; + + /// @brief Video width and height, i.e. diameter of the video message + std::optional length; + + /// @brief Thumbnail of the file sent; + /// @note Can be ignored if thumbnail generation for the file is supported + /// server-side. + /// @note The thumbnail should be in JPEG format and less than 200 kB in + /// size. A thumbnail's width and height should not exceed 320. + /// @note Thumbnails can't be reused and can be only uploaded as a new file. + /// @see https://core.telegram.org/bots/api#sending-files + std::unique_ptr thumbnail; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding and + /// saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + std::optional reply_markup; + }; + + /// @brief On success, the sent Message is returned. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendVideoNoteMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize( + const SendVideoNoteMethod::Parameters& parameters, + formats::serialize::To); + +using SendVideoNoteRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/requests/send_voice.hpp b/telegram/include/userver/telegram/bot/requests/send_voice.hpp new file mode 100644 index 000000000000..1f1db377937e --- /dev/null +++ b/telegram/include/userver/telegram/bot/requests/send_voice.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief Use this method to send audio files, if you want Telegram clients +/// to display the file as a playable voice message. +/// @see https://core.telegram.org/bots/api#sendvoice +struct SendVoiceMethod { + static constexpr std::string_view kName = "sendVoice"; + + static constexpr auto kHttpMethod = clients::http::HttpMethod::kPost; + + struct Parameters{ + using Voice = std::variant; + + Parameters(ChatId _chat_id, Voice _voice); + + /// @brief Unique identifier for the target chat or username of the + /// target channel (in the format @channelusername). + ChatId chat_id; + + /// @brief Unique identifier for the target message thread (topic) of + /// the forum. + /// @note For forum supergroups only. + std::optional message_thread_id; + + /// @brief Voice to send. + /// 1. Pass a file_id as std::string to send a voice that exists on the + /// Telegram servers (recommended) + /// 2. Pass an HTTP URL as a std::string to get a voice from the + /// Internet. + /// 3. Pass InputFile to upload a voice file. + /// @note For this to work, your audio must be in an .OGG file encoded + /// with OPUS (other formats may be sent as Audio or Document). + /// @note Bots can currently send voice messages of up to 50 MB in size, + /// this limit may be changed in the future. + /// @see https://core.telegram.org/bots/api#sending-files + Voice voice; + + /// @brief Voice message caption, 0-1024 characters after entities parsing. + std::optional caption; + + /// @brief Mode for parsing entities in the animation caption. + /// @see https://core.telegram.org/bots/api#formatting-options + /// for more details. + std::optional parse_mode; + + /// @brief List of special entities that appear in the caption, which can + /// be specified instead of parse_mode. + std::optional> caption_entities; + + /// @brief Duration of the voice message in seconds. + std::optional duration; + + /// @brief Sends the message silently. Users will receive a notification + /// with no sound. + std::optional disable_notification; + + /// @brief Protects the contents of the sent message from forwarding and + /// saving. + std::optional protect_content; + + /// @brief If the message is a reply, ID of the original message. + std::optional reply_to_message_id; + + /// @brief Pass True if the message should be sent even if the specified + /// replied-to message is not found. + std::optional allow_sending_without_reply; + + /// @brief Additional interface options. + std::optional reply_markup; + }; + + /// @brief On success, the sent Message is returned. + using Reply = Message; + + static void FillRequestData(clients::http::Request& request, + const Parameters& parameters); + + static Reply ParseResponseData(clients::http::Response& response); +}; + +SendVoiceMethod::Parameters Parse( + const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const SendVoiceMethod::Parameters& parameters, + formats::serialize::To); + +using SendVoiceRequest = Request; + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/types/animation.hpp b/telegram/include/userver/telegram/bot/types/animation.hpp new file mode 100644 index 000000000000..5aab4d5e077e --- /dev/null +++ b/telegram/include/userver/telegram/bot/types/animation.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief This object represents an animation file +/// (GIF or H.264/MPEG-4 AVC video without sound). +/// @see https://core.telegram.org/bots/api#animation +struct Animation { + /// @brief Identifier for this file, which can be used to download + /// or reuse the file. + std::string file_id; + + /// @brief Unique identifier for this file, which is supposed to be + /// the same over time and for different bots. + /// @note Can't be used to download or reuse the file. + std::string file_unique_id; + + /// @brief Video width as defined by sender. + std::int64_t width{}; + + /// @brief Video height as defined by sender. + std::int64_t height{}; + + /// @brief Duration of the video in seconds as defined by sender. + std::int64_t duration{}; + + /// @brief Optional. Animation thumbnail as defined by sender. + std::unique_ptr thumbnail; + + /// @brief Optional. Original animation filename as defined by sender. + std::optional file_name; + + /// @brief Optional. MIME type of the file as defined by sender. + std::optional mime_type; + + /// @brief Optional. File size in bytes. + std::optional file_size; +}; + +Animation Parse(const formats::json::Value& json, + formats::parse::To); + +formats::json::Value Serialize(const Animation& animation, + formats::serialize::To); + +} // namespace telegram::bot + +USERVER_NAMESPACE_END diff --git a/telegram/include/userver/telegram/bot/types/audio.hpp b/telegram/include/userver/telegram/bot/types/audio.hpp new file mode 100644 index 000000000000..6ec754da7b78 --- /dev/null +++ b/telegram/include/userver/telegram/bot/types/audio.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace telegram::bot { + +/// @brief This object represents an audio file to be treated as music +/// by the Telegram clients. +/// @see https://core.telegram.org/bots/api#audio +struct Audio { + /// @brief Identifier for this file, which can be used to download + /// or reuse the file. + std::string file_id; + + /// @brief Unique identifier for this file, which is supposed to be + /// the same over time and for different bots. + /// @note Can't be used to download or reuse the file. + std::string file_unique_id; + + /// @brief Duration of the audio in seconds as defined by sender. + std::int64_t duration{}; + + /// @brief Optional. Performer of the audio as defined by sender or + /// by audio tags. + std::optional performer; + + /// @brief Optional. Title of the audio as defined by sender or by audio tags. + std::optional title; + + /// @brief Optional. Original filename as defined by sender. + std::optional file_name; + + /// @brief Optional. MIME type of the file as defined by sender. + std::optional mime_type; + + /// @brief Optional. File size in bytes. + std::optional file_size; + + /// @brief Optional. Thumbnail of the album cover to which + /// the music file belongs. + std::unique_ptr thumbnail; +}; + +Audio Parse(const formats::json::Value& json, + formats::parse::To