shortener-bot/main.cpp
2024-10-14 01:26:31 +03:00

339 lines
11 KiB
C++

#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <sys/select.h>
#include <termios.h>
#include <unistd.h>
#include <spdlog/spdlog.h>
#include <uv.h>
#include "common.h"
#include "commands.h"
#include "curl/multi.h"
#include "td/telegram/td_api.h"
#include "td/telegram/td_api.hpp"
const char *APP_GREETING = "--------------------\n"\
"| shortener-bot v0 |\n"\
"--------------------\n\n";
void async_startup(uv_idle_t *h);
void configure_blocked_signals();
void configure_logging();
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);
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);
void gc_pending_queries(uv_timer_t *handle);
int main(int argc, char **argv) {
CommandLineParams cmdLineParams;
try {
cmdLineParams = parse_command_line(argc, argv);
} catch (CommandLineParamError e) {
switch (e) {
case CommandLineParamError::NoApiCreds:
print_usage(argv[0]);
return 1;
}
}
configure_logging();
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);
uv_timer_t pendingQueryGcTimer;
uv_timer_init(defaultLoop, &pendingQueryGcTimer);
pendingQueryGcTimer.data = &ctx;
uv_timer_start(&pendingQueryGcTimer, gc_pending_queries, 60000, 60000);
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;
}
void async_startup(uv_idle_t *h) {
context *ctx = (context*)h->data;
spdlog::info("Starting telegram client (using api id {}, hash {})", ctx->apiCreds.id, ctx->apiCreds.hash);
ctx->tg->add_update_handler([ctx](void*, td_api::Object &update){
td_api::downcast_call(update, overloaded(
[ctx](td_api::updateAuthorizationState &upd) {
td_api::downcast_call(*upd.authorization_state_, overloaded(
[ctx](td_api::authorizationStateWaitTdlibParameters &state) {
auto request = td_api::make_object<td_api::setTdlibParameters>();
request->database_directory_ = "tdata";
request->use_message_database_ = false;
request->use_secret_chats_ = false;
request->api_id_ = ctx->apiCreds.id;
request->api_hash_ = ctx->apiCreds.hash;
request->system_language_code_ = "en";
request->device_model_ = "server";
request->application_version_ = "1.0.0";
ctx->tg->send_query(std::move(request), {});
},
[ctx](td_api::authorizationStateWaitPhoneNumber &state) {
ctx->tg->send_query(td_api::make_object<td_api::checkAuthenticationBotToken>(ctx->apiCreds.botToken), {}); // TODO add error handler
},
[ctx](td_api::authorizationStateReady &state) {
spdlog::info("Telegram ready");
},
[ctx,&upd](td_api::Object &obj){
spdlog::debug("unknown authorization state ID {}", upd.authorization_state_->get_id());
}
));
},
[ctx](td_api::updateNewMessage &upd) {
if (upd.message_->is_outgoing_) return;
cmd::handle_regular_message(ctx, *upd.message_);
},
[ctx](td_api::updateNewInlineQuery &upd) {
cmd::handle_inline_query(ctx, upd);
},
[ctx](td_api::updateNewInlineCallbackQuery &upd) {
cmd::handle_callback_query(ctx, upd);
},
[](td_api::Object &obj){}
));
}, nullptr);
ctx->tg->start();
uv_close((uv_handle_t*)h, nullptr);
}
void configure_logging() {
auto loggingSink = std::make_shared<spdlog::sinks::ansicolor_sink<spdlog::details::console_mutex>>(stdout, spdlog::color_mode::automatic);
loggingSink->set_color(spdlog::level::debug, "\033[38;5;61m");
auto logger = std::make_shared<spdlog::logger>("main", loggingSink);
spdlog::set_default_logger(logger);
spdlog::set_level(spdlog::level::debug);
}
void configure_blocked_signals() {
sigset_t signalSet;
sigemptyset(&signalSet);
sigaddset(&signalSet, SIGUSR1);
sigaddset(&signalSet, SIGUSR2);
sigaddset(&signalSet, SIGHUP);
sigprocmask(SIG_BLOCK, &signalSet, nullptr);
}
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) {
uv_close((uv_handle_t*)handle, [](uv_handle_t *h){ delete h; });
};
std::unique_ptr<uv_signal_t, void(*)(uv_signal_t*)> handle(new uv_signal_t, deleter);
uv_signal_init(loop, handle.get());
uv_signal_start(handle.get(), signal_handler, signum);
return handle;
}
std::array<std::unique_ptr<uv_signal_t, void(*)(uv_signal_t*)>, 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) {
spdlog::info("Stopping");
uv_stop(uv_default_loop());
}
CommandLineParams parse_command_line(int argc, char **argv) {
if (argc < 4) throw CommandLineParamError::NoApiCreds;
char *apiIdEnd = nullptr;
int apiId = strtol(argv[1], &apiIdEnd, 10);
if (apiIdEnd == argv[1]) throw CommandLineParamError::NoApiCreds;
return CommandLineParams {apiId, std::string(argv[2]), std::string(argv[3])};
}
void print_greeting() {
std::cout << APP_GREETING << std::flush;
}
void print_usage(const char *programName) {
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;
}
void gc_pending_queries(uv_timer_t *handle) {
context *ctx = reinterpret_cast<context*>(handle->data);
std::map<uint64_t, pending_inline_query> oldQueries(ctx->inlineQueries);
std::map<uint64_t, pending_inline_query> newQueries;
auto now = std::chrono::steady_clock::now();
for (auto i = oldQueries.begin(); i != oldQueries.end(); i++) {
if (i->second.ttl > now) {
newQueries.insert(*i);
} else {
spdlog::debug("deleting a pending query {} that expired {} seconds ago", i->first, std::chrono::duration_cast<std::chrono::seconds>(now - i->second.ttl).count());
}
}
ctx->inlineQueries = newQueries;
}