From f049304b1d134c846825846aafc14b33589b86ff Mon Sep 17 00:00:00 2001 From: Slavasil Date: Sun, 13 Oct 2024 01:00:56 +0300 Subject: [PATCH] implement real /shorten command --- commands.cpp | 53 ++++++++++++++++ commands.h | 2 + common.h | 18 +++++- main.cpp | 169 ++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 233 insertions(+), 9 deletions(-) diff --git a/commands.cpp b/commands.cpp index e09cb93..c833bca 100644 --- a/commands.cpp +++ b/commands.cpp @@ -1,6 +1,9 @@ #include "commands.h" +#include "curl/curl.h" +#include "curl/easy.h" #include "td/telegram/td_api.h" #include +#include #include #include #include @@ -14,6 +17,31 @@ void cmd::handle_regular_message(context *ctx, td_api::message &msg) { if (nextSpace != std::string_view::npos) param.remove_suffix(param.size() - nextSpace); spdlog::info("Command /shorten received with parameter '{}'", param); + + // TODO check URL validity + + bool result = shorten_link(std::string(param), ctx, [tg = ctx->tg, chat_id = msg.chat_id_, thread_id = msg.message_thread_id_](std::string url){ + tg->send_query(td_api::make_object( + chat_id, thread_id, + nullptr /*reply_to*/, nullptr /*options*/, + nullptr, // reply_markup + static_cast>(td_api::make_object( + std::move(td_api::make_object(url, std::move(std::vector>()))), + nullptr /*link_preview_options*/, false /*clear_draft*/ + )) + ), {}); + }); + if (!result) { + ctx->tg->send_query(td_api::make_object( + msg.chat_id_, msg.message_thread_id_, + nullptr /*reply_to*/, nullptr /*options*/, + nullptr, // reply_markup + static_cast>(td_api::make_object( + std::move(td_api::make_object("произошла какая-то ошибка :(", std::move(std::vector>()))), + nullptr /*link_preview_options*/, false /*clear_draft*/ + )) + ), {}); + } } else if (std::strncmp(text.c_str(), "/shorten", 8) == 0) { std::string textRaw("usage: /shorten "); std::vector> empty; @@ -21,4 +49,29 @@ void cmd::handle_regular_message(context *ctx, td_api::message &msg) { ctx->tg->send_query(td_api::make_object(msg.chat_id_, msg.message_thread_id_, nullptr, nullptr, nullptr, std::move(text)), {}); } } +} + +bool cmd::shorten_link(std::string link, context *ctx, std::function cb) { + spdlog::debug("creating CURL request"); + CURL *req = curl_easy_init(); + if (!req) { + return false; + } + char *escapedParam = curl_easy_escape(req, link.data(), link.size()); + std::string url("https://slavasil.ru/create?url="); + url += escapedParam; + curl_free(escapedParam); + curl_easy_setopt(req, CURLOPT_URL, url.c_str()); + curl_easy_setopt(req, CURLOPT_WRITEFUNCTION, curl_receive_cb); + curl_easy_setopt(req, CURLOPT_WRITEDATA, req); + curl_easy_setopt(req, CURLOPT_PRIVATE, ctx); + curl_easy_setopt(req, CURLOPT_FOLLOWLOCATION, 1); + ctx->requests.emplace(req, [cb, req, ctx](active_request &r){ + std::string shortenedUrl(r.receivedData.data(), r.receivedData.size()); + spdlog::info("Received data from HTTP server: {}", shortenedUrl); + cb(shortenedUrl); + ctx->requests.erase(req); + }); + CURLMcode r = curl_multi_add_handle(ctx->curl, req); + return r == CURLM_OK; } \ No newline at end of file diff --git a/commands.h b/commands.h index 62ed557..b929515 100644 --- a/commands.h +++ b/commands.h @@ -1,8 +1,10 @@ #pragma once #include "common.h" +#include "td/telegram/td_api.h" #include "telegram_client.h" #include namespace cmd { void handle_regular_message(context *ctx, td_api::message &msg); + bool shorten_link(std::string link, context *ctx, std::function cb); } \ No newline at end of file diff --git a/common.h b/common.h index d7bce24..823c904 100644 --- a/common.h +++ b/common.h @@ -1,4 +1,8 @@ #pragma once +#include "curl/multi.h" +#include "uv.h" +#include +#include #include namespace detail { @@ -44,7 +48,19 @@ struct CommandLineParams { struct TelegramClient; +struct active_request { + std::vector receivedData; + std::function doneCallback; + + active_request(std::function doneCallback): doneCallback(doneCallback) {} +}; + struct context { TelegramClient *tg; ApiCreds apiCreds; -}; \ No newline at end of file + CURLM *curl; + uv_timer_t curlTimer; + std::map requests; +}; + +size_t curl_receive_cb(char *ptr, size_t size, size_t nmemb, CURL *ctx); \ No newline at end of file diff --git a/main.cpp b/main.cpp index b20db4c..6513d33 100644 --- a/main.cpp +++ b/main.cpp @@ -1,12 +1,16 @@ #include #include #include +#include #include +#include +#include #include #include #include "common.h" #include "commands.h" +#include "curl/multi.h" #include "td/telegram/td_api.h" #include "td/telegram/td_api.hpp" @@ -17,11 +21,16 @@ const char *APP_GREETING = "--------------------\n"\ void async_startup(uv_idle_t *h); void configure_blocked_signals(); void configure_logging(); -void configure_shutdown_signals(uv_loop_t *loop, context *ctx); +std::array, 3> configure_shutdown_signals(uv_loop_t *loop, context *ctx); CommandLineParams parse_command_line(int argc, char **argv); void print_greeting(); void print_usage(const char *programName); void shutdown_signal_handler(uv_signal_t *h, int signum); +int curl_socket_cb(CURL*, curl_socket_t, int, context*, void*); +int curl_timeout_cb(CURLM*, long, context*); +void uv_socket_cb(uv_poll_t *handle, int status, int events); +void uv_curl_timeout_cb(uv_timer_t *handle); +void check_curl_multi_info(context *ctx); int main(int argc, char **argv) { CommandLineParams cmdLineParams; @@ -38,26 +47,48 @@ int main(int argc, char **argv) { configure_blocked_signals(); print_greeting(); + uv_loop_t *defaultLoop = uv_default_loop(); context ctx; ctx.apiCreds = cmdLineParams.apiCreds; - + + spdlog::info("Initializing CURL"); + curl_global_init(CURL_GLOBAL_DEFAULT); + CURLM *curl = curl_multi_init(); + ctx.curl = curl; + curl_multi_setopt(curl, CURLMOPT_SOCKETFUNCTION, &curl_socket_cb); + curl_multi_setopt(curl, CURLMOPT_SOCKETDATA, &ctx); + curl_multi_setopt(curl, CURLMOPT_TIMERFUNCTION, &curl_timeout_cb); + curl_multi_setopt(curl, CURLMOPT_TIMERDATA, &ctx); + uv_timer_init(defaultLoop, &ctx.curlTimer); + ctx.curlTimer.data = &ctx; + spdlog::info("Creating Telegram client"); TelegramClient tg(defaultLoop); ctx.tg = &tg; - + uv_idle_t startupHandle; startupHandle.data = (void*)&ctx; uv_idle_init(defaultLoop, &startupHandle); uv_idle_start(&startupHandle, async_startup); - configure_shutdown_signals(defaultLoop, &ctx); + auto signalHandles = configure_shutdown_signals(defaultLoop, &ctx); + + termios termSettings; + tcgetattr(1, &termSettings); + termSettings.c_lflag &= ~(ECHO | ICANON); + tcsetattr(1, 0, &termSettings); spdlog::info("Entering event loop"); uv_run(defaultLoop, UV_RUN_DEFAULT); spdlog::info("Leaving event loop"); + termSettings.c_lflag |= ECHO | ICANON; + tcsetattr(1, 0, &termSettings); + spdlog::info("Cleaning up"); + curl_multi_cleanup(curl); + curl_global_cleanup(); return 0; } @@ -95,6 +126,9 @@ void async_startup(uv_idle_t *h) { [ctx](td_api::updateNewMessage &upd) { if (upd.message_->is_outgoing_) return; cmd::handle_regular_message(ctx, *upd.message_); + }, + [ctx](td_api::updateNewInlineQuery &upd) { + }, [](td_api::Object &obj){} )); @@ -121,17 +155,21 @@ void configure_blocked_signals() { sigprocmask(SIG_BLOCK, &signalSet, nullptr); } -void register_signal_handle(uv_loop_t *loop, context *ctx, int signum, void(*signal_handler)(uv_signal_t*, int)) { +std::unique_ptr register_signal_handle(uv_loop_t *loop, context *ctx, int signum, void(*signal_handler)(uv_signal_t*, int)) { + spdlog::debug("register signal handler for {}", signum); auto deleter = [](uv_signal_t *handle) { uv_close((uv_handle_t*)handle, [](uv_handle_t *h){ delete h; }); }; - std::unique_ptr handle(new uv_signal_t, deleter); + std::unique_ptr handle(new uv_signal_t, deleter); uv_signal_init(loop, handle.get()); uv_signal_start(handle.get(), signal_handler, signum); + return handle; } -void configure_shutdown_signals(uv_loop_t *loop, context *ctx) { - register_signal_handle(loop, ctx, SIGINT, shutdown_signal_handler); +std::array, 3> configure_shutdown_signals(uv_loop_t *loop, context *ctx) { + return {register_signal_handle(loop, ctx, SIGINT, shutdown_signal_handler), + register_signal_handle(loop, ctx, SIGQUIT, shutdown_signal_handler), + register_signal_handle(loop, ctx, SIGTERM, shutdown_signal_handler)}; } void shutdown_signal_handler(uv_signal_t *h, int signum) { @@ -153,4 +191,119 @@ void print_greeting() { void print_usage(const char *programName) { std::cout << "usage: " << programName << " \n"; +} + +struct curl_context { + uv_poll_t *pollHandle; + curl_socket_t socketFd; + context *ctx; + + curl_context(uv_loop_t *loop, context *ctx, int fd) { + spdlog::debug("creating new curl context for socket {}", fd); + pollHandle = new uv_poll_t; + uv_poll_init(loop, pollHandle, fd); + pollHandle->data = this; + socketFd = fd; + this->ctx = ctx; + } + ~curl_context() { + spdlog::debug("destroying curl context for socket {}", socketFd); + uv_close((uv_handle_t*)pollHandle, [](uv_handle_t *h){ delete h; }); + } +}; + +int curl_socket_cb(CURL *curl, curl_socket_t s, int action, context* ctx, void* socketPtr) { + (void)curl; + spdlog::debug("curl socket callback: {}", action); + curl_context *curlCtx; + switch (action) { + case CURL_POLL_IN: + case CURL_POLL_INOUT: + case CURL_POLL_OUT: { + curlCtx = socketPtr ? (curl_context*)socketPtr : new curl_context(uv_default_loop(), ctx, s); + curl_multi_assign(ctx->curl, s, curlCtx); + int events = 0; + if (action != CURL_POLL_IN) events |= UV_WRITABLE; + if (action != CURL_POLL_OUT) events |= UV_READABLE; + uv_poll_start(curlCtx->pollHandle, events, uv_socket_cb); + break; + } + case CURL_POLL_REMOVE: + if (socketPtr) { + uv_poll_stop(((curl_context*)socketPtr)->pollHandle); + curl_multi_assign(ctx->curl, s, NULL); + delete (curl_context*)socketPtr; + } + } + return 0; +} + +int curl_timeout_cb(CURLM* curl, long timeout, context* ctx) { + (void)curl; + spdlog::debug("curl timeout change: {} ms", timeout); + if (timeout < 0) uv_timer_stop(&ctx->curlTimer); + else { + if (timeout == 0) timeout = 1; + uv_timer_start(&ctx->curlTimer, uv_curl_timeout_cb, timeout, 0); + } + return 0; +} + +void uv_socket_cb(uv_poll_t *handle, int status, int events) { + spdlog::debug("libuv: socket {}", events & UV_READABLE ? (events & UV_WRITABLE ? "readable+writable" : "readable") : "writable"); + (void)status; + auto ctx = reinterpret_cast(handle->data); + int flags = 0; + if (events & UV_READABLE) flags |= CURL_CSELECT_IN; + if (events & UV_WRITABLE) flags |= CURL_CSELECT_OUT; + int runningHandles; + curl_multi_socket_action(ctx->ctx->curl, ctx->socketFd, flags, &runningHandles); + check_curl_multi_info(ctx->ctx); +} + +void uv_curl_timeout_cb(uv_timer_t *handle) { + spdlog::debug("libuv: curl timeout ended"); + auto ctx = reinterpret_cast(handle->data); + if (ctx) { + int runningHandles; + curl_multi_socket_action(ctx->curl, CURL_SOCKET_TIMEOUT, 0, &runningHandles); + check_curl_multi_info(ctx); + } +} + + +void check_curl_multi_info(context *ctx) { + CURLMsg *message; + int pending; + while ((message = curl_multi_info_read(ctx->curl, &pending))) { + switch (message->msg) { + case CURLMSG_DONE: { + spdlog::debug("curl transfer done"); + curl_multi_remove_handle(ctx->curl, message->easy_handle); + curl_easy_cleanup(message->easy_handle); + try { + active_request &req = ctx->requests.at(message->easy_handle); + req.doneCallback(req); + } catch (std::out_of_range) {} + } + default: + break; + } + } +} + +size_t curl_receive_cb(char *ptr, size_t size, size_t nmemb, CURL *curl) { + spdlog::debug("received {} bytes from server: '{}'", nmemb, std::string(ptr, nmemb)); + + context *ctx = nullptr; + curl_easy_getinfo(curl, CURLINFO_PRIVATE, &ctx); + spdlog::debug("ctx pointer: {}", (size_t)ctx); + try { + active_request &req = ctx->requests.at(curl); + req.receivedData.insert(req.receivedData.end(), ptr, ptr + nmemb); + } catch (std::out_of_range) { + spdlog::error("No associated request data for handle {}", (size_t)curl); + } + + return nmemb; } \ No newline at end of file