Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Telegram Bot API library and sample of a Telegram chat bot #385

Draft
wants to merge 6 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ if (USERVER_FEATURE_MYSQL)
add_subdirectory(mysql "${CMAKE_BINARY_DIR}/userver/mysql")
endif()

if (USERVER_FEATURE_CORE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this feature should be named as USERVER_FEATURE_TELEGRAM.

add_subdirectory(telegram "${CMAKE_BINARY_DIR}/userver/telegram")
endif()

if (USERVER_IS_THE_ROOT_PROJECT AND USERVER_FEATURE_CORE)
add_subdirectory(samples)
endif()
3 changes: 3 additions & 0 deletions samples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions samples/telegram_bot_service/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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()
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 76 additions & 0 deletions samples/telegram_bot_service/dynamic_config_fallback.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
}
61 changes: 61 additions & 0 deletions samples/telegram_bot_service/static_config.yaml
Original file line number Diff line number Diff line change
@@ -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
182 changes: 182 additions & 0 deletions samples/telegram_bot_service/telegram_bot_service.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#include <userver/testsuite/testsuite_support.hpp>
#include <userver/utest/using_namespace_userver.hpp>

#include <userver/clients/dns/component.hpp>
#include <userver/clients/http/component.hpp>
#include <userver/components/component.hpp>
#include <userver/components/minimal_server_component_list.hpp>
#include <userver/fs/read.hpp>
#include <userver/server/handlers/tests_control.hpp>
#include <userver/utils/daemon_run.hpp>
#include <userver/yaml_config/merge_schemas.hpp>

#include <userver/telegram/bot/client/client.hpp>
#include <userver/telegram/bot/components/client.hpp>
#include <userver/telegram/bot/components/long_poller.hpp>

#include <codecvt>
#include <locale>

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<std::string>& username) const;

static std::string GreetingCaption(
const std::optional<std::string>& username);

static std::vector<telegram::bot::MessageEntity> GreetingMessageEntities(
std::string_view caption);

telegram::bot::SendPhotoMethod::Parameters::Photo GreetingPhoto() const;

static std::shared_ptr<std::string> ReadPhoto(
const components::ComponentConfig& config,
const components::ComponentContext& context);

telegram::bot::RequestOptions request_options_;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field could be moved inside of components clients and provided within client.hpp.
It looks statically configurable and may be provided by ComponentConfig or dynamic_config

mutable rcu::Variable<std::string> photo_file_id_;
const std::shared_ptr<std::string> 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<telegram::bot::TelegramBotLongPoller>(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<std::string>& 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<std::string>& 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<telegram::bot::MessageEntity> GreetUser::GreetingMessageEntities(
std::string_view caption) {
std::vector<telegram::bot::MessageEntity> result;
telegram::bot::MessageEntity& linkEntity = result.emplace_back();

std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> 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<std::string> GreetUser::ReadPhoto(
const components::ComponentConfig& config,
const components::ComponentContext& context) {
const auto fs_tp_name = config["fs-task-processor"].As<std::string>();
const auto greeting_photo_path = config["greeting-photo-path"]
.As<std::string>();
auto& fs_task_processor = context.GetTaskProcessor(fs_tp_name);
return std::make_shared<std::string>(
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<telegram::bot::TelegramBotClient>()
.Append<samples::telegram_bot::GreetUser>()
.Append<clients::dns::Component>()
.Append<components::HttpClient>()
.Append<components::TestsuiteSupport>()
.Append<server::handlers::TestsControl>();
return utils::DaemonMain(argc, argv, component_list);
}
Loading
Loading