342 lines
12 KiB
C++
342 lines
12 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);
|
|
},
|
|
[ctx](td_api::updateNewChosenInlineResult &upd) {
|
|
cmd::handle_chosen_inline_result(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;
|
|
} |