implement real /shorten command

This commit is contained in:
Slavasil 2024-10-13 01:00:56 +03:00
parent 087d70fa0f
commit f049304b1d
4 changed files with 233 additions and 9 deletions

View File

@ -1,6 +1,9 @@
#include "commands.h" #include "commands.h"
#include "curl/curl.h"
#include "curl/easy.h"
#include "td/telegram/td_api.h" #include "td/telegram/td_api.h"
#include <cstring> #include <cstring>
#include <functional>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
@ -14,6 +17,31 @@ void cmd::handle_regular_message(context *ctx, td_api::message &msg) {
if (nextSpace != std::string_view::npos) if (nextSpace != std::string_view::npos)
param.remove_suffix(param.size() - nextSpace); param.remove_suffix(param.size() - nextSpace);
spdlog::info("Command /shorten received with parameter '{}'", param); 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<td_api::sendMessage>(
chat_id, thread_id,
nullptr /*reply_to*/, nullptr /*options*/,
nullptr, // reply_markup
static_cast<td_api::object_ptr<td_api::InputMessageContent>>(td_api::make_object<td_api::inputMessageText>(
std::move(td_api::make_object<td_api::formattedText>(url, std::move(std::vector<td_api::object_ptr<td_api::textEntity>>()))),
nullptr /*link_preview_options*/, false /*clear_draft*/
))
), {});
});
if (!result) {
ctx->tg->send_query(td_api::make_object<td_api::sendMessage>(
msg.chat_id_, msg.message_thread_id_,
nullptr /*reply_to*/, nullptr /*options*/,
nullptr, // reply_markup
static_cast<td_api::object_ptr<td_api::InputMessageContent>>(td_api::make_object<td_api::inputMessageText>(
std::move(td_api::make_object<td_api::formattedText>("произошла какая-то ошибка :(", std::move(std::vector<td_api::object_ptr<td_api::textEntity>>()))),
nullptr /*link_preview_options*/, false /*clear_draft*/
))
), {});
}
} else if (std::strncmp(text.c_str(), "/shorten", 8) == 0) { } else if (std::strncmp(text.c_str(), "/shorten", 8) == 0) {
std::string textRaw("usage: /shorten <url>"); std::string textRaw("usage: /shorten <url>");
std::vector<td_api::object_ptr<td_api::textEntity>> empty; std::vector<td_api::object_ptr<td_api::textEntity>> empty;
@ -22,3 +50,28 @@ void cmd::handle_regular_message(context *ctx, td_api::message &msg) {
} }
} }
} }
bool cmd::shorten_link(std::string link, context *ctx, std::function<void(std::string)> 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;
}

View File

@ -1,8 +1,10 @@
#pragma once #pragma once
#include "common.h" #include "common.h"
#include "td/telegram/td_api.h"
#include "telegram_client.h" #include "telegram_client.h"
#include <string> #include <string>
namespace cmd { namespace cmd {
void handle_regular_message(context *ctx, td_api::message &msg); void handle_regular_message(context *ctx, td_api::message &msg);
bool shorten_link(std::string link, context *ctx, std::function<void(std::string)> cb);
} }

View File

@ -1,4 +1,8 @@
#pragma once #pragma once
#include "curl/multi.h"
#include "uv.h"
#include <functional>
#include <map>
#include <string> #include <string>
namespace detail { namespace detail {
@ -44,7 +48,19 @@ struct CommandLineParams {
struct TelegramClient; struct TelegramClient;
struct active_request {
std::vector<char> receivedData;
std::function<void(active_request&)> doneCallback;
active_request(std::function<void(active_request&)> doneCallback): doneCallback(doneCallback) {}
};
struct context { struct context {
TelegramClient *tg; TelegramClient *tg;
ApiCreds apiCreds; ApiCreds apiCreds;
CURLM *curl;
uv_timer_t curlTimer;
std::map<CURL*, active_request> requests;
}; };
size_t curl_receive_cb(char *ptr, size_t size, size_t nmemb, CURL *ctx);

165
main.cpp
View File

@ -1,12 +1,16 @@
#include <cstdlib> #include <cstdlib>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <stdexcept>
#include <string> #include <string>
#include <termios.h>
#include <unistd.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <uv.h> #include <uv.h>
#include "common.h" #include "common.h"
#include "commands.h" #include "commands.h"
#include "curl/multi.h"
#include "td/telegram/td_api.h" #include "td/telegram/td_api.h"
#include "td/telegram/td_api.hpp" #include "td/telegram/td_api.hpp"
@ -17,11 +21,16 @@ const char *APP_GREETING = "--------------------\n"\
void async_startup(uv_idle_t *h); void async_startup(uv_idle_t *h);
void configure_blocked_signals(); void configure_blocked_signals();
void configure_logging(); void configure_logging();
void configure_shutdown_signals(uv_loop_t *loop, context *ctx); std::array<std::unique_ptr<uv_signal_t, void(*)(uv_signal_t*)>, 3> configure_shutdown_signals(uv_loop_t *loop, context *ctx);
CommandLineParams parse_command_line(int argc, char **argv); CommandLineParams parse_command_line(int argc, char **argv);
void print_greeting(); void print_greeting();
void print_usage(const char *programName); void print_usage(const char *programName);
void shutdown_signal_handler(uv_signal_t *h, int signum); 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) { int main(int argc, char **argv) {
CommandLineParams cmdLineParams; CommandLineParams cmdLineParams;
@ -38,10 +47,22 @@ int main(int argc, char **argv) {
configure_blocked_signals(); configure_blocked_signals();
print_greeting(); print_greeting();
uv_loop_t *defaultLoop = uv_default_loop(); uv_loop_t *defaultLoop = uv_default_loop();
context ctx; context ctx;
ctx.apiCreds = cmdLineParams.apiCreds; 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"); spdlog::info("Creating Telegram client");
TelegramClient tg(defaultLoop); TelegramClient tg(defaultLoop);
ctx.tg = &tg; ctx.tg = &tg;
@ -51,13 +72,23 @@ int main(int argc, char **argv) {
uv_idle_init(defaultLoop, &startupHandle); uv_idle_init(defaultLoop, &startupHandle);
uv_idle_start(&startupHandle, async_startup); 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"); spdlog::info("Entering event loop");
uv_run(defaultLoop, UV_RUN_DEFAULT); uv_run(defaultLoop, UV_RUN_DEFAULT);
spdlog::info("Leaving event loop"); spdlog::info("Leaving event loop");
termSettings.c_lflag |= ECHO | ICANON;
tcsetattr(1, 0, &termSettings);
spdlog::info("Cleaning up"); spdlog::info("Cleaning up");
curl_multi_cleanup(curl);
curl_global_cleanup();
return 0; return 0;
} }
@ -95,6 +126,9 @@ void async_startup(uv_idle_t *h) {
[ctx](td_api::updateNewMessage &upd) { [ctx](td_api::updateNewMessage &upd) {
if (upd.message_->is_outgoing_) return; if (upd.message_->is_outgoing_) return;
cmd::handle_regular_message(ctx, *upd.message_); cmd::handle_regular_message(ctx, *upd.message_);
},
[ctx](td_api::updateNewInlineQuery &upd) {
}, },
[](td_api::Object &obj){} [](td_api::Object &obj){}
)); ));
@ -121,17 +155,21 @@ void configure_blocked_signals() {
sigprocmask(SIG_BLOCK, &signalSet, nullptr); 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<uv_signal_t, void(*)(uv_signal_t*)> 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) { auto deleter = [](uv_signal_t *handle) {
uv_close((uv_handle_t*)handle, [](uv_handle_t *h){ delete h; }); uv_close((uv_handle_t*)handle, [](uv_handle_t *h){ delete h; });
}; };
std::unique_ptr<uv_signal_t, decltype(deleter)> handle(new uv_signal_t, deleter); std::unique_ptr<uv_signal_t, void(*)(uv_signal_t*)> handle(new uv_signal_t, deleter);
uv_signal_init(loop, handle.get()); uv_signal_init(loop, handle.get());
uv_signal_start(handle.get(), signal_handler, signum); uv_signal_start(handle.get(), signal_handler, signum);
return handle;
} }
void configure_shutdown_signals(uv_loop_t *loop, context *ctx) { std::array<std::unique_ptr<uv_signal_t, void(*)(uv_signal_t*)>, 3> configure_shutdown_signals(uv_loop_t *loop, context *ctx) {
register_signal_handle(loop, ctx, SIGINT, shutdown_signal_handler); 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) { void shutdown_signal_handler(uv_signal_t *h, int signum) {
@ -154,3 +192,118 @@ void print_greeting() {
void print_usage(const char *programName) { void print_usage(const char *programName) {
std::cout << "usage: " << programName << " <api_id> <api_hash> <token>\n"; std::cout << "usage: " << programName << " <api_id> <api_hash> <token>\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<curl_context*>(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<context*>(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;
}